Merged in feature/280-dev-dev01 (pull request #21)
auto-patch 280-dev-dev01-2024-01-19T16_41_58 * auto-patch 280-dev-dev01-2024-01-19T16_41_58
This commit is contained in:
@@ -126,7 +126,7 @@ class Homescreen {
|
||||
if (
|
||||
( 'US' === $country_code && $is_jetpack_installed )
|
||||
||
|
||||
( ! in_array( $country_code, array( 'CA', 'AU', 'GB', 'ES', 'IT', 'DE', 'FR', 'MX', 'CO', 'CL', 'AR', 'PE', 'BR', 'UY', 'GT', 'NL', 'AT', 'BE' ), true ) )
|
||||
( ! in_array( $country_code, array( 'CA', 'AU', 'NZ', 'SG', 'HK', 'GB', 'ES', 'IT', 'DE', 'FR', 'CL', 'AR', 'PE', 'BR', 'UY', 'GT', 'NL', 'AT', 'BE' ), true ) )
|
||||
||
|
||||
( 'US' === $country_code && false === $is_jetpack_installed && false === $is_wcs_installed )
|
||||
) {
|
||||
|
||||
@@ -92,36 +92,9 @@ class Loader {
|
||||
*/
|
||||
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
|
||||
|
||||
add_action( 'admin_init', array( __CLASS__, 'deactivate_wc_admin_plugin' ) );
|
||||
|
||||
add_action( 'load-themes.php', array( __CLASS__, 'add_appearance_theme_view_tracks_event' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* If WooCommerce Admin is installed and activated, it will attempt to deactivate and show a notice.
|
||||
*/
|
||||
public static function deactivate_wc_admin_plugin() {
|
||||
$plugin_path = PluginsHelper::get_plugin_path_from_slug( 'woocommerce-admin' );
|
||||
if ( is_plugin_active( $plugin_path ) ) {
|
||||
$path = PluginsHelper::get_plugin_path_from_slug( 'woocommerce-admin' );
|
||||
deactivate_plugins( $path );
|
||||
$notice_action = is_network_admin() ? 'network_admin_notices' : 'admin_notices';
|
||||
add_action(
|
||||
$notice_action,
|
||||
function() {
|
||||
echo '<div class="error"><p>';
|
||||
printf(
|
||||
/* translators: %s: is referring to the plugin's name. */
|
||||
esc_html__( 'The %1$s plugin has been deactivated as the latest improvements are now included with the %2$s plugin.', 'woocommerce' ),
|
||||
'<code>WooCommerce Admin</code>',
|
||||
'<code>WooCommerce</code>'
|
||||
);
|
||||
echo '</p></div>';
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns breadcrumbs for the current page.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use WP_Filesystem_Direct;
|
||||
|
||||
/**
|
||||
@@ -35,9 +36,9 @@ class File {
|
||||
/**
|
||||
* The date the file was created, as a Unix timestamp, derived from the filename.
|
||||
*
|
||||
* @var int|false
|
||||
* @var int
|
||||
*/
|
||||
protected $created = false;
|
||||
protected $created = 0;
|
||||
|
||||
/**
|
||||
* The hash property of the file, derived from the filename.
|
||||
@@ -59,13 +60,16 @@ class File {
|
||||
* @param string $path The absolute path of the file.
|
||||
*/
|
||||
public function __construct( $path ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
|
||||
global $wp_filesystem;
|
||||
|
||||
if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) {
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->parse_filename();
|
||||
$this->ingest_path();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,51 +83,155 @@ class File {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the log filename to derive certain properties of the file.
|
||||
* Parse a path to a log file to determine if it uses the standard filename structure and various properties.
|
||||
*
|
||||
* This makes assumptions about the structure of the log file's name. Using `-` to separate the name into segments,
|
||||
* if there are at least 5 segments, it assumes that the last segment is the hash, and the three segments before
|
||||
* that make up the date when the file was created in YYYY-MM-DD format. Any segments left after that are the
|
||||
* "source" that generated the log entries. If the filename doesn't have enough segments, it falls back to the
|
||||
* source and the hash both being the entire filename, and using the inode change time as the creation date.
|
||||
* if there are at least 5 segments, it assumes that the last segment is the hash, and the three segments before
|
||||
* that make up the date when the file was created in YYYY-MM-DD format. Any segments left after that are the
|
||||
* "source" that generated the log entries. If the filename doesn't have enough segments, it falls back to the
|
||||
* source and the hash both being the entire filename, and using the inode change time as the creation date.
|
||||
*
|
||||
* Example:
|
||||
* my-custom-plugin.2-2025-01-01-a1b2c3d4e5f.log
|
||||
* | | | |
|
||||
* 'my-custom-plugin' | '2025-01-01' |
|
||||
* (source) | (created) |
|
||||
* '2' 'a1b2c3d4e5f'
|
||||
* (rotation) (hash)
|
||||
* Example:
|
||||
* my-custom-plugin.2-2025-01-01-a1b2c3d4e5f.log
|
||||
* | | | |
|
||||
* 'my-custom-plugin' | '2025-01-01' |
|
||||
* (source) | (created) |
|
||||
* '2' 'a1b2c3d4e5f'
|
||||
* (rotation) (hash)
|
||||
*
|
||||
* @param string $path The full path of the log file.
|
||||
*
|
||||
* @return array {
|
||||
* @type string $dirname The directory structure containing the file. See pathinfo().
|
||||
* @type string $basename The filename with extension. See pathinfo().
|
||||
* @type string $extension The file extension. See pathinfo().
|
||||
* @type string $filename The filename without extension. See pathinfo().
|
||||
* @type string $source The source of the log entries contained in the file.
|
||||
* @type int|null $rotation The 0-based incremental rotation marker, if the file has been rotated.
|
||||
* Should only be a single digit.
|
||||
* @type int $created The date the file was created, as a Unix timestamp.
|
||||
* @type string $hash The hash suffix of the filename that protects from direct access.
|
||||
* @type string $file_id The public ID of the log file (filename without the hash).
|
||||
* }
|
||||
*/
|
||||
public static function parse_path( string $path ): array {
|
||||
$defaults = array(
|
||||
'dirname' => '',
|
||||
'basename' => '',
|
||||
'extension' => '',
|
||||
'filename' => '',
|
||||
'source' => '',
|
||||
'rotation' => null,
|
||||
'created' => 0,
|
||||
'hash' => '',
|
||||
'file_id' => '',
|
||||
);
|
||||
|
||||
$parsed = array_merge( $defaults, pathinfo( $path ) );
|
||||
|
||||
$segments = explode( '-', $parsed['filename'] );
|
||||
$timestamp = strtotime( implode( '-', array_slice( $segments, -4, 3 ) ) );
|
||||
|
||||
if ( count( $segments ) >= 5 && false !== $timestamp ) {
|
||||
$parsed['source'] = implode( '-', array_slice( $segments, 0, -4 ) );
|
||||
$parsed['created'] = $timestamp;
|
||||
$parsed['hash'] = array_slice( $segments, -1 )[0];
|
||||
} else {
|
||||
$parsed['source'] = implode( '-', $segments );
|
||||
}
|
||||
|
||||
$rotation_marker = strrpos( $parsed['source'], '.', -1 );
|
||||
if ( false !== $rotation_marker ) {
|
||||
$rotation = substr( $parsed['source'], -1 );
|
||||
if ( is_numeric( $rotation ) ) {
|
||||
$parsed['rotation'] = intval( $rotation );
|
||||
}
|
||||
|
||||
$parsed['source'] = substr( $parsed['source'], 0, $rotation_marker );
|
||||
}
|
||||
|
||||
$parsed['file_id'] = static::generate_file_id(
|
||||
$parsed['source'],
|
||||
$parsed['rotation'],
|
||||
$parsed['created']
|
||||
);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a public ID for a log file based on its properties.
|
||||
*
|
||||
* The file ID is the basename of the file without the hash part. It allows us to identify a file without revealing
|
||||
* its full name in the filesystem, so that it's difficult to access the file directly with an HTTP request.
|
||||
*
|
||||
* @param string $source The source of the log entries contained in the file.
|
||||
* @param int|null $rotation Optional. The 0-based incremental rotation marker, if the file has been rotated.
|
||||
* Should only be a single digit.
|
||||
* @param int $created Optional. The date the file was created, as a Unix timestamp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_file_id( string $source, ?int $rotation = null, int $created = 0 ): string {
|
||||
$file_id = static::sanitize_source( $source );
|
||||
|
||||
if ( ! is_null( $rotation ) ) {
|
||||
$file_id .= '.' . $rotation;
|
||||
}
|
||||
|
||||
if ( $created > 0 ) {
|
||||
$file_id .= '-' . gmdate( 'Y-m-d', $created );
|
||||
}
|
||||
|
||||
return $file_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash to use as the suffix on a log filename.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_hash( string $file_id ): string {
|
||||
$key = Constants::get_constant( 'AUTH_SALT' ) ?? 'wc-logs';
|
||||
|
||||
return hash_hmac( 'md5', $file_id, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the source property of a log file.
|
||||
*
|
||||
* @param string $source The source of the log entries contained in the file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_source( string $source ): string {
|
||||
return sanitize_file_name( $source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the log file path and assign various properties to this class instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function parse_filename(): void {
|
||||
$info = pathinfo( $this->path );
|
||||
$filename = $info['filename'];
|
||||
$segments = explode( '-', $filename );
|
||||
protected function ingest_path(): void {
|
||||
$parsed_path = static::parse_path( $this->path );
|
||||
$this->source = $parsed_path['source'];
|
||||
$this->rotation = $parsed_path['rotation'];
|
||||
$this->created = $parsed_path['created'];
|
||||
$this->hash = $parsed_path['hash'];
|
||||
}
|
||||
|
||||
if ( count( $segments ) >= 5 ) {
|
||||
$this->source = implode( '-', array_slice( $segments, 0, -4 ) );
|
||||
$this->created = strtotime( implode( '-', array_slice( $segments, -4, 3 ) ) );
|
||||
$this->hash = array_slice( $segments, -1 )[0];
|
||||
} else {
|
||||
$this->source = implode( '-', $segments );
|
||||
$this->created = filectime( $this->path );
|
||||
$this->hash = $this->source;
|
||||
}
|
||||
|
||||
$rotation_marker = strrpos( $this->source, '.', -1 );
|
||||
if ( false !== $rotation_marker ) {
|
||||
$rotation = substr( $this->source, -1 );
|
||||
if ( is_numeric( $rotation ) ) {
|
||||
$this->rotation = intval( $rotation );
|
||||
}
|
||||
|
||||
$this->source = substr( $this->source, 0, $rotation_marker );
|
||||
if ( count( $segments ) < 5 ) {
|
||||
$this->hash = $this->source;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if the filename structure is in the expected format.
|
||||
*
|
||||
* @see parse_path().
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_standard_filename(): bool {
|
||||
return ! ! $this->get_hash();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,11 +261,15 @@ class File {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a read-only stream file this file.
|
||||
* Open a read-only stream for this file.
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
public function get_stream() {
|
||||
if ( ! $this->is_readable() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! is_resource( $this->stream ) ) {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
|
||||
$this->stream = fopen( $this->path, 'rb' );
|
||||
@@ -166,6 +278,28 @@ class File {
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the stream for this file.
|
||||
*
|
||||
* The stream will also close automatically when the class instance destructs, but this can be useful for
|
||||
* avoiding having a large number of streams open simultaneously.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function close_stream(): bool {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
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.
|
||||
*
|
||||
@@ -205,21 +339,19 @@ class File {
|
||||
/**
|
||||
* Get the file's public ID.
|
||||
*
|
||||
* The file ID is the basename of the file without the hash part. It allows us to identify a file without revealing
|
||||
* its full name in the filesystem, so that it's difficult to access the file directly with an HTTP request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_file_id(): string {
|
||||
$file_id = $this->get_source();
|
||||
|
||||
if ( ! is_null( $this->get_rotation() ) ) {
|
||||
$file_id .= '.' . $this->get_rotation();
|
||||
$created = 0;
|
||||
if ( $this->has_standard_filename() ) {
|
||||
$created = $this->get_created_timestamp();
|
||||
}
|
||||
|
||||
if ( $this->get_source() !== $this->get_hash() ) {
|
||||
$file_id .= '-' . gmdate( 'Y-m-d', $this->get_created_timestamp() );
|
||||
}
|
||||
$file_id = static::generate_file_id(
|
||||
$this->get_source(),
|
||||
$this->get_rotation(),
|
||||
$created
|
||||
);
|
||||
|
||||
return $file_id;
|
||||
}
|
||||
@@ -227,9 +359,13 @@ class File {
|
||||
/**
|
||||
* Get the file's created property.
|
||||
*
|
||||
* @return int|false
|
||||
* @return int
|
||||
*/
|
||||
public function get_created_timestamp() {
|
||||
public function get_created_timestamp(): int {
|
||||
if ( ! $this->created && $this->is_readable() ) {
|
||||
$this->created = filectime( $this->path );
|
||||
}
|
||||
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
@@ -263,6 +399,107 @@ class File {
|
||||
return $wp_filesystem->size( $this->path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and set permissions on the file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function create(): bool {
|
||||
global $wp_filesystem;
|
||||
|
||||
$created = $wp_filesystem->touch( $this->path );
|
||||
$modded = $wp_filesystem->chmod( $this->path );
|
||||
|
||||
return $created && $modded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to the file, appending it to the end.
|
||||
*
|
||||
* @param string $text The content to add to the file.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function write( string $text ): bool {
|
||||
if ( ! $this->is_writable() ) {
|
||||
$created = $this->create();
|
||||
|
||||
if ( ! $created || ! $this->is_writable() ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure content ends with a line ending.
|
||||
$eol_pos = strrpos( $text, PHP_EOL );
|
||||
if ( false === $eol_pos || strlen( $text ) !== $eol_pos + 1 ) {
|
||||
$text .= PHP_EOL;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
|
||||
$resource = fopen( $this->path, 'ab' );
|
||||
|
||||
mbstring_binary_safe_encoding();
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite -- No suitable alternative.
|
||||
$bytes_written = fwrite( $resource, $text );
|
||||
reset_mbstring_encoding();
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
|
||||
fclose( $resource );
|
||||
|
||||
if ( strlen( $text ) !== $bytes_written ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename this file with an incremented rotation number.
|
||||
*
|
||||
* @return bool True if the file was successfully rotated.
|
||||
*/
|
||||
public function rotate(): bool {
|
||||
if ( ! $this->is_writable() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wp_filesystem;
|
||||
|
||||
$created = 0;
|
||||
if ( $this->has_standard_filename() ) {
|
||||
$created = $this->get_created_timestamp();
|
||||
}
|
||||
|
||||
if ( is_null( $this->get_rotation() ) ) {
|
||||
$new_rotation = 0;
|
||||
} else {
|
||||
$new_rotation = $this->get_rotation() + 1;
|
||||
}
|
||||
|
||||
$new_file_id = static::generate_file_id( $this->get_source(), $new_rotation, $created );
|
||||
|
||||
$search = array( $this->get_file_id() );
|
||||
$replace = array( $new_file_id );
|
||||
if ( $this->has_standard_filename() ) {
|
||||
$search[] = $this->get_hash();
|
||||
$replace[] = static::generate_hash( $new_file_id );
|
||||
}
|
||||
|
||||
$old_filename = $this->get_basename();
|
||||
$new_filename = str_replace( $search, $replace, $old_filename );
|
||||
$new_path = str_replace( $old_filename, $new_filename, $this->path );
|
||||
|
||||
$moved = $wp_filesystem->move( $this->path, $new_path, true );
|
||||
if ( ! $moved ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->path = $new_path;
|
||||
$this->ingest_path();
|
||||
|
||||
return $this->is_readable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the file from the filesystem.
|
||||
*
|
||||
|
||||
@@ -4,25 +4,77 @@ declare( strict_types = 1 );
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use PclZip;
|
||||
use WC_Cache_Helper;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* FileController class.
|
||||
*/
|
||||
class FileController {
|
||||
/**
|
||||
* The maximum number of rotations for a file before they start getting overwritten.
|
||||
*
|
||||
* This number should not go above 10, or it will cause issues with the glob patterns.
|
||||
*
|
||||
* const int
|
||||
*/
|
||||
private const MAX_FILE_ROTATIONS = 10;
|
||||
|
||||
/**
|
||||
* Default values for arguments for the get_files method.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
public const DEFAULTS_GET_FILES = array(
|
||||
'offset' => 0,
|
||||
'order' => 'desc',
|
||||
'orderby' => 'modified',
|
||||
'per_page' => 20,
|
||||
'source' => '',
|
||||
'date_end' => 0,
|
||||
'date_filter' => '',
|
||||
'date_start' => 0,
|
||||
'offset' => 0,
|
||||
'order' => 'desc',
|
||||
'orderby' => 'modified',
|
||||
'per_page' => 20,
|
||||
'source' => '',
|
||||
);
|
||||
|
||||
/**
|
||||
* Default values for arguments for the search_within_files method.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
public const DEFAULTS_SEARCH_WITHIN_FILES = array(
|
||||
'offset' => 0,
|
||||
'per_page' => 50,
|
||||
);
|
||||
|
||||
/**
|
||||
* The maximum number of files that can be searched at one time.
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
public const SEARCH_MAX_FILES = 100;
|
||||
|
||||
/**
|
||||
* The maximum number of search results that can be returned at one time.
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
public const SEARCH_MAX_RESULTS = 200;
|
||||
|
||||
/**
|
||||
* The cache group name to use for caching operations.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const CACHE_GROUP = 'log-files';
|
||||
|
||||
/**
|
||||
* A cache key for storing and retrieving the results of the last logs search.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const SEARCH_CACHE_KEY = 'logs_previous_search';
|
||||
|
||||
/**
|
||||
* The absolute path to the log directory.
|
||||
*
|
||||
@@ -37,17 +89,125 @@ class FileController {
|
||||
$this->log_directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file size limit that determines when to rotate a file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function get_file_size_limit(): int {
|
||||
$default = 5 * MB_IN_BYTES;
|
||||
|
||||
/**
|
||||
* Filter the threshold size of a log file at which point it will get rotated.
|
||||
*
|
||||
* @since 3.4.0
|
||||
*
|
||||
* @param int $file_size_limit The file size limit in bytes.
|
||||
*/
|
||||
$file_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $default );
|
||||
|
||||
if ( ! is_int( $file_size_limit ) || $file_size_limit < 1 ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $file_size_limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a log entry to the appropriate file, after rotating the file if necessary.
|
||||
*
|
||||
* @param string $source The source property of the log entry, which determines which file to write to.
|
||||
* @param string $text The contents of the log entry to add to a file.
|
||||
* @param int|null $time Optional. The time of the log entry as a Unix timestamp. Defaults to the current time.
|
||||
*
|
||||
* @return bool True if the contents were successfully written to the file.
|
||||
*/
|
||||
public function write_to_file( string $source, string $text, ?int $time = null ): bool {
|
||||
if ( is_null( $time ) ) {
|
||||
$time = time();
|
||||
}
|
||||
|
||||
$file_id = File::generate_file_id( $source, null, $time );
|
||||
$file = $this->get_file_by_id( $file_id );
|
||||
|
||||
if ( $file instanceof File && $file->get_file_size() >= $this->get_file_size_limit() ) {
|
||||
$rotated = $this->rotate_file( $file->get_file_id() );
|
||||
|
||||
if ( $rotated ) {
|
||||
$file = null;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $file instanceof File ) {
|
||||
$new_path = $this->log_directory . $this->generate_filename( $source, $time );
|
||||
$file = new File( $new_path );
|
||||
}
|
||||
|
||||
return $file->write( $text );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the full name of a file based on source and date values.
|
||||
*
|
||||
* @param string $source The source property of a log entry, which determines the filename.
|
||||
* @param int $time The time of the log entry as a Unix timestamp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate_filename( string $source, int $time ): string {
|
||||
$file_id = File::generate_file_id( $source, null, $time );
|
||||
$hash = File::generate_hash( $file_id );
|
||||
|
||||
return "$file_id-$hash.log";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the rotations of a file and increment them, so that they overwrite the previous file with that rotation.
|
||||
*
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return bool True if the file and all its rotations were successfully rotated.
|
||||
*/
|
||||
private function rotate_file( $file_id ): bool {
|
||||
$rotations = $this->get_file_rotations( $file_id );
|
||||
|
||||
if ( is_wp_error( $rotations ) || ! isset( $rotations['current'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$max_rotation_marker = self::MAX_FILE_ROTATIONS - 1;
|
||||
|
||||
// Don't rotate a file with the maximum rotation.
|
||||
unset( $rotations[ $max_rotation_marker ] );
|
||||
|
||||
$results = array();
|
||||
// Rotate starting with oldest first and working backwards.
|
||||
for ( $i = $max_rotation_marker; $i >= 0; $i -- ) {
|
||||
if ( isset( $rotations[ $i ] ) ) {
|
||||
$results[] = $rotations[ $i ]->rotate();
|
||||
}
|
||||
}
|
||||
$results[] = $rotations['current']->rotate();
|
||||
|
||||
return ! in_array( false, $results, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of log files.
|
||||
*
|
||||
* @param array $args {
|
||||
* Optional. Arguments to filter and sort the files that are returned.
|
||||
*
|
||||
* @type int $offset Omit this number of files from the beginning of the list. Works with $per_page to do pagination.
|
||||
* @type string $order The sort direction. 'asc' or 'desc'. Defaults to 'desc'.
|
||||
* @type string $orderby The property to sort the list by. 'created', 'modified', 'source', 'size'. Defaults to 'modified'.
|
||||
* @type int $per_page The number of files to include in the list. Works with $offset to do pagination.
|
||||
* @type string $source Only include files from this source.
|
||||
* @type int $date_end The end of the date range to filter by, as a Unix timestamp.
|
||||
* @type string $date_filter Filter files by one of the date props. 'created' or 'modified'.
|
||||
* @type int $date_start The beginning of the date range to filter by, as a Unix timestamp.
|
||||
* @type int $offset Omit this number of files from the beginning of the list. Works with $per_page to do pagination.
|
||||
* @type string $order The sort direction. 'asc' or 'desc'. Defaults to 'desc'.
|
||||
* @type string $orderby The property to sort the list by. 'created', 'modified', 'source', 'size'. Defaults to 'modified'.
|
||||
* @type int $per_page The number of files to include in the list. Works with $offset to do pagination.
|
||||
* @type string $source Only include files from this source.
|
||||
* }
|
||||
* @param bool $count_only Optional. True to return a total count of the files.
|
||||
*
|
||||
@@ -66,11 +226,30 @@ class FileController {
|
||||
);
|
||||
}
|
||||
|
||||
if ( true === $count_only ) {
|
||||
return count( $paths );
|
||||
$files = $this->convert_paths_to_objects( $paths );
|
||||
|
||||
if ( $args['date_filter'] && $args['date_start'] && $args['date_end'] ) {
|
||||
switch ( $args['date_filter'] ) {
|
||||
case 'created':
|
||||
$files = array_filter(
|
||||
$files,
|
||||
fn( $file ) => $file->get_created_timestamp() >= $args['date_start']
|
||||
&& $file->get_created_timestamp() <= $args['date_end']
|
||||
);
|
||||
break;
|
||||
case 'modified':
|
||||
$files = array_filter(
|
||||
$files,
|
||||
fn( $file ) => $file->get_modified_timestamp() >= $args['date_start']
|
||||
&& $file->get_modified_timestamp() <= $args['date_end']
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$files = $this->convert_paths_to_objects( $paths );
|
||||
if ( true === $count_only ) {
|
||||
return count( $files );
|
||||
}
|
||||
|
||||
$multi_sorter = function( $sort_sets, $order_sets ) {
|
||||
$comparison = 0;
|
||||
@@ -156,14 +335,19 @@ class FileController {
|
||||
$paths = array();
|
||||
|
||||
foreach ( $file_ids as $file_id ) {
|
||||
$glob = glob( $this->log_directory . $file_id . '*.log' );
|
||||
// Look for the standard filename format first, which includes a hash.
|
||||
$glob = glob( $this->log_directory . $file_id . '-*.log' );
|
||||
|
||||
if ( ! $glob ) {
|
||||
$glob = glob( $this->log_directory . $file_id . '.log' );
|
||||
}
|
||||
|
||||
if ( is_array( $glob ) ) {
|
||||
$paths = array_merge( $paths, $glob );
|
||||
}
|
||||
}
|
||||
|
||||
$files = $this->convert_paths_to_objects( $paths );
|
||||
$files = $this->convert_paths_to_objects( array_unique( $paths ) );
|
||||
|
||||
return $files;
|
||||
}
|
||||
@@ -185,6 +369,13 @@ class FileController {
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $result ) > 1 ) {
|
||||
return new WP_Error(
|
||||
'wc_log_file_error',
|
||||
esc_html__( 'Multiple files match this ID.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
return reset( $result );
|
||||
}
|
||||
|
||||
@@ -207,19 +398,32 @@ class FileController {
|
||||
$rotations = array();
|
||||
|
||||
$source = $file->get_source();
|
||||
$created = gmdate( 'Y-m-d', $file->get_created_timestamp() );
|
||||
$created = 0;
|
||||
if ( $file->has_standard_filename() ) {
|
||||
$created = $file->get_created_timestamp();
|
||||
}
|
||||
|
||||
if ( is_null( $file->get_rotation() ) ) {
|
||||
$current['current'] = $file;
|
||||
} else {
|
||||
$current_file_id = $source . '-' . $created;
|
||||
$current_file_id = File::generate_file_id( $source, null, $created );
|
||||
$result = $this->get_file_by_id( $current_file_id );
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$current['current'] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
$rotation_pattern = $this->log_directory . $source . '.[0123456789]-' . $created . '*.log';
|
||||
$rotations_pattern = sprintf(
|
||||
'.[%s]',
|
||||
implode(
|
||||
'',
|
||||
range( 0, self::MAX_FILE_ROTATIONS - 1 )
|
||||
)
|
||||
);
|
||||
|
||||
$created_pattern = $created ? '-' . gmdate( 'Y-m-d', $created ) . '-' : '';
|
||||
|
||||
$rotation_pattern = $this->log_directory . $source . $rotations_pattern . $created_pattern . '*.log';
|
||||
$rotation_paths = glob( $rotation_pattern );
|
||||
$rotation_files = $this->convert_paths_to_objects( $rotation_paths );
|
||||
foreach ( $rotation_files as $rotation_file ) {
|
||||
@@ -282,7 +486,7 @@ class FileController {
|
||||
*
|
||||
* @param array $file_ids An array of file IDs (file basename without the hash).
|
||||
*
|
||||
* @return int
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function delete_files( array $file_ids ): int {
|
||||
$deleted = 0;
|
||||
@@ -290,7 +494,7 @@ class FileController {
|
||||
$files = $this->get_files_by_id( $file_ids );
|
||||
foreach ( $files as $file ) {
|
||||
$result = false;
|
||||
if ( $file->is_readable() ) {
|
||||
if ( $file->is_writable() ) {
|
||||
$result = $file->delete();
|
||||
}
|
||||
|
||||
@@ -299,17 +503,173 @@ class FileController {
|
||||
}
|
||||
}
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->invalidate_cache();
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the source property of a log file.
|
||||
* Stream a single file to the browser without zipping it first.
|
||||
*
|
||||
* @param string $source The source property of a log file.
|
||||
* @param string $file_id A file ID (file basename without the hash).
|
||||
*
|
||||
* @return string
|
||||
* @return WP_Error|void Only returns something if there is an error.
|
||||
*/
|
||||
public function sanitize_source( string $source ): string {
|
||||
return sanitize_file_name( $source );
|
||||
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.
|
||||
*
|
||||
* @param string $search The string to search for.
|
||||
* @param array $args Optional. Arguments for pagination of search results.
|
||||
* @param array $file_args Optional. Arguments to filter and sort the files that are returned. See get_files().
|
||||
* @param bool $count_only Optional. True to return a total count of the matches.
|
||||
*
|
||||
* @return array|int|WP_Error When matches are found, each array item is an associative array that includes the
|
||||
* file ID, line number, and the matched string with HTML markup around the matched parts.
|
||||
*/
|
||||
public function search_within_files( string $search, array $args = array(), array $file_args = array(), bool $count_only = false ) {
|
||||
if ( '' === $search ) {
|
||||
return $count_only ? 0 : array();
|
||||
}
|
||||
|
||||
$search = esc_html( $search );
|
||||
|
||||
$args = wp_parse_args( $args, self::DEFAULTS_SEARCH_WITHIN_FILES );
|
||||
|
||||
$file_args = array_merge(
|
||||
$file_args,
|
||||
array(
|
||||
'offset' => 0,
|
||||
'per_page' => self::SEARCH_MAX_FILES,
|
||||
)
|
||||
);
|
||||
|
||||
$cache_key = WC_Cache_Helper::get_prefixed_key( self::SEARCH_CACHE_KEY, self::CACHE_GROUP );
|
||||
$query = wp_json_encode( array( $search, $args, $file_args ) );
|
||||
$cache = wp_cache_get( $cache_key );
|
||||
$is_cached = isset( $cache['query'], $cache['results'] ) && $query === $cache['query'];
|
||||
|
||||
if ( true === $is_cached ) {
|
||||
$matched_lines = $cache['results'];
|
||||
} else {
|
||||
$files = $this->get_files( $file_args );
|
||||
if ( is_wp_error( $files ) ) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
// Max string size * SEARCH_MAX_RESULTS = ~1MB largest possible cache entry.
|
||||
$max_string_size = 5 * KB_IN_BYTES;
|
||||
|
||||
$matched_lines = array();
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
$stream = $file->get_stream();
|
||||
$line_number = 1;
|
||||
|
||||
while ( ! feof( $stream ) ) {
|
||||
$line = fgets( $stream, $max_string_size );
|
||||
if ( ! is_string( $line ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sanitized_line = esc_html( trim( $line ) );
|
||||
if ( false !== stripos( $sanitized_line, $search ) ) {
|
||||
$matched_lines[] = array(
|
||||
'file_id' => $file->get_file_id(),
|
||||
'line_number' => $line_number,
|
||||
'line' => $sanitized_line,
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $matched_lines ) >= self::SEARCH_MAX_RESULTS ) {
|
||||
$file->close_stream();
|
||||
break 2;
|
||||
}
|
||||
|
||||
if ( false !== strstr( $line, PHP_EOL ) ) {
|
||||
$line_number ++;
|
||||
}
|
||||
}
|
||||
|
||||
$file->close_stream();
|
||||
}
|
||||
|
||||
$to_cache = array(
|
||||
'query' => $query,
|
||||
'results' => $matched_lines,
|
||||
);
|
||||
wp_cache_set( $cache_key, $to_cache, self::CACHE_GROUP, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
if ( true === $count_only ) {
|
||||
return count( $matched_lines );
|
||||
}
|
||||
|
||||
return array_slice( $matched_lines, $args['offset'], $args['per_page'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cache group related to log file data.
|
||||
*
|
||||
* @return bool True on successfully invalidating the cache.
|
||||
*/
|
||||
public function invalidate_cache(): bool {
|
||||
return WC_Cache_Helper::invalidate_cache_group( self::CACHE_GROUP );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\PageController;
|
||||
|
||||
use WP_List_Table;
|
||||
|
||||
/**
|
||||
* FileListTable class.
|
||||
*/
|
||||
class FileListTable extends WP_List_Table {
|
||||
/**
|
||||
* The user option key for saving the preferred number of files displayed per page.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
public const PER_PAGE_USER_OPTION_KEY = 'woocommerce_logging_file_list_per_page';
|
||||
|
||||
/**
|
||||
* Instance of FileController.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of PageController.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $page_controller;
|
||||
|
||||
/**
|
||||
* FileListTable class.
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param PageController $page_controller Instance of PageController.
|
||||
*/
|
||||
public function __construct( FileController $file_controller, PageController $page_controller ) {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->page_controller = $page_controller;
|
||||
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'log-file',
|
||||
'plural' => 'log-files',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render message when there are no items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function no_items(): void {
|
||||
esc_html_e( 'No log files found.', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of bulk actions available for this table.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_bulk_actions(): array {
|
||||
return array(
|
||||
'export' => esc_html__( 'Download', 'woocommerce' ),
|
||||
'delete' => esc_html__( 'Delete permanently', 'woocommerce' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the existing log sources for the filter dropdown.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_sources_list(): array {
|
||||
$sources = $this->file_controller->get_file_sources();
|
||||
if ( is_wp_error( $sources ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
sort( $sources );
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays extra controls between bulk actions and pagination.
|
||||
*
|
||||
* @param string $which The location of the tablenav being rendered. 'top' or 'bottom'.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function extra_tablenav( $which ): void {
|
||||
$all_sources = $this->get_sources_list();
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
|
||||
$current_source = File::sanitize_source( wp_unslash( $_GET['source'] ?? '' ) );
|
||||
|
||||
?>
|
||||
<div class="alignleft actions">
|
||||
<?php if ( 'top' === $which ) : ?>
|
||||
<label for="filter-by-source" class="screen-reader-text"><?php esc_html_e( 'Filter by log source', 'woocommerce' ); ?></label>
|
||||
<select name="source" id="filter-by-source">
|
||||
<option<?php selected( $current_source, '' ); ?> value=""><?php esc_html_e( 'All sources', 'woocommerce' ); ?></option>
|
||||
<?php foreach ( $all_sources as $source ) : ?>
|
||||
<option<?php selected( $current_source, $source ); ?> value="<?php echo esc_attr( $source ); ?>">
|
||||
<?php echo esc_html( $source ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
submit_button(
|
||||
__( 'Filter', 'woocommerce' ),
|
||||
'',
|
||||
'filter_action',
|
||||
false,
|
||||
array(
|
||||
'id' => 'logs-filter-submit',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the column header info.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_column_headers(): void {
|
||||
$this->_column_headers = array(
|
||||
$this->get_columns(),
|
||||
get_hidden_columns( $this->screen ),
|
||||
$this->get_sortable_columns(),
|
||||
$this->get_primary_column(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of items for displaying.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_items(): void {
|
||||
$per_page = $this->get_items_per_page(
|
||||
self::PER_PAGE_USER_OPTION_KEY,
|
||||
$this->get_per_page_default()
|
||||
);
|
||||
|
||||
$defaults = array(
|
||||
'per_page' => $per_page,
|
||||
'offset' => ( $this->get_pagenum() - 1 ) * $per_page,
|
||||
);
|
||||
$file_args = wp_parse_args(
|
||||
$this->page_controller->get_query_params( array( 'order', 'orderby', 'source' ) ),
|
||||
$defaults
|
||||
);
|
||||
|
||||
$total_items = $this->file_controller->get_files( $file_args, true );
|
||||
if ( is_wp_error( $total_items ) ) {
|
||||
printf(
|
||||
'<div class="notice notice-warning"><p>%s</p></div>',
|
||||
esc_html( $total_items->get_error_message() )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total_items / $per_page );
|
||||
$items = $this->file_controller->get_files( $file_args );
|
||||
|
||||
$this->items = $items;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'per_page' => $per_page,
|
||||
'total_items' => $total_items,
|
||||
'total_pages' => $total_pages,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns(): array {
|
||||
$columns = array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'source' => esc_html__( 'Source', 'woocommerce' ),
|
||||
'created' => esc_html__( 'Date created', 'woocommerce' ),
|
||||
'modified' => esc_html__( 'Date modified', 'woocommerce' ),
|
||||
'size' => esc_html__( 'File size', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of sortable columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_sortable_columns(): array {
|
||||
$sortable = array(
|
||||
'source' => array( 'source' ),
|
||||
'created' => array( 'created' ),
|
||||
'modified' => array( 'modified', true ),
|
||||
'size' => array( 'size' ),
|
||||
);
|
||||
|
||||
return $sortable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the checkbox column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<input
|
||||
id="cb-select-<?php echo esc_attr( $item->get_file_id() ); ?>"
|
||||
type="checkbox"
|
||||
name="file_id[]"
|
||||
value="<?php echo esc_attr( $item->get_file_id() ); ?>"
|
||||
/>
|
||||
<label for="cb-select-<?php echo esc_attr( $item->get_file_id() ); ?>">
|
||||
<span class="screen-reader-text">
|
||||
<?php
|
||||
printf(
|
||||
// translators: 1. a date, 2. a slug-style name for a file.
|
||||
esc_html__( 'Select the %1$s log file for %2$s', 'woocommerce' ),
|
||||
esc_html( gmdate( get_option( 'date_format' ), $item->get_created_timestamp() ) ),
|
||||
esc_html( $item->get_source() )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</label>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the source column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_source( $item ): string {
|
||||
$log_file = $item->get_file_id();
|
||||
$single_file_url = add_query_arg(
|
||||
array(
|
||||
'view' => 'single_file',
|
||||
'file_id' => $log_file,
|
||||
),
|
||||
$this->page_controller->get_logs_tab_url()
|
||||
);
|
||||
$rotation = '';
|
||||
if ( ! is_null( $item->get_rotation() ) ) {
|
||||
$rotation = sprintf(
|
||||
' – <span class="post-state">%d</span>',
|
||||
$item->get_rotation()
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<a class="row-title" href="%1$s">%2$s</a>%3$s',
|
||||
esc_url( $single_file_url ),
|
||||
esc_html( $item->get_source() ),
|
||||
$rotation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the created column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_created( $item ): string {
|
||||
$timestamp = $item->get_created_timestamp();
|
||||
|
||||
return gmdate( 'Y-m-d', $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modified column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_modified( $item ): string {
|
||||
$timestamp = $item->get_modified_timestamp();
|
||||
|
||||
return gmdate( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the size column.
|
||||
*
|
||||
* @param File $item The current log file being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_size( $item ): string {
|
||||
$size = $item->get_file_size();
|
||||
|
||||
return size_format( $size );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the default value for the per_page arg.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_per_page_default(): int {
|
||||
return $this->file_controller::DEFAULTS_GET_FILES['per_page'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\PageController;
|
||||
|
||||
use WP_List_Table;
|
||||
|
||||
/**
|
||||
* SearchListTable class.
|
||||
*/
|
||||
class SearchListTable extends WP_List_Table {
|
||||
/**
|
||||
* The user option key for saving the preferred number of search results displayed per page.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
public const PER_PAGE_USER_OPTION_KEY = 'woocommerce_logging_search_results_per_page';
|
||||
|
||||
/**
|
||||
* Instance of FileController.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of PageController.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $page_controller;
|
||||
|
||||
/**
|
||||
* SearchListTable class.
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param PageController $page_controller Instance of PageController.
|
||||
*/
|
||||
public function __construct( FileController $file_controller, PageController $page_controller ) {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->page_controller = $page_controller;
|
||||
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'wc-logs-search-result',
|
||||
'plural' => 'wc-logs-search-results',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render message when there are no items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function no_items(): void {
|
||||
esc_html_e( 'No search results.', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the column header info.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_column_headers(): void {
|
||||
$this->_column_headers = array(
|
||||
$this->get_columns(),
|
||||
array(),
|
||||
array(),
|
||||
$this->get_primary_column(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of items for displaying.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepare_items(): void {
|
||||
$per_page = $this->get_items_per_page(
|
||||
self::PER_PAGE_USER_OPTION_KEY,
|
||||
$this->get_per_page_default()
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'per_page' => $per_page,
|
||||
'offset' => ( $this->get_pagenum() - 1 ) * $per_page,
|
||||
);
|
||||
|
||||
$file_args = $this->page_controller->get_query_params(
|
||||
array( 'date_end', 'date_filter', 'date_start', 'order', 'orderby', 'search', 'source' )
|
||||
);
|
||||
$search = $file_args['search'];
|
||||
unset( $file_args['search'] );
|
||||
|
||||
$total_items = $this->file_controller->search_within_files( $search, $args, $file_args, true );
|
||||
if ( is_wp_error( $total_items ) ) {
|
||||
printf(
|
||||
'<div class="notice notice-warning"><p>%s</p></div>',
|
||||
esc_html( $total_items->get_error_message() )
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $total_items >= $this->file_controller::SEARCH_MAX_RESULTS ) {
|
||||
printf(
|
||||
'<div class="notice notice-info"><p>%s</p></div>',
|
||||
sprintf(
|
||||
// translators: %s is a number.
|
||||
esc_html__( 'The number of search results has reached the limit of %s. Try refining your search.', 'woocommerce' ),
|
||||
esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_RESULTS ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total_items / $per_page );
|
||||
$results = $this->file_controller->search_within_files( $search, $args, $file_args );
|
||||
$this->items = $results;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'per_page' => $per_page,
|
||||
'total_items' => $total_items,
|
||||
'total_pages' => $total_pages,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns(): array {
|
||||
$columns = array(
|
||||
'file_id' => esc_html__( 'File', 'woocommerce' ),
|
||||
'line_number' => esc_html__( 'Line #', 'woocommerce' ),
|
||||
'line' => esc_html__( 'Matched Line', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the file_id column.
|
||||
*
|
||||
* @param array $item The current search result being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_file_id( array $item ): string {
|
||||
// Add a word break after the rotation number, if it exists.
|
||||
$file_id = preg_replace( '/\.([0-9])+\-/', '.\1<wbr>-', $item['file_id'] );
|
||||
|
||||
return wp_kses( $file_id, array( 'wbr' => array() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the line_number column.
|
||||
*
|
||||
* @param array $item The current search result being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_line_number( array $item ): string {
|
||||
$match_url = add_query_arg(
|
||||
array(
|
||||
'view' => 'single_file',
|
||||
'file_id' => $item['file_id'],
|
||||
),
|
||||
$this->page_controller->get_logs_tab_url() . '#L' . absint( $item['line_number'] )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<a href="%1$s">%2$s</a>',
|
||||
esc_url( $match_url ),
|
||||
sprintf(
|
||||
// translators: %s is a line number in a file.
|
||||
esc_html__( 'Line %s', 'woocommerce' ),
|
||||
number_format_i18n( absint( $item['line_number'] ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the line column.
|
||||
*
|
||||
* @param array $item The current search result being rendered.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function column_line( array $item ): string {
|
||||
$params = $this->page_controller->get_query_params( array( 'search' ) );
|
||||
$line = $item['line'];
|
||||
|
||||
// Highlight matches within the line.
|
||||
$pattern = preg_quote( $params['search'], '/' );
|
||||
preg_match_all( "/$pattern/i", $line, $matches, PREG_OFFSET_CAPTURE );
|
||||
if ( is_array( $matches[0] ) && count( $matches[0] ) >= 1 ) {
|
||||
$length_change = 0;
|
||||
|
||||
foreach ( $matches[0] as $match ) {
|
||||
$replace = '<span class="search-match">' . $match[0] . '</span>';
|
||||
$offset = $match[1] + $length_change;
|
||||
$orig_length = strlen( $match[0] );
|
||||
$replace_length = strlen( $replace );
|
||||
|
||||
$line = substr_replace( $line, $replace, $offset, $orig_length );
|
||||
|
||||
$length_change += $replace_length - $orig_length;
|
||||
}
|
||||
}
|
||||
|
||||
return wp_kses_post( $line );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the default value for the per_page arg.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_per_page_default(): int {
|
||||
return $this->file_controller::DEFAULTS_SEARCH_WITHIN_FILES['per_page'];
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,229 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use WC_Log_Handler_File;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use WC_Log_Handler;
|
||||
|
||||
/**
|
||||
* LogHandlerFileV2 class.
|
||||
*/
|
||||
class LogHandlerFileV2 extends WC_Log_Handler_File {}
|
||||
class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
/**
|
||||
* Instance of the FileController class.
|
||||
*
|
||||
* @var FileController
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* LogHandlerFileV2 class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->file_controller = wc_get_container()->get( FileController::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a log entry.
|
||||
*
|
||||
* @param int $timestamp Log timestamp.
|
||||
* @param string $level emergency|alert|critical|error|warning|notice|info|debug.
|
||||
* @param string $message Log message.
|
||||
* @param array $context {
|
||||
* Optional. Additional information for log handlers. Any data can be added here, but there are some array
|
||||
* keys that have special behavior.
|
||||
*
|
||||
* @type string $source Determines which log file to write to. Must be at least 3 characters in length.
|
||||
* @type bool $backtrace True to include a backtrace that shows where the logging function got called.
|
||||
* }
|
||||
*
|
||||
* @return bool False if value was not handled and true if value was handled.
|
||||
*/
|
||||
public function handle( $timestamp, $level, $message, $context ) {
|
||||
if ( isset( $context['source'] ) && is_string( $context['source'] ) && strlen( $context['source'] ) >= 3 ) {
|
||||
$source = sanitize_title( trim( $context['source'] ) );
|
||||
} else {
|
||||
$source = $this->determine_source();
|
||||
}
|
||||
|
||||
$entry = static::format_entry( $timestamp, $level, $message, $context );
|
||||
|
||||
$written = $this->file_controller->write_to_file( $source, $entry, $timestamp );
|
||||
|
||||
if ( $written ) {
|
||||
$this->file_controller->invalidate_cache();
|
||||
}
|
||||
|
||||
return $written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a log entry text from level, timestamp, and message.
|
||||
*
|
||||
* @param int $timestamp Log timestamp.
|
||||
* @param string $level emergency|alert|critical|error|warning|notice|info|debug.
|
||||
* @param string $message Log message.
|
||||
* @param array $context Additional information for log handlers.
|
||||
*
|
||||
* @return string Formatted log entry.
|
||||
*/
|
||||
protected static function format_entry( $timestamp, $level, $message, $context ) {
|
||||
$time_string = static::format_time( $timestamp );
|
||||
$level_string = strtoupper( $level );
|
||||
|
||||
// Remove line breaks so the whole entry is on one line in the file.
|
||||
$formatted_message = str_replace( PHP_EOL, ' ', $message );
|
||||
|
||||
unset( $context['source'] );
|
||||
if ( ! empty( $context ) ) {
|
||||
if ( isset( $context['backtrace'] ) && true === filter_var( $context['backtrace'], FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$context['backtrace'] = static::get_backtrace();
|
||||
}
|
||||
|
||||
$formatted_context = wp_json_encode( $context );
|
||||
$formatted_message .= " CONTEXT: $formatted_context";
|
||||
}
|
||||
|
||||
$entry = "$time_string $level_string $formatted_message";
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/** This filter is documented in includes/abstracts/abstract-wc-log-handler.php */
|
||||
return apply_filters(
|
||||
'woocommerce_format_log_entry',
|
||||
$entry,
|
||||
array(
|
||||
'timestamp' => $timestamp,
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
)
|
||||
);
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out a source string to use for a log entry based on where the log method was called from.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function determine_source(): string {
|
||||
$source_roots = array(
|
||||
'mu-plugin' => trailingslashit( Constants::get_constant( 'WPMU_PLUGIN_DIR' ) ),
|
||||
'plugin' => trailingslashit( Constants::get_constant( 'WP_PLUGIN_DIR' ) ),
|
||||
'theme' => trailingslashit( get_theme_root() ),
|
||||
);
|
||||
|
||||
$source = '';
|
||||
$backtrace = static::get_backtrace();
|
||||
|
||||
foreach ( $backtrace as $frame ) {
|
||||
foreach ( $source_roots as $type => $path ) {
|
||||
if ( 0 === strpos( $frame['file'], $path ) ) {
|
||||
$relative_path = trim( substr( $frame['file'], strlen( $path ) ), DIRECTORY_SEPARATOR );
|
||||
|
||||
if ( 'mu-plugin' === $type ) {
|
||||
$info = pathinfo( $relative_path );
|
||||
|
||||
if ( '.' === $info['dirname'] ) {
|
||||
$source = "$type-" . $info['filename'];
|
||||
} else {
|
||||
$source = "$type-" . $info['dirname'];
|
||||
}
|
||||
|
||||
break 2;
|
||||
}
|
||||
|
||||
$segments = explode( DIRECTORY_SEPARATOR, $relative_path );
|
||||
if ( is_array( $segments ) ) {
|
||||
$source = "$type-" . reset( $segments );
|
||||
}
|
||||
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $source ) {
|
||||
$source = 'log';
|
||||
}
|
||||
|
||||
return sanitize_title( $source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs older than a specified timestamp.
|
||||
*
|
||||
* @param int $timestamp All files created before this timestamp will be deleted.
|
||||
*
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function delete_logs_before_timestamp( int $timestamp = 0 ): int {
|
||||
if ( ! $timestamp ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$files = $this->file_controller->get_files(
|
||||
array(
|
||||
'date_filter' => 'created',
|
||||
'date_start' => 1,
|
||||
'date_end' => $timestamp,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file_ids = array_map(
|
||||
fn( $file ) => $file->get_file_id(),
|
||||
$files
|
||||
);
|
||||
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/** This filter is documented in includes/class-wc-logger.php. */
|
||||
$retention_days = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) );
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
time(),
|
||||
'info',
|
||||
sprintf(
|
||||
'%s %s',
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %s is a number of log files.
|
||||
_n(
|
||||
'%s expired log file was deleted.',
|
||||
'%s expired log files were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $deleted )
|
||||
),
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %s is a number of days.
|
||||
_n(
|
||||
'The retention period for log files is %s day.',
|
||||
'The retention period for log files is %s days.',
|
||||
$retention_days,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $retention_days )
|
||||
)
|
||||
),
|
||||
array(
|
||||
'source' => 'wc_logger',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ declare( strict_types = 1 );
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ FileController, ListTable };
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController, FileListTable, SearchListTable };
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Status;
|
||||
use WC_Log_Handler_File, WC_Log_Handler_DB;
|
||||
use WC_Log_Levels;
|
||||
use WP_List_Table;
|
||||
|
||||
/**
|
||||
* PageController class.
|
||||
@@ -24,9 +27,9 @@ class PageController {
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of ListTable.
|
||||
* Instance of FileListTable or SearchListTable.
|
||||
*
|
||||
* @var ListTable
|
||||
* @var FileListTable|SearchListTable
|
||||
*/
|
||||
private $list_table;
|
||||
|
||||
@@ -81,7 +84,7 @@ class PageController {
|
||||
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
|
||||
|
||||
if ( is_null( $handler ) || ! class_exists( $handler ) ) {
|
||||
$handler = \WC_Log_Handler_File::class;
|
||||
$handler = WC_Log_Handler_File::class;
|
||||
}
|
||||
|
||||
return $handler;
|
||||
@@ -97,8 +100,7 @@ class PageController {
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
$params = $this->get_query_params();
|
||||
$this->render_filev2( $params );
|
||||
$this->render_filev2();
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
WC_Admin_Status::status_logs_db();
|
||||
@@ -112,20 +114,21 @@ class PageController {
|
||||
/**
|
||||
* Render the views for the FileV2 log handler.
|
||||
*
|
||||
* @param array $params Args for rendering the views.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_filev2( array $params = array() ): void {
|
||||
$view = $params['view'] ?? '';
|
||||
private function render_filev2(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
switch ( $view ) {
|
||||
switch ( $params['view'] ) {
|
||||
case 'list_files':
|
||||
default:
|
||||
$this->render_file_list_page( $params );
|
||||
$this->render_list_files_view();
|
||||
break;
|
||||
case 'search_results':
|
||||
$this->render_search_results_view();
|
||||
break;
|
||||
case 'single_file':
|
||||
$this->render_single_file_page( $params );
|
||||
$this->render_single_file_view();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -133,18 +136,21 @@ class PageController {
|
||||
/**
|
||||
* Render the file list view.
|
||||
*
|
||||
* @param array $params Args for rendering the view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_file_list_page( array $params = array() ): void {
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
private function render_list_files_view(): void {
|
||||
$params = $this->get_query_params( array( 'order', 'orderby', 'source', 'view' ) );
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
|
||||
$list_table->prepare_items();
|
||||
|
||||
?>
|
||||
<header id="logs-header" class="wc-logs-header">
|
||||
<h2>
|
||||
<?php esc_html_e( 'Browse log files', 'woocommerce' ); ?>
|
||||
</h2>
|
||||
<?php $this->render_search_field(); ?>
|
||||
</header>
|
||||
<form id="logs-list-table-form" method="get">
|
||||
<input type="hidden" name="page" value="wc-status" />
|
||||
@@ -158,8 +164,7 @@ class PageController {
|
||||
/>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php $this->get_list_table()->prepare_items(); ?>
|
||||
<?php $this->get_list_table()->display(); ?>
|
||||
<?php $list_table->display(); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
@@ -167,12 +172,11 @@ class PageController {
|
||||
/**
|
||||
* Render the single file view.
|
||||
*
|
||||
* @param array $params Args for rendering the view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_single_file_page( array $params ): void {
|
||||
$file = $this->file_controller->get_file_by_id( $params['file_id'] );
|
||||
private function render_single_file_view(): void {
|
||||
$params = $this->get_query_params( array( 'file_id', 'view' ) );
|
||||
$file = $this->file_controller->get_file_by_id( $params['file_id'] );
|
||||
|
||||
if ( is_wp_error( $file ) ) {
|
||||
?>
|
||||
@@ -194,22 +198,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>
|
||||
@@ -253,6 +263,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(
|
||||
@@ -279,6 +297,26 @@ class PageController {
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the search results view.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_search_results_view(): void {
|
||||
$params = $this->get_query_params( array( 'order', 'orderby', 'search', 'source', 'view' ) );
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
|
||||
$list_table->prepare_items();
|
||||
|
||||
?>
|
||||
<header id="logs-header" class="wc-logs-header">
|
||||
<h2><?php esc_html_e( 'Search results', 'woocommerce' ); ?></h2>
|
||||
<?php $this->render_search_field(); ?>
|
||||
</header>
|
||||
<?php $list_table->display(); ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default values for URL query params for FileV2 views.
|
||||
*
|
||||
@@ -289,6 +327,7 @@ class PageController {
|
||||
'file_id' => '',
|
||||
'order' => $this->file_controller::DEFAULTS_GET_FILES['order'],
|
||||
'orderby' => $this->file_controller::DEFAULTS_GET_FILES['orderby'],
|
||||
'search' => '',
|
||||
'source' => $this->file_controller::DEFAULTS_GET_FILES['source'],
|
||||
'view' => 'list_files',
|
||||
);
|
||||
@@ -297,9 +336,11 @@ class PageController {
|
||||
/**
|
||||
* Get and validate URL query params for FileV2 views.
|
||||
*
|
||||
* @param array $param_keys Optional. The names of the params you want to get.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_query_params(): array {
|
||||
public function get_query_params( array $param_keys = array() ): array {
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
$params = filter_input_array(
|
||||
INPUT_GET,
|
||||
@@ -307,7 +348,7 @@ class PageController {
|
||||
'file_id' => array(
|
||||
'filter' => FILTER_CALLBACK,
|
||||
'options' => function( $file_id ) {
|
||||
return sanitize_file_name( $file_id );
|
||||
return sanitize_file_name( wp_unslash( $file_id ) );
|
||||
},
|
||||
),
|
||||
'order' => array(
|
||||
@@ -324,16 +365,22 @@ class PageController {
|
||||
'default' => $defaults['orderby'],
|
||||
),
|
||||
),
|
||||
'search' => array(
|
||||
'filter' => FILTER_CALLBACK,
|
||||
'options' => function( $search ) {
|
||||
return esc_html( wp_unslash( $search ) );
|
||||
},
|
||||
),
|
||||
'source' => array(
|
||||
'filter' => FILTER_CALLBACK,
|
||||
'options' => function( $source ) {
|
||||
return $this->file_controller->sanitize_source( wp_unslash( $source ) );
|
||||
return File::sanitize_source( wp_unslash( $source ) );
|
||||
},
|
||||
),
|
||||
'view' => array(
|
||||
'filter' => FILTER_VALIDATE_REGEXP,
|
||||
'options' => array(
|
||||
'regexp' => '/^(list_files|single_file)$/',
|
||||
'regexp' => '/^(list_files|single_file|search_results)$/',
|
||||
'default' => $defaults['view'],
|
||||
),
|
||||
),
|
||||
@@ -342,20 +389,33 @@ class PageController {
|
||||
);
|
||||
$params = wp_parse_args( $params, $defaults );
|
||||
|
||||
if ( count( $param_keys ) > 0 ) {
|
||||
$params = array_intersect_key( $params, array_flip( $param_keys ) );
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and cache an instance of the list table.
|
||||
*
|
||||
* @return ListTable
|
||||
* @param string $view The current view, which determines which list table class to get.
|
||||
*
|
||||
* @return FileListTable|SearchListTable
|
||||
*/
|
||||
private function get_list_table(): ListTable {
|
||||
if ( $this->list_table instanceof ListTable ) {
|
||||
private function get_list_table( string $view ) {
|
||||
if ( $this->list_table instanceof WP_List_Table ) {
|
||||
return $this->list_table;
|
||||
}
|
||||
|
||||
$this->list_table = new ListTable( $this->file_controller, $this );
|
||||
switch ( $view ) {
|
||||
case 'list_files':
|
||||
$this->list_table = new FileListTable( $this->file_controller, $this );
|
||||
break;
|
||||
case 'search_results':
|
||||
$this->list_table = new SearchListTable( $this->file_controller, $this );
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->list_table;
|
||||
}
|
||||
@@ -366,17 +426,30 @@ class PageController {
|
||||
* @return void
|
||||
*/
|
||||
private function setup_screen_options(): void {
|
||||
$params = $this->get_query_params();
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$handler = $this->get_default_handler();
|
||||
$list_table = null;
|
||||
|
||||
if ( 'list_files' === $params['view'] ) {
|
||||
// Ensure list table columns are initialized early enough to enable column hiding.
|
||||
$this->get_list_table()->prepare_column_headers();
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
if ( in_array( $params['view'], array( 'list_files', 'search_results' ), true ) ) {
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
}
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
$list_table = WC_Admin_Status::get_db_log_list_table();
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $list_table instanceof WP_List_Table ) {
|
||||
// Ensure list table columns are initialized early enough to enable column hiding, if available.
|
||||
$list_table->prepare_column_headers();
|
||||
|
||||
add_screen_option(
|
||||
'per_page',
|
||||
array(
|
||||
'default' => 20,
|
||||
'option' => ListTable::PER_PAGE_USER_OPTION_KEY,
|
||||
'default' => $list_table->get_per_page_default(),
|
||||
'option' => $list_table::PER_PAGE_USER_OPTION_KEY,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -388,13 +461,19 @@ class PageController {
|
||||
* @return void
|
||||
*/
|
||||
private function handle_list_table_bulk_actions(): void {
|
||||
// Bail if we're not using the file handler.
|
||||
if ( LogHandlerFileV2::class !== $this->get_default_handler() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = $this->get_query_params( array( 'file_id', 'view' ) );
|
||||
|
||||
// Bail if this is not the list table view.
|
||||
$params = $this->get_query_params();
|
||||
if ( 'list_files' !== $params['view'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $this->get_list_table()->current_action();
|
||||
$action = $this->get_list_table( $params['view'] )->current_action();
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : $this->get_logs_tab_url();
|
||||
@@ -409,16 +488,7 @@ class PageController {
|
||||
$sendback = remove_query_arg( array( 'deleted' ), wp_get_referer() );
|
||||
|
||||
// Multiple file_id[] params will be filtered separately, but assigned to $files as an array.
|
||||
$file_ids = filter_input(
|
||||
INPUT_GET,
|
||||
'file_id',
|
||||
FILTER_CALLBACK,
|
||||
array(
|
||||
'options' => function( $file ) {
|
||||
return sanitize_file_name( wp_unslash( $file ) );
|
||||
},
|
||||
)
|
||||
);
|
||||
$file_ids = $params['file_id'];
|
||||
|
||||
if ( ! is_array( $file_ids ) || count( $file_ids ) < 1 ) {
|
||||
wp_safe_redirect( $sendback );
|
||||
@@ -426,6 +496,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 );
|
||||
@@ -475,28 +556,31 @@ class PageController {
|
||||
/**
|
||||
* Format a log file line.
|
||||
*
|
||||
* @param string $text The unformatted log file line.
|
||||
* @param string $line The unformatted log file line.
|
||||
* @param int $line_number The line number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function format_line( string $text, int $line_number ): string {
|
||||
private function format_line( string $line, int $line_number ): string {
|
||||
$severity_levels = WC_Log_Levels::get_all_severity_levels();
|
||||
$classes = array( 'line' );
|
||||
|
||||
$text = esc_html( trim( $text ) );
|
||||
if ( empty( $text ) ) {
|
||||
$text = ' ';
|
||||
$line = esc_html( trim( $line ) );
|
||||
if ( empty( $line ) ) {
|
||||
$line = ' ';
|
||||
}
|
||||
|
||||
$segments = explode( ' ', $text, 3 );
|
||||
$segments = explode( ' ', $line, 3 );
|
||||
$has_timestamp = false;
|
||||
$has_level = false;
|
||||
|
||||
if ( isset( $segments[0] ) && false !== strtotime( $segments[0] ) ) {
|
||||
$classes[] = 'log-entry';
|
||||
$segments[0] = sprintf(
|
||||
$classes[] = 'log-entry';
|
||||
$segments[0] = sprintf(
|
||||
'<span class="log-timestamp">%s</span>',
|
||||
$segments[0]
|
||||
);
|
||||
$has_timestamp = true;
|
||||
}
|
||||
|
||||
if ( isset( $segments[1] ) && in_array( strtolower( $segments[1] ), $severity_levels, true ) ) {
|
||||
@@ -505,10 +589,32 @@ class PageController {
|
||||
esc_attr( 'log-level log-level--' . strtolower( $segments[1] ) ),
|
||||
esc_html( $segments[1] )
|
||||
);
|
||||
$has_level = true;
|
||||
}
|
||||
|
||||
if ( isset( $segments[2] ) && $has_timestamp && $has_level ) {
|
||||
$message_chunks = explode( 'CONTEXT:', $segments[2], 2 );
|
||||
if ( isset( $message_chunks[1] ) ) {
|
||||
try {
|
||||
$maybe_json = stripslashes( html_entity_decode( trim( $message_chunks[1] ) ) );
|
||||
$context = json_decode( $maybe_json, false, 512, JSON_THROW_ON_ERROR );
|
||||
|
||||
$message_chunks[1] = sprintf(
|
||||
'<details><summary>%1$s</summary><pre>%2$s</pre></details>',
|
||||
esc_html__( 'Additional context', 'woocommerce' ),
|
||||
wp_json_encode( $context, JSON_PRETTY_PRINT )
|
||||
);
|
||||
|
||||
$segments[2] = implode( ' ', $message_chunks );
|
||||
$classes[] = 'has-context';
|
||||
} catch ( \JsonException $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
// It's not valid JSON so don't do anything with it.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $segments ) > 1 ) {
|
||||
$text = implode( ' ', $segments );
|
||||
$line = implode( ' ', $segments );
|
||||
}
|
||||
|
||||
$classes = implode( ' ', $classes );
|
||||
@@ -523,8 +629,65 @@ class PageController {
|
||||
),
|
||||
sprintf(
|
||||
'<span class="line-content">%s</span>',
|
||||
wp_kses_post( $text )
|
||||
wp_kses_post( $line )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a form for searching within log files.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_search_field(): void {
|
||||
$params = $this->get_query_params( array( 'date_end', 'date_filter', 'date_start', 'search', 'source' ) );
|
||||
$defaults = $this->get_query_param_defaults();
|
||||
$file_count = $this->file_controller->get_files( $params, true );
|
||||
|
||||
if ( $file_count > 0 ) {
|
||||
?>
|
||||
<form id="logs-search" class="wc-logs-search" method="get">
|
||||
<fieldset class="wc-logs-search-fieldset">
|
||||
<input type="hidden" name="page" value="wc-status" />
|
||||
<input type="hidden" name="tab" value="logs" />
|
||||
<input type="hidden" name="view" value="search_results" />
|
||||
<?php foreach ( $params as $key => $value ) : ?>
|
||||
<?php if ( $value !== $defaults[ $key ] ) : ?>
|
||||
<input
|
||||
type="hidden"
|
||||
name="<?php echo esc_attr( $key ); ?>"
|
||||
value="<?php echo esc_attr( $value ); ?>"
|
||||
/>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<label for="logs-search-field">
|
||||
<?php esc_html_e( 'Search within these files', 'woocommerce' ); ?>
|
||||
<input
|
||||
id="logs-search-field"
|
||||
class="wc-logs-search-field"
|
||||
type="text"
|
||||
name="search"
|
||||
value="<?php echo esc_attr( $params['search'] ); ?>"
|
||||
/>
|
||||
</label>
|
||||
<?php submit_button( __( 'Search', 'woocommerce' ), 'secondary', null, false ); ?>
|
||||
</fieldset>
|
||||
<?php if ( $file_count >= $this->file_controller::SEARCH_MAX_FILES ) : ?>
|
||||
<div class="wc-logs-search-notice">
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is a number.
|
||||
esc_html__(
|
||||
'⚠️ Only %s files can be searched at one time. Try filtering the file list before searching.',
|
||||
'woocommerce'
|
||||
),
|
||||
esc_html( number_format_i18n( $this->file_controller::SEARCH_MAX_FILES ) )
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the Marketplace feature.
|
||||
@@ -18,15 +19,18 @@ class Marketplace {
|
||||
* Class initialization, to be executed when the class is resolved by the container.
|
||||
*/
|
||||
final public function init() {
|
||||
if ( FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 70 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
|
||||
// Add a Woo Marketplace link to the plugin install action links.
|
||||
add_filter( 'install_plugins_tabs', array( $this, 'add_woo_plugin_install_action_link' ) );
|
||||
add_action( 'install_plugins_pre_woo', array( $this, 'maybe_open_woo_tab' ) );
|
||||
add_action( 'admin_print_styles-plugin-install.php', array( $this, 'add_plugins_page_styles' ) );
|
||||
if ( false === FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
|
||||
/** Feature controller instance @var FeaturesController $feature_controller */
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
$feature_controller->change_feature_enable( 'marketplace', true );
|
||||
}
|
||||
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 70 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
// Add a Woo Marketplace link to the plugin install action links.
|
||||
add_filter( 'install_plugins_tabs', array( $this, 'add_woo_plugin_install_action_link' ) );
|
||||
add_action( 'install_plugins_pre_woo', array( $this, 'maybe_open_woo_tab' ) );
|
||||
add_action( 'admin_print_styles-plugin-install.php', array( $this, 'add_plugins_page_styles' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomerHistory;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\OrderAttribution;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Class Edit.
|
||||
@@ -77,6 +81,7 @@ class Edit {
|
||||
add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $screen_id, 'normal', 'default' );
|
||||
/* Translators: %s order type name. */
|
||||
add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Actions::output', $screen_id, 'side', 'high' );
|
||||
self::maybe_register_order_attribution( $screen_id, $title );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,6 +207,71 @@ class Edit {
|
||||
$this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register order attribution meta boxes if the feature is enabled.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $screen_id Screen ID.
|
||||
* @param string $title Title of the page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function maybe_register_order_attribution( string $screen_id, string $title ) {
|
||||
/**
|
||||
* Features controller.
|
||||
*
|
||||
* @var FeaturesController $feature_controller
|
||||
*/
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
if ( ! $feature_controller->feature_is_enabled( 'order_attribution' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order attribution meta box.
|
||||
*
|
||||
* @var OrderAttribution $order_attribution_meta_box
|
||||
*/
|
||||
$order_attribution_meta_box = wc_get_container()->get( OrderAttribution::class );
|
||||
|
||||
add_meta_box(
|
||||
'woocommerce-order-source-data',
|
||||
/* Translators: %s order type name. */
|
||||
sprintf( __( '%s attribution', 'woocommerce' ), $title ),
|
||||
function( $post_or_order ) use ( $order_attribution_meta_box ) {
|
||||
$order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order );
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$order_attribution_meta_box->output( $order );
|
||||
}
|
||||
},
|
||||
$screen_id,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
/**
|
||||
* Customer history meta box.
|
||||
*
|
||||
* @var CustomerHistory $customer_history_meta_box
|
||||
*/
|
||||
$customer_history_meta_box = wc_get_container()->get( CustomerHistory::class );
|
||||
|
||||
add_meta_box(
|
||||
'woocommerce-customer-history',
|
||||
__( 'Customer history', 'woocommerce' ),
|
||||
function( $post_or_order ) use ( $customer_history_meta_box ) {
|
||||
$order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order );
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$customer_history_meta_box->output( $order );
|
||||
}
|
||||
},
|
||||
$screen_id,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Class CustomerHistory
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class CustomerHistory {
|
||||
|
||||
use OrderAttributionMeta;
|
||||
|
||||
/**
|
||||
* Output the customer history template for the order.
|
||||
*
|
||||
* @param WC_Order $order The order object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function output( WC_Order $order ): void {
|
||||
$this->display_customer_history( $order->get_customer_id(), $order->get_billing_email() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the customer history template for the customer.
|
||||
*
|
||||
* @param int $customer_id The customer ID.
|
||||
* @param string $billing_email The customer billing email.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function display_customer_history( int $customer_id, string $billing_email ): void {
|
||||
$has_customer_id = false;
|
||||
if ( $customer_id ) {
|
||||
$has_customer_id = true;
|
||||
$args = $this->get_customer_history( $customer_id );
|
||||
} elseif ( $billing_email ) {
|
||||
$args = $this->get_customer_history( $billing_email );
|
||||
} else {
|
||||
$args = array(
|
||||
'order_count' => 0,
|
||||
'total_spent' => 0,
|
||||
'average_spent' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$args['has_customer_id'] = $has_customer_id;
|
||||
wc_get_template( 'order/customer-history.php', $args );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Class OrderAttribution
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
class OrderAttribution {
|
||||
|
||||
use OrderAttributionMeta;
|
||||
|
||||
/**
|
||||
* OrderAttribution constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->set_fields_and_prefix();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the meta data for display.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param array $meta The array of meta data to format.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function format_meta_data( array &$meta ) {
|
||||
|
||||
if ( array_key_exists( 'device_type', $meta ) ) {
|
||||
|
||||
switch ( $meta['device_type'] ) {
|
||||
case 'Mobile':
|
||||
$meta['device_type'] = __( 'Mobile', 'woocommerce' );
|
||||
break;
|
||||
case 'Tablet':
|
||||
$meta['device_type'] = __( 'Tablet', 'woocommerce' );
|
||||
break;
|
||||
case 'Desktop':
|
||||
$meta['device_type'] = __( 'Desktop', 'woocommerce' );
|
||||
break;
|
||||
|
||||
default:
|
||||
$meta['device_type'] = __( 'Unknown', 'woocommerce' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the attribution data metabox for the order.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param WC_Order $order The order object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function output( WC_Order $order ) {
|
||||
$meta = $this->filter_meta_data( $order->get_meta_data() );
|
||||
|
||||
// If we don't have any meta to show, return.
|
||||
if ( empty( $meta ) ) {
|
||||
esc_html_e( 'No order source data available.', 'woocommerce' );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->format_meta_data( $meta );
|
||||
|
||||
$template_data = array(
|
||||
'meta' => $meta,
|
||||
// Only show more details toggle if there is more than just the origin.
|
||||
'has_more_details' => array( 'origin' ) !== array_keys( $meta ),
|
||||
);
|
||||
wc_get_template( 'order/attribution-data-fields.php', $template_data );
|
||||
}
|
||||
}
|
||||
@@ -410,8 +410,9 @@ class WCAdminAssets {
|
||||
* Injects wp-shared-settings as a dependency if it's present.
|
||||
*/
|
||||
public function inject_wc_settings_dependencies() {
|
||||
$wp_scripts = wp_scripts();
|
||||
if ( wp_script_is( 'wc-settings', 'registered' ) ) {
|
||||
$handles_for_injection = [
|
||||
$handles_for_injection = array(
|
||||
'wc-admin-layout',
|
||||
'wc-csv',
|
||||
'wc-currency',
|
||||
@@ -426,11 +427,31 @@ class WCAdminAssets {
|
||||
'wc-tracks',
|
||||
'wc-block-templates',
|
||||
'wc-product-editor',
|
||||
];
|
||||
);
|
||||
foreach ( $handles_for_injection as $handle ) {
|
||||
$script = wp_scripts()->query( $handle, 'registered' );
|
||||
$script = $wp_scripts->query( $handle, 'registered' );
|
||||
if ( $script instanceof _WP_Dependency ) {
|
||||
$script->deps[] = 'wc-settings';
|
||||
$wp_scripts->add_data( $handle, 'group', 1 );
|
||||
}
|
||||
}
|
||||
foreach ( $wp_scripts->registered as $handle => $script ) {
|
||||
// scripts that are loaded in the footer has extra->group = 1.
|
||||
if ( array_intersect( $handles_for_injection, $script->deps ) && ! isset( $script->extra['group'] ) ) {
|
||||
// Append the script to footer.
|
||||
$wp_scripts->add_data( $handle, 'group', 1 );
|
||||
// Show a warning.
|
||||
$error_handle = 'wc-settings-dep-in-header';
|
||||
$used_deps = implode( ', ', array_intersect( $handles_for_injection, $script->deps ) );
|
||||
$error_message = "Scripts that have a dependency on [$used_deps] must be loaded in the footer, {$handle} was registered to load in the header, but has been switched to load in the footer instead. See https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5059";
|
||||
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter,WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
wp_register_script( $error_handle, '' );
|
||||
wp_enqueue_script( $error_handle );
|
||||
wp_add_inline_script(
|
||||
$error_handle,
|
||||
sprintf( 'console.warn( "%s" );', $error_message )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user