Logging: Enable downloading log files singularly and in bulk (#41801)
Add functionality and UI for downloading log files directly from WC Admin. Fixes #40645
This commit is contained in:
parent
0f3c4ea4bd
commit
aaff5a0f43
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add functionality and UI for downloading log files directly from WC Admin
|
|
@ -4115,7 +4115,7 @@ table.wc_shipping {
|
|||
|
||||
.wc-backbone-modal-main article {
|
||||
padding: 0 32px 32px 32px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.wc-backbone-modal-main header{
|
||||
|
@ -4124,7 +4124,7 @@ table.wc_shipping {
|
|||
|
||||
.wc-backbone-modal-main footer {
|
||||
padding: 20px 32px 12px 32px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.wc-backbone-modal-main .wc-backbone-modal-header h1 {
|
||||
|
@ -4168,7 +4168,7 @@ table.wc_shipping {
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.wc-shipping-method-add-class-costs {
|
||||
|
@ -4186,7 +4186,7 @@ table.wc_shipping {
|
|||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
|
||||
&:checked + label {
|
||||
|
@ -4237,7 +4237,7 @@ table.wc_shipping {
|
|||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.wc-shipping-zone-method-fields {
|
||||
|
||||
& > label {
|
||||
|
@ -4258,7 +4258,7 @@ table.wc_shipping {
|
|||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
|
||||
input,
|
||||
input,
|
||||
select {
|
||||
margin: 6px 0;
|
||||
padding: 12px;
|
||||
|
@ -4362,7 +4362,7 @@ table {
|
|||
.edit {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
|
||||
.edit > input,
|
||||
.edit > select {
|
||||
width: 100%;
|
||||
|
@ -4390,7 +4390,7 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
.wc-shipping-class-hide-sibling-view + .view {
|
||||
.wc-shipping-class-hide-sibling-view + .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -4402,7 +4402,7 @@ table {
|
|||
display: inline-block;
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-shipping-zone-heading-help-text {
|
||||
max-width: 800px;
|
||||
|
|
|
@ -179,6 +179,15 @@ class File {
|
|||
return fclose( $this->stream );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full absolute path of the file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path(): string {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the file, with extension, but without full path.
|
||||
*
|
||||
|
|
|
@ -4,6 +4,7 @@ declare( strict_types = 1 );
|
|||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use PclZip;
|
||||
use WC_Cache_Helper;
|
||||
use WP_Error;
|
||||
|
||||
|
@ -345,6 +346,68 @@ class FileController {
|
|||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a single file to the browser without zipping it first.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function export_single_file( $file_id ) {
|
||||
$file = $this->get_file_by_id( $file_id );
|
||||
|
||||
if ( is_wp_error( $file ) ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$file_name = $file->get_file_id() . '.log';
|
||||
$exporter = new FileExporter( $file->get_path(), $file_name );
|
||||
|
||||
return $exporter->emit_file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a zip archive of log files and stream it to the browser.
|
||||
*
|
||||
* @param array $file_ids An array of file IDs (file basename without the hash).
|
||||
*
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function export_multiple_files( array $file_ids ) {
|
||||
$files = $this->get_files_by_id( $file_ids );
|
||||
|
||||
if ( count( $files ) < 1 ) {
|
||||
return new WP_Error(
|
||||
'wc_logs_invalid_file',
|
||||
__( 'Could not access the specified files.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$temp_dir = get_temp_dir();
|
||||
|
||||
if ( ! is_dir( $temp_dir ) || ! wp_is_writable( $temp_dir ) ) {
|
||||
return new WP_Error(
|
||||
'wc_logs_invalid_directory',
|
||||
__( 'Could not write to the temp directory. Try downloading files one at a time instead.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
|
||||
|
||||
$path = trailingslashit( $temp_dir ) . 'woocommerce_logs_' . gmdate( 'Y-m-d_H-i-s' ) . '.zip';
|
||||
$file_paths = array_map(
|
||||
fn( $file ) => $file->get_path(),
|
||||
$files
|
||||
);
|
||||
$archive = new PclZip( $path );
|
||||
|
||||
$archive->create( $file_paths, PCLZIP_OPT_REMOVE_ALL_PATH );
|
||||
|
||||
$exporter = new FileExporter( $path );
|
||||
|
||||
return $exporter->emit_file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search within a set of log files for a particular string.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Filesystem_Direct;
|
||||
|
||||
/**
|
||||
* FileExport class.
|
||||
*/
|
||||
class FileExporter {
|
||||
/**
|
||||
* The number of bytes per read while streaming the file.
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
private const CHUNK_SIZE = 4 * KB_IN_BYTES;
|
||||
|
||||
/**
|
||||
* The absolute path of the file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* A name of the file to send to the browser rather than the filename part of the path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $alternate_filename;
|
||||
|
||||
/**
|
||||
* Class FileExporter.
|
||||
*
|
||||
* @param string $path The absolute path of the file.
|
||||
* @param string $alternate_filename Optional. The name of the file to send to the browser rather than the filename
|
||||
* part of the path.
|
||||
*/
|
||||
public function __construct( string $path, string $alternate_filename = '' ) {
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) {
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->alternate_filename = $alternate_filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure PHP and stream the file to the browser.
|
||||
*
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function emit_file() {
|
||||
global $wp_filesystem;
|
||||
if ( ! $wp_filesystem->is_file( $this->path ) || ! $wp_filesystem->is_readable( $this->path ) ) {
|
||||
return new WP_Error(
|
||||
'wc_logs_invalid_file',
|
||||
__( 'Could not access file.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
// These configuration tweaks are copied from WC_CSV_Exporter::send_headers().
|
||||
// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
if ( function_exists( 'gc_enable' ) ) {
|
||||
gc_enable(); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gc_enableFound
|
||||
}
|
||||
if ( function_exists( 'apache_setenv' ) ) {
|
||||
@apache_setenv( 'no-gzip', '1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv
|
||||
}
|
||||
@ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
|
||||
@ini_set( 'output_buffering', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
|
||||
@ini_set( 'output_handler', '' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
|
||||
ignore_user_abort( true );
|
||||
wc_set_time_limit();
|
||||
wc_nocache_headers();
|
||||
// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
|
||||
|
||||
$this->send_headers();
|
||||
$this->send_contents();
|
||||
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HTTP headers at the beginning of a file.
|
||||
*
|
||||
* Modeled on WC_CSV_Exporter::send_headers().
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function send_headers(): void {
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename=' . $this->get_filename() );
|
||||
header( 'Pragma: no-cache' );
|
||||
header( 'Expires: 0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the contents of the file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function send_contents(): void {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
|
||||
$stream = fopen( $this->path, 'rb' );
|
||||
|
||||
while ( is_resource( $stream ) && ! feof( $stream ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread -- No suitable alternative.
|
||||
$chunk = fread( $stream, self::CHUNK_SIZE );
|
||||
|
||||
if ( is_string( $chunk ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputting to file.
|
||||
echo $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
fclose( $stream );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the file that will be sent to the browser.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_filename(): string {
|
||||
if ( $this->alternate_filename ) {
|
||||
return $this->alternate_filename;
|
||||
}
|
||||
|
||||
return basename( $this->path );
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@ class FileListTable extends WP_List_Table {
|
|||
*/
|
||||
protected function get_bulk_actions(): array {
|
||||
return array(
|
||||
'export' => esc_html__( 'Download', 'woocommerce' ),
|
||||
'delete' => esc_html__( 'Delete permanently', 'woocommerce' ),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -196,22 +196,28 @@ class PageController {
|
|||
$rotations = $this->file_controller->get_file_rotations( $file->get_file_id() );
|
||||
$rotation_url_base = add_query_arg( 'view', 'single_file', $this->get_logs_tab_url() );
|
||||
|
||||
$delete_url = add_query_arg(
|
||||
$download_url = add_query_arg(
|
||||
array(
|
||||
'action' => 'export',
|
||||
'file_id' => array( $file->get_file_id() ),
|
||||
),
|
||||
wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' )
|
||||
);
|
||||
$delete_url = add_query_arg(
|
||||
array(
|
||||
'action' => 'delete',
|
||||
'file_id' => array( $file->get_file_id() ),
|
||||
),
|
||||
wp_nonce_url( $this->get_logs_tab_url(), 'bulk-log-files' )
|
||||
);
|
||||
|
||||
$stream = $file->get_stream();
|
||||
$line_number = 1;
|
||||
|
||||
$delete_confirmation_js = sprintf(
|
||||
"return window.confirm( '%s' )",
|
||||
esc_js( __( 'Delete this log file permanently?', 'woocommerce' ) )
|
||||
);
|
||||
|
||||
$stream = $file->get_stream();
|
||||
$line_number = 1;
|
||||
|
||||
?>
|
||||
<header id="logs-header" class="wc-logs-header">
|
||||
<h2>
|
||||
|
@ -255,6 +261,14 @@ class PageController {
|
|||
</nav>
|
||||
<?php endif; ?>
|
||||
<div class="wc-logs-single-file-actions">
|
||||
<?php
|
||||
// Download button.
|
||||
printf(
|
||||
'<a href="%1$s" class="button button-secondary">%2$s</a>',
|
||||
esc_url( $download_url ),
|
||||
esc_html__( 'Download', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
<?php
|
||||
// Delete button.
|
||||
printf(
|
||||
|
@ -464,6 +478,17 @@ class PageController {
|
|||
}
|
||||
|
||||
switch ( $action ) {
|
||||
case 'export':
|
||||
if ( 1 === count( $file_ids ) ) {
|
||||
$export_error = $this->file_controller->export_single_file( reset( $file_ids ) );
|
||||
} else {
|
||||
$export_error = $this->file_controller->export_multiple_files( $file_ids );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $export_error ) ) {
|
||||
wp_die( wp_kses_post( $export_error ) );
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
$sendback = add_query_arg( 'deleted', $deleted, $sendback );
|
||||
|
|
Loading…
Reference in New Issue