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:
Tony Volpe
2024-01-19 16:44:43 +00:00
parent 2699b5437a
commit be83910651
2125 changed files with 179300 additions and 35639 deletions

View File

@@ -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 )
) {

View File

@@ -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.
*/

View File

@@ -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.
*

View File

@@ -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 );
}
}

View File

@@ -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 );
}
}

View File

@@ -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(
' &ndash; <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'];
}
}

View File

@@ -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'];
}
}

View File

@@ -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;
}
}

View File

@@ -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 = '&nbsp;';
$line = esc_html( trim( $line ) );
if ( empty( $line ) ) {
$line = '&nbsp;';
}
$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
}
}
}

View File

@@ -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' ) );
}
/**

View File

@@ -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.
*

View File

@@ -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 );
}
}

View File

@@ -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 );
}
}

View File

@@ -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 )
);
}
}
}