plugin updates
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template registry.
|
||||
*/
|
||||
final class BlockTemplateRegistry {
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var BlockTemplateRegistry|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Templates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $templates = array();
|
||||
|
||||
/**
|
||||
* Get the instance of the class.
|
||||
*/
|
||||
public static function get_instance(): BlockTemplateRegistry {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single template.
|
||||
*
|
||||
* @param BlockTemplateInterface $template Template to register.
|
||||
*
|
||||
* @throws \ValueError If a template with the same ID already exists.
|
||||
*/
|
||||
public function register( BlockTemplateInterface $template ) {
|
||||
$id = $template->get_id();
|
||||
|
||||
if ( isset( $this->templates[ $id ] ) ) {
|
||||
throw new \ValueError( 'A template with the specified ID already exists in the registry.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a template is registered.
|
||||
*
|
||||
* @param BlockTemplateInterface $template Template that was registered.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
do_action( 'woocommerce_block_template_register', $template );
|
||||
|
||||
$this->templates[ $id ] = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered templates.
|
||||
*/
|
||||
public function get_all_registered(): array {
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single registered template.
|
||||
*
|
||||
* @param string $id ID of the template.
|
||||
*/
|
||||
public function get_registered( $id ): BlockTemplateInterface {
|
||||
return isset( $this->templates[ $id ] ) ? $this->templates[ $id ] : null;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
/**
|
||||
* Block template controller.
|
||||
*/
|
||||
class BlockTemplatesController {
|
||||
|
||||
/**
|
||||
* Block template registry
|
||||
*
|
||||
* @var BlockTemplateRegistry
|
||||
*/
|
||||
private $block_template_registry;
|
||||
|
||||
/**
|
||||
* Block template transformer.
|
||||
*
|
||||
* @var TemplateTransformer
|
||||
*/
|
||||
private $template_transformer;
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init( $block_template_registry, $template_transformer ) {
|
||||
$this->block_template_registry = $block_template_registry;
|
||||
$this->template_transformer = $template_transformer;
|
||||
add_action( 'rest_api_init', array( $this, 'register_templates' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register templates in the blocks endpoint.
|
||||
*/
|
||||
public function register_templates() {
|
||||
$templates = $this->block_template_registry->get_all_registered();
|
||||
|
||||
foreach ( $templates as $template ) {
|
||||
add_filter( 'pre_get_block_templates', function( $query_result, $query, $template_type ) use( $template ) {
|
||||
if ( ! isset( $query['area'] ) || $query['area'] !== $template->get_area() ) {
|
||||
return $query_result;
|
||||
}
|
||||
|
||||
$wp_block_template = $this->template_transformer->transform( $template );
|
||||
$query_result[] = $wp_block_template;
|
||||
|
||||
return $query_result;
|
||||
}, 10, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Template transformer.
|
||||
*/
|
||||
class TemplateTransformer {
|
||||
|
||||
/**
|
||||
* Transform the WooCommerceBlockTemplate to a WP_Block_Template.
|
||||
*
|
||||
* @param object $block_template The product template.
|
||||
*/
|
||||
public function transform( BlockTemplateInterface $block_template ): \WP_Block_Template {
|
||||
$template = new \WP_Block_Template();
|
||||
$template->id = $block_template->get_id();
|
||||
$template->theme = 'woocommerce/woocommerce';
|
||||
$template->content = $block_template->get_formatted_template();
|
||||
$template->source = 'plugin';
|
||||
$template->slug = $block_template->get_id();
|
||||
$template->type = 'wp_template';
|
||||
$template->title = $block_template->get_title();
|
||||
$template->description = $block_template->get_description();
|
||||
$template->status = 'publish';
|
||||
$template->has_theme_file = true;
|
||||
$template->origin = 'plugin';
|
||||
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
|
||||
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
|
||||
$template->area = $block_template->get_area();
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -131,4 +131,19 @@ abstract class AbstractBlockTemplate implements BlockTemplateInterface {
|
||||
|
||||
return $inner_blocks_formatted_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template as JSON like array.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
public function to_json(): array {
|
||||
return array(
|
||||
'id' => $this->get_id(),
|
||||
'title' => $this->get_title(),
|
||||
'description' => $this->get_description(),
|
||||
'area' => $this->get_area(),
|
||||
'blockTemplates' => $this->get_formatted_template(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,25 +210,35 @@ class BlockTemplateLogger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template events for a given template.
|
||||
* Get all template events for a given template as a JSON like array.
|
||||
*
|
||||
* @param string $template_id Template ID.
|
||||
*/
|
||||
public function get_formatted_template_events( string $template_id ): array {
|
||||
public function template_events_to_json( string $template_id ): array {
|
||||
if ( ! isset( $this->all_template_events[ $template_id ] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$template_events = $this->all_template_events[ $template_id ];
|
||||
$template = $this->templates[ $template_id ];
|
||||
|
||||
$formatted_template_events = array();
|
||||
return $this->to_json( $template_events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template events as a JSON like array.
|
||||
*
|
||||
* @param array $template_events Template events.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
private function to_json( array $template_events ): array {
|
||||
$json = array();
|
||||
|
||||
foreach ( $template_events as $template_event ) {
|
||||
$container = $template_event['container'];
|
||||
$block = $template_event['block'];
|
||||
|
||||
$formatted_template_events[] = array(
|
||||
$json[] = array(
|
||||
'level' => $template_event['level'],
|
||||
'event_type' => $template_event['event_type'],
|
||||
'message' => $template_event['message'],
|
||||
@@ -246,7 +256,7 @@ class BlockTemplateLogger {
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted_template_events;
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,7 +321,7 @@ class BlockTemplateLogger {
|
||||
* @param array $template_events Template events.
|
||||
*/
|
||||
private function generate_template_events_hash( array $template_events ): string {
|
||||
return md5( wp_json_encode( $template_events ) );
|
||||
return md5( wp_json_encode( $this->to_json( $template_events ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace Automattic\WooCommerce\Internal\Admin;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
|
||||
use Automattic\WooCommerce\Internal\Admin\Settings;
|
||||
@@ -73,7 +72,6 @@ class Loader {
|
||||
|
||||
wc_get_container()->get( Reviews::class );
|
||||
wc_get_container()->get( ReviewsCommentsOverrides::class );
|
||||
wc_get_container()->get( BlockTemplatesController::class );
|
||||
|
||||
add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
|
||||
add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
|
||||
|
||||
@@ -86,7 +86,7 @@ class FileController {
|
||||
* Class FileController
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->log_directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
|
||||
$this->log_directory = trailingslashit( Constants::get_constant( 'WC_LOG_DIR' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController };
|
||||
use WC_Log_Handler;
|
||||
|
||||
/**
|
||||
@@ -18,11 +17,19 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of the Settings class.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* LogHandlerFileV2 class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->file_controller = wc_get_container()->get( FileController::class );
|
||||
$this->settings = wc_get_container()->get( Settings::class );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,20 +80,17 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
$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";
|
||||
$formatted_context = wp_json_encode( $context );
|
||||
$message .= " CONTEXT: $formatted_context";
|
||||
}
|
||||
|
||||
$entry = "$time_string $level_string $formatted_message";
|
||||
$entry = "$time_string $level_string $message";
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/** This filter is documented in includes/abstracts/abstract-wc-log-handler.php */
|
||||
@@ -152,6 +156,63 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
return sanitize_title( $source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs from a specific source.
|
||||
*
|
||||
* @param string $source The source of the log entries.
|
||||
*
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function clear( string $source ): int {
|
||||
$source = File::sanitize_source( $source );
|
||||
|
||||
$files = $this->file_controller->get_files(
|
||||
array(
|
||||
'source' => $source,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file_ids = array_map(
|
||||
fn( $file ) => $file->get_file_id(),
|
||||
$files
|
||||
);
|
||||
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
time(),
|
||||
'info',
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %1$s is a number of log files, %2$s is a slug-style name for a file.
|
||||
_n(
|
||||
'%1$s log file from source %2$s was deleted.',
|
||||
'%1$s log files from source %2$s were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $deleted ),
|
||||
sprintf(
|
||||
'<code>%s</code>',
|
||||
esc_html( $source )
|
||||
)
|
||||
),
|
||||
array(
|
||||
'source' => 'wc_logger',
|
||||
'backtrace' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs older than a specified timestamp.
|
||||
*
|
||||
@@ -172,7 +233,7 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) ) {
|
||||
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -181,12 +242,8 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
$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
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
$retention_days = $this->settings->get_retention_period();
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
|
||||
@@ -4,7 +4,7 @@ declare( strict_types = 1 );
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\{ LogHandlerFileV2, Settings };
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController, FileListTable, SearchListTable };
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Status;
|
||||
@@ -26,6 +26,13 @@ class PageController {
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of Settings.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Instance of FileListTable or SearchListTable.
|
||||
*
|
||||
@@ -39,13 +46,16 @@ class PageController {
|
||||
* @internal
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param Settings $settings Instance of Settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function init(
|
||||
FileController $file_controller
|
||||
FileController $file_controller,
|
||||
Settings $settings
|
||||
): void {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->settings = $settings;
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
@@ -56,8 +66,61 @@ class PageController {
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'setup_screen_options' ) );
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'handle_list_table_bulk_actions' ) );
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'maybe_do_logs_tab_action' ), 2 );
|
||||
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'setup_screen_options' ) );
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'handle_list_table_bulk_actions' ) );
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'notices' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current tab on the Status page is Logs, and if so, fire an action.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_do_logs_tab_action(): void {
|
||||
$is_logs_tab = 'logs' === filter_input( INPUT_GET, 'tab' );
|
||||
|
||||
if ( $is_logs_tab ) {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
/**
|
||||
* Action fires when the Logs tab starts loading.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_load_tab', $params['view'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notices to display on Logs screens.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function notices() {
|
||||
if ( ! $this->settings->logging_is_enabled() ) {
|
||||
add_action(
|
||||
'admin_notices',
|
||||
function() {
|
||||
?>
|
||||
<div class="notice notice-warning">
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is a URL to another admin screen.
|
||||
wp_kses_post( __( 'Logging is disabled. It can be enabled in <a href="%s">Logs Settings</a>.', 'woocommerce' ) ),
|
||||
esc_url( add_query_arg( 'view', 'settings', $this->get_logs_tab_url() ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,40 +138,83 @@ class PageController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the default log handler.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_handler(): string {
|
||||
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
|
||||
|
||||
if ( is_null( $handler ) || ! class_exists( $handler ) ) {
|
||||
$handler = WC_Log_Handler_File::class;
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Logs" tab, depending on the current default log handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render(): void {
|
||||
$handler = $this->get_default_handler();
|
||||
$handler = $this->settings->get_default_handler();
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
$this->render_section_nav();
|
||||
|
||||
if ( 'settings' === $params['view'] ) {
|
||||
$this->settings->render_form();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
$this->render_filev2();
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
return;
|
||||
case WC_Log_Handler_DB::class:
|
||||
WC_Admin_Status::status_logs_db();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
case WC_Log_Handler_File::class:
|
||||
WC_Admin_Status::status_logs_file();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action fires only if there is not a built-in rendering method for the current default log handler.
|
||||
*
|
||||
* This is intended as a way for extensions to render log views for custom handlers.
|
||||
*
|
||||
* @param string $handler
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_render_page', $handler );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render navigation to switch between logs browsing and settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_section_nav(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$browse_url = $this->get_logs_tab_url();
|
||||
$settings_url = add_query_arg( 'view', 'settings', $this->get_logs_tab_url() );
|
||||
|
||||
?>
|
||||
<ul class="subsubsub">
|
||||
<li>
|
||||
<?php
|
||||
printf(
|
||||
'<a href="%1$s"%2$s>%3$s</a>',
|
||||
esc_url( $browse_url ),
|
||||
'settings' !== $params['view'] ? ' class="current"' : '',
|
||||
esc_html__( 'Browse', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
|
|
||||
</li>
|
||||
<li>
|
||||
<?php
|
||||
printf(
|
||||
'<a href="%1$s"%2$s>%3$s</a>',
|
||||
esc_url( $settings_url ),
|
||||
'settings' === $params['view'] ? ' class="current"' : '',
|
||||
esc_html__( 'Settings', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
</li>
|
||||
</ul>
|
||||
<br class="clear">
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +400,17 @@ class PageController {
|
||||
?>
|
||||
<?php endwhile; ?>
|
||||
</section>
|
||||
<script>
|
||||
// Clear the line number hash and highlight with a click.
|
||||
document.documentElement.addEventListener( 'click', ( event ) => {
|
||||
if ( window.location.hash && ! event.target.classList.contains( 'line-anchor' ) ) {
|
||||
let scrollPos = document.documentElement.scrollTop;
|
||||
window.location.hash = '';
|
||||
document.documentElement.scrollTop = scrollPos;
|
||||
history.replaceState( null, '', window.location.pathname + window.location.search );
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -303,7 +420,7 @@ class PageController {
|
||||
* @return void
|
||||
*/
|
||||
private function render_search_results_view(): void {
|
||||
$params = $this->get_query_params( array( 'order', 'orderby', 'search', 'source', 'view' ) );
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
|
||||
$list_table->prepare_items();
|
||||
@@ -380,7 +497,7 @@ class PageController {
|
||||
'view' => array(
|
||||
'filter' => FILTER_VALIDATE_REGEXP,
|
||||
'options' => array(
|
||||
'regexp' => '/^(list_files|single_file|search_results)$/',
|
||||
'regexp' => '/^(list_files|single_file|search_results|settings)$/',
|
||||
'default' => $defaults['view'],
|
||||
),
|
||||
),
|
||||
@@ -423,17 +540,18 @@ class PageController {
|
||||
/**
|
||||
* Register screen options for the logging views.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setup_screen_options(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$handler = $this->get_default_handler();
|
||||
private function setup_screen_options( string $view ): void {
|
||||
$handler = $this->settings->get_default_handler();
|
||||
$list_table = null;
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
if ( in_array( $params['view'], array( 'list_files', 'search_results' ), true ) ) {
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
if ( in_array( $view, array( 'list_files', 'search_results' ), true ) ) {
|
||||
$list_table = $this->get_list_table( $view );
|
||||
}
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
@@ -458,22 +576,24 @@ class PageController {
|
||||
/**
|
||||
* Process bulk actions initiated from the log file list table.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_list_table_bulk_actions(): void {
|
||||
private function handle_list_table_bulk_actions( string $view ): void {
|
||||
// Bail if we're not using the file handler.
|
||||
if ( LogHandlerFileV2::class !== $this->get_default_handler() ) {
|
||||
if ( LogHandlerFileV2::class !== $this->settings->get_default_handler() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = $this->get_query_params( array( 'file_id', 'view' ) );
|
||||
$params = $this->get_query_params( array( 'file_id' ) );
|
||||
|
||||
// Bail if this is not the list table view.
|
||||
if ( 'list_files' !== $params['view'] ) {
|
||||
if ( 'list_files' !== $view ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $this->get_list_table( $params['view'] )->current_action();
|
||||
$action = $this->get_list_table( $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();
|
||||
@@ -562,10 +682,9 @@ class PageController {
|
||||
* @return string
|
||||
*/
|
||||
private function format_line( string $line, int $line_number ): string {
|
||||
$severity_levels = WC_Log_Levels::get_all_severity_levels();
|
||||
$classes = array( 'line' );
|
||||
$classes = array( 'line' );
|
||||
|
||||
$line = esc_html( trim( $line ) );
|
||||
$line = esc_html( $line );
|
||||
if ( empty( $line ) ) {
|
||||
$line = ' ';
|
||||
}
|
||||
@@ -583,11 +702,11 @@ class PageController {
|
||||
$has_timestamp = true;
|
||||
}
|
||||
|
||||
if ( isset( $segments[1] ) && in_array( strtolower( $segments[1] ), $severity_levels, true ) ) {
|
||||
if ( isset( $segments[1] ) && WC_Log_Levels::is_valid_level( strtolower( $segments[1] ) ) ) {
|
||||
$segments[1] = sprintf(
|
||||
'<span class="%1$s">%2$s</span>',
|
||||
esc_attr( 'log-level log-level--' . strtolower( $segments[1] ) ),
|
||||
esc_html( $segments[1] )
|
||||
esc_html( WC_Log_Levels::get_level_label( strtolower( $segments[1] ) ) )
|
||||
);
|
||||
$has_level = true;
|
||||
}
|
||||
@@ -600,7 +719,7 @@ class PageController {
|
||||
$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>',
|
||||
'<details><summary>%1$s</summary>%2$s</details>',
|
||||
esc_html__( 'Additional context', 'woocommerce' ),
|
||||
wp_json_encode( $context, JSON_PRETTY_PRINT )
|
||||
);
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Settings;
|
||||
use WC_Log_Handler, WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
|
||||
|
||||
/**
|
||||
* Settings class.
|
||||
*/
|
||||
class Settings {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Default values for logging settings.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
private const DEFAULTS = array(
|
||||
'logging_enabled' => true,
|
||||
'default_handler' => LogHandlerFileV2::class,
|
||||
'retention_period_days' => 30,
|
||||
'level_threshold' => 'none',
|
||||
'file_entry_collapse_lines' => true,
|
||||
);
|
||||
|
||||
/**
|
||||
* The prefix for settings keys used in the options table.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const PREFIX = 'woocommerce_logs_';
|
||||
|
||||
/**
|
||||
* Class Settings.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'save_settings' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render and save settings controls.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_settings_definitions(): array {
|
||||
$settings = array(
|
||||
'start' => array(
|
||||
'title' => __( 'Logs settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'logging_enabled' => array(
|
||||
'title' => __( 'Logger', 'woocommerce' ),
|
||||
'desc' => __( 'Enable logging', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'logging_enabled',
|
||||
'type' => 'checkbox',
|
||||
'value' => $this->logging_is_enabled() ? 'yes' : 'no',
|
||||
'default' => self::DEFAULTS['logging_enabled'] ? 'yes' : 'no',
|
||||
'autoload' => false,
|
||||
),
|
||||
'default_handler' => array(),
|
||||
'retention_period_days' => array(),
|
||||
'level_threshold' => array(),
|
||||
'end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
|
||||
if ( true === $this->logging_is_enabled() ) {
|
||||
$settings['default_handler'] = $this->get_default_handler_setting_definition();
|
||||
$settings['retention_period_days'] = $this->get_retention_period_days_setting_definition();
|
||||
$settings['level_threshold'] = $this->get_level_threshold_setting_definition();
|
||||
}
|
||||
|
||||
$default_handler = $this->get_default_handler();
|
||||
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
|
||||
$settings += $this->get_filesystem_settings_definitions();
|
||||
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
|
||||
$settings += $this->get_database_settings_definitions();
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the default_handler setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_default_handler_setting_definition(): array {
|
||||
$handler_options = array(
|
||||
LogHandlerFileV2::class => __( 'File system (default)', 'woocommerce' ),
|
||||
WC_Log_Handler_DB::class => __( 'Database (not recommended on live sites)', 'woocommerce' ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the list of logging handlers that can be set as the default handler.
|
||||
*
|
||||
* @param array $handler_options An associative array of class_name => description.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$handler_options = apply_filters( 'woocommerce_logger_handler_options', $handler_options );
|
||||
|
||||
$current_value = $this->get_default_handler();
|
||||
if ( ! array_key_exists( $current_value, $handler_options ) ) {
|
||||
$handler_options[ $current_value ] = $current_value;
|
||||
}
|
||||
|
||||
$desc = array();
|
||||
|
||||
$desc[] = __( 'Note that if this setting is changed, any log entries that have already been recorded will remain stored in their current location, but will not migrate.', 'woocommerce' );
|
||||
|
||||
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_HANDLER' ) );
|
||||
if ( $hardcoded ) {
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a code variable.
|
||||
__( 'This setting cannot be changed here because it is defined in the %s constant.', 'woocommerce' ),
|
||||
'<code>WC_LOG_HANDLER</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Log storage', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This determines where log entries are saved.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'default_handler',
|
||||
'type' => 'radio',
|
||||
'value' => $current_value,
|
||||
'default' => self::DEFAULTS['default_handler'],
|
||||
'autoload' => false,
|
||||
'options' => $handler_options,
|
||||
'disabled' => $hardcoded ? array_keys( $handler_options ) : array(),
|
||||
'desc' => implode( '<br><br>', $desc ),
|
||||
'desc_at_end' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the retention_period_days setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_retention_period_days_setting_definition(): array {
|
||||
$custom_attributes = array(
|
||||
'min' => 1,
|
||||
'step' => 1,
|
||||
);
|
||||
|
||||
$hardcoded = has_filter( 'woocommerce_logger_days_to_retain_logs' );
|
||||
$desc = '';
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
|
||||
$desc = sprintf(
|
||||
// translators: %s is the name of a filter hook.
|
||||
__( 'This setting cannot be changed here because it is being set by a filter on the %s hook.', 'woocommerce' ),
|
||||
'<code>woocommerce_logger_days_to_retain_logs</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Retention period', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets how many days log entries will be kept before being auto-deleted.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'retention_period_days',
|
||||
'type' => 'number',
|
||||
'value' => $this->get_retention_period(),
|
||||
'default' => self::DEFAULTS['retention_period_days'],
|
||||
'autoload' => false,
|
||||
'custom_attributes' => $custom_attributes,
|
||||
'css' => 'width:70px;',
|
||||
'row_class' => 'logs-retention-period-days',
|
||||
'suffix' => sprintf(
|
||||
' %s',
|
||||
__( 'days', 'woocommerce' ),
|
||||
),
|
||||
'desc' => $desc,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the level_threshold setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_level_threshold_setting_definition(): array {
|
||||
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_THRESHOLD' ) );
|
||||
$desc = '';
|
||||
if ( $hardcoded ) {
|
||||
$desc = sprintf(
|
||||
// translators: %1$s is the name of a code variable. %2$s is the name of a file.
|
||||
__( 'This setting cannot be changed here because it is defined in the %1$s constant, probably in your %2$s file.', 'woocommerce' ),
|
||||
'<code>WC_LOG_THRESHOLD</code>',
|
||||
'<b>wp-config.php</b>'
|
||||
);
|
||||
}
|
||||
|
||||
$labels = WC_Log_Levels::get_all_level_labels();
|
||||
$labels['none'] = __( 'None', 'woocommerce' );
|
||||
|
||||
$custom_attributes = array();
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Level threshold', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets the minimum severity level of logs that will be stored. Lower severity levels will be ignored. "None" means all logs will be stored.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'level_threshold',
|
||||
'type' => 'select',
|
||||
'value' => $this->get_level_threshold(),
|
||||
'default' => self::DEFAULTS['level_threshold'],
|
||||
'autoload' => false,
|
||||
'options' => $labels,
|
||||
'custom_attributes' => $custom_attributes,
|
||||
'css' => 'width:auto;',
|
||||
'desc' => $desc,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render settings related to filesystem log handlers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_filesystem_settings_definitions(): array {
|
||||
$location_info = array();
|
||||
$directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
__( 'Log files are stored in this directory: %s', 'woocommerce' ),
|
||||
sprintf(
|
||||
'<code>%s</code>',
|
||||
esc_html( $directory )
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! wp_is_writable( $directory ) ) {
|
||||
$location_info[] = __( '⚠️ This directory does not appear to be writable.', 'woocommerce' );
|
||||
}
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %1$s is a code variable. %2$s is the name of a file.
|
||||
__( 'Change the location by defining the %1$s constant in your %2$s file with a new path.', 'woocommerce' ),
|
||||
'<code>WC_LOG_DIR</code>',
|
||||
'<code>wp-config.php</code>'
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'File system settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'log_directory' => array(
|
||||
'type' => 'info',
|
||||
'text' => implode( "\n\n", $location_info ),
|
||||
),
|
||||
'entry_format' => array(),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render settings related to database log handlers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_database_settings_definitions(): array {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}woocommerce_log";
|
||||
|
||||
$location_info = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
__( 'Log entries are stored in this database table: %s', 'woocommerce' ),
|
||||
"<code>$table</code>"
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'Database settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'database_table' => array(
|
||||
'type' => 'info',
|
||||
'text' => $location_info,
|
||||
),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission of the settings form and update the settings values.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function save_settings( string $view ): void {
|
||||
$is_saving = 'settings' === $view && isset( $_POST['save_settings'] );
|
||||
|
||||
if ( $is_saving ) {
|
||||
check_admin_referer( self::PREFIX . 'settings' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to manage logging settings.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$settings = $this->get_settings_definitions();
|
||||
|
||||
WC_Admin_Settings::save_fields( $settings );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_form(): void {
|
||||
$settings = $this->get_settings_definitions();
|
||||
|
||||
?>
|
||||
<form id="mainform" class="wc-logs-settings" method="post">
|
||||
<?php WC_Admin_Settings::output_fields( $settings ); ?>
|
||||
<?php
|
||||
/**
|
||||
* Action fires after the built-in logging settings controls have been rendered.
|
||||
*
|
||||
* This is intended as a way to allow other logging settings controls to be added by extensions.
|
||||
*
|
||||
* @param bool $enabled True if logging is currently enabled.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_settings_form_fields', $this->logging_is_enabled() );
|
||||
?>
|
||||
<?php wp_nonce_field( self::PREFIX . 'settings' ); ?>
|
||||
<?php submit_button( __( 'Save changes', 'woocommerce' ), 'primary', 'save_settings' ); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the logging_enabled setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function logging_is_enabled(): bool {
|
||||
$key = self::PREFIX . 'logging_enabled';
|
||||
|
||||
$enabled = WC_Admin_Settings::get_option( $key, self::DEFAULTS['logging_enabled'] );
|
||||
$enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
|
||||
|
||||
if ( is_null( $enabled ) ) {
|
||||
$enabled = self::DEFAULTS['logging_enabled'];
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the default_handler setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_handler(): string {
|
||||
$key = self::PREFIX . 'default_handler';
|
||||
|
||||
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
|
||||
|
||||
if ( is_null( $handler ) ) {
|
||||
$handler = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
if ( ! class_exists( $handler ) || ! is_a( $handler, 'WC_Log_Handler_Interface', true ) ) {
|
||||
$handler = self::DEFAULTS['default_handler'];
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the retention_period_days setting.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_retention_period(): int {
|
||||
$key = self::PREFIX . 'retention_period_days';
|
||||
|
||||
$retention_period = self::DEFAULTS['retention_period_days'];
|
||||
|
||||
if ( has_filter( 'woocommerce_logger_days_to_retain_logs' ) ) {
|
||||
/**
|
||||
* Filter the retention period of log entries.
|
||||
*
|
||||
* @param int $days The number of days to retain log entries.
|
||||
*
|
||||
* @since 3.4.0
|
||||
*/
|
||||
$retention_period = apply_filters( 'woocommerce_logger_days_to_retain_logs', $retention_period );
|
||||
} else {
|
||||
$retention_period = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
$retention_period = absint( $retention_period );
|
||||
|
||||
if ( $retention_period < 1 ) {
|
||||
$retention_period = self::DEFAULTS['retention_period_days'];
|
||||
}
|
||||
|
||||
return $retention_period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the level_threshold setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_level_threshold(): string {
|
||||
$key = self::PREFIX . 'level_threshold';
|
||||
|
||||
$threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' );
|
||||
|
||||
if ( is_null( $threshold ) ) {
|
||||
$threshold = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
if ( ! WC_Log_Levels::is_valid_level( $threshold ) ) {
|
||||
$threshold = self::DEFAULTS['level_threshold'];
|
||||
}
|
||||
|
||||
return $threshold;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,20 @@ class Marketing {
|
||||
|
||||
use CouponsMovedTrait;
|
||||
|
||||
/**
|
||||
* Constant representing the key for the submenu name value in the global $submenu array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SUBMENU_NAME_KEY = 0;
|
||||
|
||||
/**
|
||||
* Constant representing the key for the submenu location value in the global $submenu array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SUBMENU_LOCATION_KEY = 2;
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
@@ -44,6 +58,9 @@ class Marketing {
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 5 );
|
||||
add_action( 'admin_menu', array( $this, 'add_parent_menu_item' ), 6 );
|
||||
|
||||
// Overwrite submenu default ordering for marketing menu. High priority gives plugins the chance to register their own menu items.
|
||||
add_action( 'admin_menu', array( $this, 'reorder_marketing_submenu' ), 99 );
|
||||
|
||||
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 30 );
|
||||
}
|
||||
|
||||
@@ -140,6 +157,67 @@ class Marketing {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Order marketing menu items alphabeticaly.
|
||||
* Overview should be first, and Coupons should be second, followed by other marketing menu items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reorder_marketing_submenu() {
|
||||
global $submenu;
|
||||
|
||||
if ( ! isset( $submenu['woocommerce-marketing'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$marketing_submenu = $submenu['woocommerce-marketing'];
|
||||
$new_menu_order = array();
|
||||
|
||||
// Overview should be first.
|
||||
$overview_key = array_search( 'Overview', array_column( $marketing_submenu, self::SUBMENU_NAME_KEY ), true );
|
||||
|
||||
if ( false === $overview_key ) {
|
||||
/*
|
||||
* If Overview is not found we may be on a site witha different language.
|
||||
* We can use a fallback and try to find the overview page by its path.
|
||||
*/
|
||||
$overview_key = array_search( 'admin.php?page=wc-admin&path=/marketing', array_column( $marketing_submenu, self::SUBMENU_LOCATION_KEY ), true );
|
||||
}
|
||||
|
||||
if ( false !== $overview_key ) {
|
||||
$new_menu_order[] = $marketing_submenu[ $overview_key ];
|
||||
array_splice( $marketing_submenu, $overview_key, 1 );
|
||||
}
|
||||
|
||||
// Coupons should be second.
|
||||
$coupons_key = array_search( 'Coupons', array_column( $marketing_submenu, self::SUBMENU_NAME_KEY ), true );
|
||||
|
||||
if ( false === $coupons_key ) {
|
||||
/*
|
||||
* If Coupons is not found we may be on a site witha different language.
|
||||
* We can use a fallback and try to find the coupons page by its path.
|
||||
*/
|
||||
$coupons_key = array_search( 'edit.php?post_type=shop_coupon', array_column( $marketing_submenu, self::SUBMENU_LOCATION_KEY ), true );
|
||||
}
|
||||
|
||||
if ( false !== $coupons_key ) {
|
||||
$new_menu_order[] = $marketing_submenu[ $coupons_key ];
|
||||
array_splice( $marketing_submenu, $coupons_key, 1 );
|
||||
}
|
||||
|
||||
// Sort the rest of the items alphabetically.
|
||||
usort(
|
||||
$marketing_submenu,
|
||||
function( $a, $b ) {
|
||||
return strcmp( $a[0], $b[0] );
|
||||
}
|
||||
);
|
||||
|
||||
$new_menu_order = array_merge( $new_menu_order, $marketing_submenu );
|
||||
|
||||
$submenu['woocommerce-marketing'] = $new_menu_order; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings for marketing feature.
|
||||
*
|
||||
|
||||
@@ -14,13 +14,6 @@ namespace Automattic\WooCommerce\Internal\Admin\Marketing;
|
||||
* @since x.x.x
|
||||
*/
|
||||
class MarketingSpecs {
|
||||
/**
|
||||
* Name of recommended plugins transient.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const RECOMMENDED_PLUGINS_TRANSIENT = 'wc_marketing_recommended_plugins';
|
||||
|
||||
/**
|
||||
* Name of knowledge base post transient.
|
||||
*
|
||||
@@ -28,111 +21,6 @@ class MarketingSpecs {
|
||||
*/
|
||||
const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base';
|
||||
|
||||
/**
|
||||
* Slug of the category specifying marketing extensions on the Woo.com store.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing';
|
||||
|
||||
/**
|
||||
* Slug of the subcategory specifying marketing channels on the Woo.com store.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels';
|
||||
|
||||
/**
|
||||
* Load recommended plugins from Woo.com
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_plugins(): array {
|
||||
$plugins = get_transient( self::RECOMMENDED_PLUGINS_TRANSIENT );
|
||||
|
||||
if ( false === $plugins ) {
|
||||
$request = wp_remote_get(
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.3/recommendations.json',
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
$plugins = [];
|
||||
|
||||
if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
|
||||
$plugins = json_decode( $request['body'], true );
|
||||
}
|
||||
|
||||
set_transient(
|
||||
self::RECOMMENDED_PLUGINS_TRANSIENT,
|
||||
$plugins,
|
||||
// Expire transient in 15 minutes if remote get failed.
|
||||
// Cache an empty result to avoid repeated failed requests.
|
||||
empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
return array_values( $plugins );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the recommended marketing channels from Woo.com.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_marketing_channels(): array {
|
||||
return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all recommended marketing extensions EXCEPT the marketing channels from Woo.com.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_marketing_extensions_excluding_channels(): array {
|
||||
return array_filter(
|
||||
$this->get_recommended_plugins(),
|
||||
function ( array $plugin_data ) {
|
||||
return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a plugin is a marketing extension.
|
||||
*
|
||||
* @param array $plugin_data The plugin properties returned by the API.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_marketing_plugin( array $plugin_data ): bool {
|
||||
$categories = $plugin_data['categories'] ?? [];
|
||||
|
||||
return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a plugin is a marketing channel.
|
||||
*
|
||||
* @param array $plugin_data The plugin properties returned by the API.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_marketing_channel_plugin( array $plugin_data ): bool {
|
||||
if ( ! $this->is_marketing_plugin( $plugin_data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subcategories = $plugin_data['subcategories'] ?? [];
|
||||
foreach ( $subcategories as $subcategory ) {
|
||||
if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load knowledge base posts from Woo.com
|
||||
*
|
||||
@@ -165,21 +53,21 @@ class MarketingSpecs {
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
$posts = [];
|
||||
$posts = array();
|
||||
|
||||
if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
|
||||
$raw_posts = json_decode( $request['body'], true );
|
||||
|
||||
foreach ( $raw_posts as $raw_post ) {
|
||||
$post = [
|
||||
$post = array(
|
||||
'title' => html_entity_decode( $raw_post['title']['rendered'] ),
|
||||
'date' => $raw_post['date_gmt'],
|
||||
'link' => $raw_post['link'],
|
||||
'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '',
|
||||
'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '',
|
||||
];
|
||||
);
|
||||
|
||||
$featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? [];
|
||||
$featured_media = isset( $raw_post['_embedded']['wp:featuredmedia'] ) && is_array( $raw_post['_embedded']['wp:featuredmedia'] ) ? $raw_post['_embedded']['wp:featuredmedia'] : array();
|
||||
if ( count( $featured_media ) > 0 ) {
|
||||
$image = current( $featured_media );
|
||||
$post['image'] = add_query_arg(
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
@@ -250,6 +251,15 @@ class Edit {
|
||||
'high'
|
||||
);
|
||||
|
||||
// Add customer history meta box if analytics is enabled.
|
||||
if ( 'yes' !== get_option( 'woocommerce_analytics_enabled' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! OrderUtil::is_order_edit_screen() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer history meta box.
|
||||
*
|
||||
@@ -260,7 +270,7 @@ class Edit {
|
||||
add_meta_box(
|
||||
'woocommerce-customer-history',
|
||||
__( 'Customer history', 'woocommerce' ),
|
||||
function( $post_or_order ) use ( $customer_history_meta_box ) {
|
||||
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 );
|
||||
|
||||
@@ -372,7 +372,7 @@ class ListTable extends WP_List_Table {
|
||||
'type' => $this->order_type,
|
||||
);
|
||||
|
||||
foreach ( array( 'status', 's', 'm', '_customer_user' ) as $query_var ) {
|
||||
foreach ( array( 'status', 's', 'm', '_customer_user', 'search-filter' ) as $query_var ) {
|
||||
$this->request[ $query_var ] = sanitize_text_field( wp_unslash( $_REQUEST[ $query_var ] ?? '' ) );
|
||||
}
|
||||
|
||||
@@ -532,6 +532,11 @@ class ListTable extends WP_List_Table {
|
||||
$this->order_query_args['s'] = $search_term;
|
||||
$this->has_filter = true;
|
||||
}
|
||||
|
||||
$filter = trim( sanitize_text_field( $this->request['search-filter'] ) );
|
||||
if ( ! empty( $filter ) ) {
|
||||
$this->order_query_args['search_filter'] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -541,8 +546,22 @@ class ListTable extends WP_List_Table {
|
||||
* @return array
|
||||
*/
|
||||
public function get_views() {
|
||||
$view_links = array();
|
||||
|
||||
/**
|
||||
* Filters the list of available list table view links before the actual query runs.
|
||||
* This can be used to, e.g., remove counts from the links.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param string[] $views An array of available list table view links.
|
||||
*/
|
||||
$view_links = apply_filters( 'woocommerce_before_' . $this->order_type . '_list_table_view_links', $view_links );
|
||||
if ( ! empty( $view_links ) ) {
|
||||
return $view_links;
|
||||
}
|
||||
|
||||
$view_counts = array();
|
||||
$view_links = array();
|
||||
$statuses = $this->get_visible_statuses();
|
||||
$current = ! empty( $this->request['status'] ) ? sanitize_text_field( $this->request['status'] ) : 'all';
|
||||
$all_count = 0;
|
||||
@@ -620,6 +639,24 @@ class ListTable extends WP_List_Table {
|
||||
* @return boolean TRUE when the blank state should be rendered, FALSE otherwise.
|
||||
*/
|
||||
private function should_render_blank_state(): bool {
|
||||
/**
|
||||
* Whether we should render a blank state so that custom count queries can be used.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param null $should_render_blank_state `null` will use the built-in counts. Sending a boolean will short-circuit that path.
|
||||
* @param object ListTable The current instance of the class.
|
||||
*/
|
||||
$should_render_blank_state = apply_filters(
|
||||
'woocommerce_' . $this->order_type . '_list_table_should_render_blank_state',
|
||||
null,
|
||||
$this
|
||||
);
|
||||
|
||||
if ( is_bool( $should_render_blank_state ) ) {
|
||||
return $should_render_blank_state;
|
||||
}
|
||||
|
||||
return ( ! $this->has_filter ) && 0 === $this->count_orders_by_status( array_keys( $this->get_visible_statuses() ) );
|
||||
}
|
||||
|
||||
@@ -719,11 +756,22 @@ class ListTable extends WP_List_Table {
|
||||
private function months_filter() {
|
||||
// XXX: [review] we may prefer to move this logic outside of the ListTable class.
|
||||
|
||||
/**
|
||||
* Filters whether to remove the 'Months' drop-down from the order list table.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param bool $disable Whether to disable the drop-down. Default false.
|
||||
*/
|
||||
if ( apply_filters( 'woocommerce_' . $this->order_type . '_list_table_disable_months_filter', false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp_locale;
|
||||
global $wpdb;
|
||||
|
||||
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
|
||||
$utc_offset = wc_timezone_offset();
|
||||
$utc_offset = wc_timezone_offset();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$order_dates = $wpdb->get_results(
|
||||
@@ -1347,7 +1395,7 @@ class ListTable extends WP_List_Table {
|
||||
* @return int Number of orders that were trashed.
|
||||
*/
|
||||
private function do_delete( array $ids, bool $force_delete = false ): int {
|
||||
$changed = 0;
|
||||
$changed = 0;
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
@@ -1537,4 +1585,55 @@ class ListTable extends WP_List_Table {
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the search box with various options to limit order search results.
|
||||
*
|
||||
* @param string $text The search button text.
|
||||
* @param string $input_id The search input ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function search_box( $text, $input_id ) {
|
||||
if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$input_id = $input_id . '-search-input';
|
||||
|
||||
if ( ! empty( $_REQUEST['orderby'] ) ) {
|
||||
echo '<input type="hidden" name="orderby" value="' . esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) ) . '" />';
|
||||
}
|
||||
if ( ! empty( $_REQUEST['order'] ) ) {
|
||||
echo '<input type="hidden" name="order" value="' . esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) . '" />';
|
||||
}
|
||||
?>
|
||||
<p class="search-box">
|
||||
<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $text ); ?>:</label>
|
||||
<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
|
||||
<?php $this->search_filter(); ?>
|
||||
<?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the search filter dropdown.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function search_filter() {
|
||||
$options = array(
|
||||
'order_id' => __( 'Order ID', 'woocommerce' ),
|
||||
'customer_email' => __( 'Customer Email', 'woocommerce' ),
|
||||
'customers' => __( 'Customers', 'woocommerce' ),
|
||||
'products' => __( 'Products', 'woocommerce' ),
|
||||
'all' => __( 'All', 'woocommerce' ),
|
||||
);
|
||||
?>
|
||||
<select name="search-filter" id="order-search-filter">
|
||||
<?php foreach ( $options as $value => $label ) { ?>
|
||||
<option value="<?php echo esc_attr( wp_unslash( sanitize_text_field( $value ) ) ); ?>" <?php selected( $value, sanitize_text_field( wp_unslash( $_REQUEST['search-filter'] ?? 'all' ) ) ); ?>><?php echo esc_html( $label ); ?></option>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Customers\Query as CustomersQuery;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
@@ -12,8 +12,6 @@ use WC_Order;
|
||||
*/
|
||||
class CustomerHistory {
|
||||
|
||||
use OrderAttributionMeta;
|
||||
|
||||
/**
|
||||
* Output the customer history template for the order.
|
||||
*
|
||||
@@ -22,34 +20,47 @@ class CustomerHistory {
|
||||
* @return void
|
||||
*/
|
||||
public function output( WC_Order $order ): void {
|
||||
$this->display_customer_history( $order->get_customer_id(), $order->get_billing_email() );
|
||||
}
|
||||
// No history when adding a new order.
|
||||
if ( 'auto-draft' === $order->get_status() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
$customer_history = null;
|
||||
|
||||
if ( method_exists( $order, 'get_report_customer_id' ) ) {
|
||||
$customer_history = $this->get_customer_history( $order->get_report_customer_id() );
|
||||
}
|
||||
|
||||
if ( ! $customer_history ) {
|
||||
$customer_history = array(
|
||||
'orders_count' => 0,
|
||||
'total_spend' => 0,
|
||||
'avg_order_value' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$args['has_customer_id'] = $has_customer_id;
|
||||
wc_get_template( 'order/customer-history.php', $args );
|
||||
wc_get_template( 'order/customer-history.php', $customer_history );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order history for the customer (data matches Customers report).
|
||||
*
|
||||
* @param int $customer_report_id The reports customer ID (not necessarily User ID).
|
||||
*
|
||||
* @return array|null Order count, total spend, and average spend per order.
|
||||
*/
|
||||
private function get_customer_history( $customer_report_id ): ?array {
|
||||
|
||||
$args = array(
|
||||
'customers' => array( $customer_report_id ),
|
||||
// If unset, these params have default values that affect the results.
|
||||
'order_after' => null,
|
||||
'order_before' => null,
|
||||
);
|
||||
|
||||
$customers_query = new CustomersQuery( $args );
|
||||
$customer_data = $customers_query->get_data();
|
||||
return $customer_data->data[0] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,6 @@ class OrderAttribution {
|
||||
// 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 );
|
||||
wc_get_template( 'order/attribution-details.php', $template_data );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ abstract class Component {
|
||||
*/
|
||||
public static function get_argument_from_path( $arguments, $path, $delimiter = '.' ) {
|
||||
$path_keys = explode( $delimiter, $path );
|
||||
$num_keys = count( $path_keys );
|
||||
$num_keys = false !== $path_keys ? count( $path_keys ) : 0;
|
||||
|
||||
$val = $arguments;
|
||||
for ( $i = 0; $i < $num_keys; $i++ ) {
|
||||
|
||||
@@ -64,7 +64,25 @@ class MailchimpScheduler {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = $this->make_request( $profile_data['store_email'] );
|
||||
$country_code = WC()->countries->get_base_country();
|
||||
$country_name = WC()->countries->countries[ $country_code ] ?? 'N/A';
|
||||
|
||||
$state = WC()->countries->get_base_state();
|
||||
$state_name = WC()->countries->states[ $country_code ][ $state ] ?? 'N/A';
|
||||
|
||||
$address = array(
|
||||
// Setting N/A for addr1, city, state, zipcode and country as they are
|
||||
// required fields. Setting '' doesn't work.
|
||||
'addr1' => 'N/A',
|
||||
'addr2' => '',
|
||||
'city' => 'N/A',
|
||||
'state' => $state_name,
|
||||
'zip' => 'N/A',
|
||||
'country' => $country_name,
|
||||
);
|
||||
|
||||
$response = $this->make_request( $profile_data['store_email'], $address );
|
||||
|
||||
if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
|
||||
$this->handle_request_error();
|
||||
return false;
|
||||
@@ -85,10 +103,11 @@ class MailchimpScheduler {
|
||||
*
|
||||
* @internal
|
||||
* @param string $store_email Email address to subscribe.
|
||||
* @param array $address Store address.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function make_request( $store_email ) {
|
||||
public function make_request( $store_email, $address ) {
|
||||
if ( true === defined( 'WP_ENVIRONMENT_TYPE' ) && 'development' === constant( 'WP_ENVIRONMENT_TYPE' ) ) {
|
||||
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT_DEV;
|
||||
} else {
|
||||
@@ -101,7 +120,8 @@ class MailchimpScheduler {
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
'method' => 'POST',
|
||||
'body' => array(
|
||||
'email' => $store_email,
|
||||
'email' => $store_email,
|
||||
'address' => $address,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -240,7 +240,19 @@ class Settings {
|
||||
|
||||
$settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible();
|
||||
|
||||
$settings['gutenberg_version'] = defined( 'GUTENBERG_VERSION' ) ? constant( 'GUTENBERG_VERSION' ) : 0;
|
||||
$has_gutenberg = is_plugin_active( 'gutenberg/gutenberg.php' );
|
||||
$gutenberg_version = '';
|
||||
if ( $has_gutenberg ) {
|
||||
if ( defined( 'GUTENBERG_VERSION' ) ) {
|
||||
$gutenberg_version = GUTENBERG_VERSION;
|
||||
}
|
||||
|
||||
if ( ! $gutenberg_version ) {
|
||||
$gutenberg_data = get_plugin_data( WP_PLUGIN_DIR . '/gutenberg/gutenberg.php' );
|
||||
$gutenberg_version = $gutenberg_data['Version'];
|
||||
}
|
||||
}
|
||||
$settings['gutenberg_version'] = $has_gutenberg ? $gutenberg_version : 0;
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,34 @@ class WCPaymentGatewayPreInstallWCPayPromotion extends \WC_Payment_Gateway {
|
||||
$this->method_description = $wc_pay_spec->content;
|
||||
$this->has_fields = false;
|
||||
|
||||
// Set the promotion pseudo-gateway support features.
|
||||
// If the promotion spec provides the supports property, use it.
|
||||
if ( property_exists( $wc_pay_spec, 'supports' ) ) {
|
||||
$this->supports = $wc_pay_spec->supports;
|
||||
} else {
|
||||
// Otherwise, use the default supported features in line with WooPayments ones.
|
||||
// We include all features here, even if some of them are behind settings, since this is for info only.
|
||||
$this->supports = array(
|
||||
// Regular features.
|
||||
'products',
|
||||
'refunds',
|
||||
// Subscriptions features.
|
||||
'subscriptions',
|
||||
'multiple_subscriptions',
|
||||
'subscription_cancellation',
|
||||
'subscription_reactivation',
|
||||
'subscription_suspension',
|
||||
'subscription_amount_changes',
|
||||
'subscription_date_changes',
|
||||
'subscription_payment_method_change_admin',
|
||||
'subscription_payment_method_change_customer',
|
||||
'subscription_payment_method_change',
|
||||
// Saved cards features.
|
||||
'tokenization',
|
||||
'add_payment_method',
|
||||
);
|
||||
}
|
||||
|
||||
// Get setting values.
|
||||
$this->enabled = false;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
use ActionScheduler;
|
||||
use WC_Admin_Settings;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -326,9 +327,16 @@ class CustomOrdersTableController {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
|
||||
if ( ! filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'hpos-sync-now' ) ) {
|
||||
WC_Admin_Settings::add_error( esc_html__( 'Unable to start synchronization. The link you followed may have expired.', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -506,11 +514,14 @@ class CustomOrdersTableController {
|
||||
$orders_pending_sync_count
|
||||
);
|
||||
} elseif ( $sync_is_pending ) {
|
||||
$sync_now_url = add_query_arg(
|
||||
array(
|
||||
self::SYNC_QUERY_ARG => true,
|
||||
$sync_now_url = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
self::SYNC_QUERY_ARG => true,
|
||||
),
|
||||
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
|
||||
),
|
||||
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
|
||||
'hpos-sync-now'
|
||||
);
|
||||
|
||||
if ( ! $is_dangerous ) {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Caches\OrderCache;
|
||||
use Automattic\WooCommerce\Caches\OrderCacheController;
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\{ BatchProcessingController, BatchProcessorInterface };
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
@@ -335,6 +335,33 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
return $interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys that can be ignored during synchronization or verification.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ignored_order_props() {
|
||||
/**
|
||||
* Allows modifying the list of order properties that are ignored during HPOS synchronization or verification.
|
||||
*
|
||||
* @param string[] List of order properties or meta keys.
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$ignored_props = apply_filters( 'woocommerce_hpos_sync_ignored_order_props', array() );
|
||||
$ignored_props = array_filter( array_map( 'trim', array_filter( $ignored_props, 'is_string' ) ) );
|
||||
|
||||
return array_merge(
|
||||
$ignored_props,
|
||||
array(
|
||||
'_paid_date', // This has been deprecated and replaced by '_date_paid' in the CPT datastore.
|
||||
'_completed_date', // This has been deprecated and replaced by '_date_completed' in the CPT datastore.
|
||||
EditLock::META_KEY_NAME,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an event to run background sync when the mode is set to interval.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
@@ -171,13 +173,11 @@ class LegacyDataHandler {
|
||||
/**
|
||||
* Checks whether an HPOS-backed order is newer than the corresponding post.
|
||||
*
|
||||
* @param int|\WC_Order $order An HPOS order.
|
||||
* @param \WC_Abstract_Order $order An HPOS order.
|
||||
* @return bool TRUE if the order is up to date with the corresponding post.
|
||||
* @throws \Exception When the order is not an HPOS order.
|
||||
*/
|
||||
private function is_order_newer_than_post( $order ): bool {
|
||||
$order = is_a( $order, 'WC_Order' ) ? $order : wc_get_order( absint( $order ) );
|
||||
|
||||
private function is_order_newer_than_post( \WC_Abstract_Order $order ): bool {
|
||||
if ( ! is_a( $order->get_data_store()->get_current_class_name(), OrdersTableDataStore::class, true ) ) {
|
||||
throw new \Exception( __( 'Order is not an HPOS order.', 'woocommerce' ) );
|
||||
}
|
||||
@@ -195,6 +195,137 @@ class LegacyDataHandler {
|
||||
return $order_modified_gmt >= $post_modified_gmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array with properties and metadata for which HPOS and post record have different values.
|
||||
* Given it's mostly informative nature, it doesn't perform any deep or recursive searches and operates only on top-level properties/metadata.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return array Array of [HPOS value, post value] keyed by property, for all properties where HPOS and post value differ.
|
||||
*/
|
||||
public function get_diff_for_order( int $order_id ): array {
|
||||
$diff = array();
|
||||
|
||||
$hpos_order = $this->get_order_from_datastore( $order_id, 'hpos' );
|
||||
$cpt_order = $this->get_order_from_datastore( $order_id, 'cpt' );
|
||||
|
||||
if ( $hpos_order->get_type() !== $cpt_order->get_type() ) {
|
||||
$diff['type'] = array( $hpos_order->get_type(), $cpt_order->get_type() );
|
||||
}
|
||||
|
||||
$hpos_meta = $this->order_meta_to_array( $hpos_order );
|
||||
$cpt_meta = $this->order_meta_to_array( $cpt_order );
|
||||
|
||||
// Consider only keys for which we actually have a corresponding HPOS column or are meta.
|
||||
$all_keys = array_unique(
|
||||
array_diff(
|
||||
array_merge(
|
||||
$this->get_order_base_props(),
|
||||
array_keys( $hpos_meta ),
|
||||
array_keys( $cpt_meta )
|
||||
),
|
||||
$this->data_synchronizer->get_ignored_order_props()
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $all_keys as $key ) {
|
||||
$val1 = in_array( $key, $this->get_order_base_props(), true ) ? $hpos_order->{"get_$key"}() : ( $hpos_meta[ $key ] ?? null );
|
||||
$val2 = in_array( $key, $this->get_order_base_props(), true ) ? $cpt_order->{"get_$key"}() : ( $cpt_meta[ $key ] ?? null );
|
||||
|
||||
// Workaround for https://github.com/woocommerce/woocommerce/issues/43126.
|
||||
if ( ! $val2 && in_array( $key, array( '_billing_address_index', '_shipping_address_index' ), true ) ) {
|
||||
$val2 = get_post_meta( $order_id, $key, true );
|
||||
}
|
||||
|
||||
if ( $val1 != $val2 ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$diff[ $key ] = array( $val1, $val2 );
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an order object as seen by either the HPOS or CPT datastores.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'cpt'. Defaults to 'hpos'.
|
||||
* @return \WC_Order Order instance.
|
||||
*/
|
||||
public function get_order_from_datastore( int $order_id, string $data_store_id = 'hpos' ) {
|
||||
$data_store = ( 'hpos' === $data_store_id ) ? $this->data_store : $this->data_store->get_cpt_data_store_instance();
|
||||
|
||||
wp_cache_delete( \WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' );
|
||||
|
||||
// Prime caches if we can.
|
||||
if ( method_exists( $data_store, 'prime_caches_for_orders' ) ) {
|
||||
$data_store->prime_caches_for_orders( array( $order_id ), array() );
|
||||
}
|
||||
|
||||
$classname = wc_get_order_type( $data_store->get_order_type( $order_id ) )['class_name'];
|
||||
$order = new $classname();
|
||||
$order->set_id( $order_id );
|
||||
|
||||
// Switch datastore if necessary.
|
||||
$update_data_store_func = function ( $data_store ) {
|
||||
// Each order object contains a reference to its data store, but this reference is itself
|
||||
// held inside of an instance of WC_Data_Store, so we create that first.
|
||||
$data_store_wrapper = \WC_Data_Store::load( 'order' );
|
||||
|
||||
// Bind $data_store to our WC_Data_Store.
|
||||
( function ( $data_store ) {
|
||||
$this->current_class_name = get_class( $data_store );
|
||||
$this->instance = $data_store;
|
||||
} )->call( $data_store_wrapper, $data_store );
|
||||
|
||||
// Finally, update the $order object with our WC_Data_Store( $data_store ) instance.
|
||||
$this->data_store = $data_store_wrapper;
|
||||
};
|
||||
$update_data_store_func->call( $order, $data_store );
|
||||
|
||||
// Read order.
|
||||
$data_store->read( $order );
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all metadata in an order object as an array.
|
||||
*
|
||||
* @param \WC_Order $order Order instance.
|
||||
* @return array Array of metadata grouped by meta key.
|
||||
*/
|
||||
private function order_meta_to_array( \WC_Order &$order ): array {
|
||||
$result = array();
|
||||
|
||||
foreach ( ArrayUtil::select( $order->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD ) as &$meta ) {
|
||||
if ( array_key_exists( $meta['key'], $result ) ) {
|
||||
$result[ $meta['key'] ] = array( $result[ $meta['key'] ] );
|
||||
$result[ $meta['key'] ][] = $meta['value'];
|
||||
} else {
|
||||
$result[ $meta['key'] ] = $meta['value'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns names of all order base properties supported by HPOS.
|
||||
*
|
||||
* @return string[] Property names.
|
||||
*/
|
||||
private function get_order_base_props(): array {
|
||||
return array_column(
|
||||
call_user_func_array(
|
||||
'array_merge',
|
||||
array_values( $this->data_store->get_all_order_column_mappings() )
|
||||
),
|
||||
'name'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -97,6 +97,15 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
'_new_order_email_sent',
|
||||
);
|
||||
|
||||
/**
|
||||
* Meta keys that are considered ephemereal and do not trigger a full save (updating modified date) when changed.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $ephemeral_meta_keys = array(
|
||||
EditLock::META_KEY_NAME,
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles custom metadata in the wc_orders_meta table.
|
||||
*
|
||||
@@ -1253,12 +1262,10 @@ WHERE
|
||||
$post_order_modified_date = is_null( $post_order_modified_date ) ? 0 : $post_order_modified_date->getTimestamp();
|
||||
|
||||
/**
|
||||
* We are here because there was difference in posts and order data, although the sync is enabled.
|
||||
* When order modified date is more recent than post modified date, it can only mean that COT definitely has more updated version of the order.
|
||||
*
|
||||
* In a case where post meta was updated (without updating post_modified date), post_modified would be equal to order_modified date.
|
||||
*
|
||||
* So we write back to the order table when order modified date is more recent than post modified date. Otherwise, we write to the post table.
|
||||
* We are here because there was difference in the post and order data even though sync is enabled. If the modified date in
|
||||
* the post is the same or more recent than the modified date in the order object, we update the order object with the data
|
||||
* from the post. The opposite case is handled in 'backfill_post_record'. This mitigates the case where other plugins write
|
||||
* to the post or postmeta directly.
|
||||
*/
|
||||
if ( $post_order_modified_date >= $order_modified_date ) {
|
||||
$this->migrate_post_record( $order, $post_order );
|
||||
@@ -1547,7 +1554,7 @@ WHERE
|
||||
*
|
||||
* @param array $ids List of order IDs.
|
||||
*
|
||||
* @return \stdClass[]|object|null DB Order objects or error.
|
||||
* @return \stdClass[] DB Order objects or error.
|
||||
*/
|
||||
protected function get_order_data_for_ids( $ids ) {
|
||||
global $wpdb;
|
||||
@@ -2978,9 +2985,7 @@ CREATE TABLE $meta_table (
|
||||
private function should_save_after_meta_change( $order, $meta = null ) {
|
||||
$current_time = $this->legacy_proxy->call_function( 'current_time', 'mysql', 1 );
|
||||
$current_date_time = new \WC_DateTime( $current_time, new \DateTimeZone( 'GMT' ) );
|
||||
$skip_for = array(
|
||||
EditLock::META_KEY_NAME,
|
||||
);
|
||||
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $skip_for, true ) );
|
||||
|
||||
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $this->ephemeral_meta_keys, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class OrdersTableFieldQuery {
|
||||
} else {
|
||||
$relation = $q['relation'];
|
||||
unset( $q['relation'] );
|
||||
|
||||
$chunks = array();
|
||||
foreach ( $q as $query ) {
|
||||
$chunks[] = $this->process( $query );
|
||||
}
|
||||
@@ -292,11 +292,10 @@ class OrdersTableFieldQuery {
|
||||
}
|
||||
|
||||
$clause_compare = $clause['compare'];
|
||||
|
||||
switch ( $clause_compare ) {
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( (array) $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'BETWEEN':
|
||||
case 'NOT BETWEEN':
|
||||
@@ -327,7 +326,7 @@ class OrdersTableFieldQuery {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $where ) {
|
||||
if ( ! empty( $where ) ) {
|
||||
if ( 'CHAR' === $clause['cast'] ) {
|
||||
return "`{$clause['alias']}`.`{$clause['column']}` {$clause_compare} {$where}";
|
||||
} else {
|
||||
|
||||
@@ -389,7 +389,7 @@ class OrdersTableMetaQuery {
|
||||
// Nested.
|
||||
$relation = $arg['relation'];
|
||||
unset( $arg['relation'] );
|
||||
|
||||
$chunks = array();
|
||||
foreach ( $arg as $index => &$clause ) {
|
||||
$chunks[] = $this->process( $clause, $arg );
|
||||
}
|
||||
@@ -519,6 +519,9 @@ class OrdersTableMetaQuery {
|
||||
|
||||
$alias = $clause['alias'];
|
||||
|
||||
$meta_compare_string_start = '';
|
||||
$meta_compare_string_end = '';
|
||||
$subquery_alias = '';
|
||||
if ( in_array( $clause['compare_key'], array( '!=', 'NOT IN', 'NOT LIKE', 'NOT EXISTS', 'NOT REGEXP' ), true ) ) {
|
||||
$i = count( $this->table_aliases );
|
||||
$subquery_alias = self::ALIAS_PREFIX . $i;
|
||||
@@ -541,7 +544,7 @@ class OrdersTableMetaQuery {
|
||||
$where = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
break;
|
||||
case 'IN':
|
||||
$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ')';
|
||||
$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( (array) $clause['key'] ) ), 1 ) . ')';
|
||||
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'RLIKE':
|
||||
@@ -566,7 +569,7 @@ class OrdersTableMetaQuery {
|
||||
$where = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'NOT IN':
|
||||
$array_subclause = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
|
||||
$array_subclause = '(' . substr( str_repeat( ',%s', count( (array) $clause['key'] ) ), 1 ) . ') ';
|
||||
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key IN " . $array_subclause . $meta_compare_string_end;
|
||||
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
@@ -618,7 +621,7 @@ class OrdersTableMetaQuery {
|
||||
switch ( $meta_compare ) {
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')', $meta_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( (array) $meta_value ) ), 1 ) . ')', $meta_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
|
||||
case 'BETWEEN':
|
||||
|
||||
@@ -346,6 +346,7 @@ class OrdersTableQuery {
|
||||
'day' => '',
|
||||
);
|
||||
|
||||
$precision = null;
|
||||
if ( is_numeric( $date ) ) {
|
||||
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( 'UTC' ) );
|
||||
$precision = 'second';
|
||||
@@ -919,6 +920,23 @@ class OrdersTableQuery {
|
||||
}
|
||||
$orders_table = $this->tables['orders'];
|
||||
$this->count_sql = "SELECT COUNT(DISTINCT $fields) FROM $orders_table $join WHERE $where";
|
||||
|
||||
if ( ! $this->suppress_filters ) {
|
||||
/**
|
||||
* Filters the count SQL query.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param string $sql The count SQL query.
|
||||
* @param OrdersTableQuery $query The OrdersTableQuery instance (passed by reference).
|
||||
* @param array $args Query args.
|
||||
* @param string $fields Prepared fields for SELECT clause.
|
||||
* @param string $join Prepared JOIN clause.
|
||||
* @param string $where Prepared WHERE clause.
|
||||
* @param string $groupby Prepared GROUP BY clause.
|
||||
*/
|
||||
$this->count_sql = apply_filters_ref_array( 'woocommerce_orders_table_query_count_sql', array( $this->count_sql, &$this, $this->args, $fields, $join, $where, $groupby ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1131,7 +1149,7 @@ class OrdersTableQuery {
|
||||
$values = is_array( $values ) ? $values : array( $values );
|
||||
$ids = array();
|
||||
$emails = array();
|
||||
|
||||
$pieces = array();
|
||||
foreach ( $values as $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
$sql = $this->generate_customer_query( $value, 'AND' );
|
||||
|
||||
@@ -24,6 +24,13 @@ class OrdersTableSearchQuery {
|
||||
*/
|
||||
private $search_term;
|
||||
|
||||
/**
|
||||
* Limits the search to a specific field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $search_filters;
|
||||
|
||||
/**
|
||||
* Creates the JOIN and WHERE clauses needed to execute a search of orders.
|
||||
*
|
||||
@@ -32,8 +39,31 @@ class OrdersTableSearchQuery {
|
||||
* @param OrdersTableQuery $query The order query object.
|
||||
*/
|
||||
public function __construct( OrdersTableQuery $query ) {
|
||||
$this->query = $query;
|
||||
$this->search_term = urldecode( $query->get( 's' ) );
|
||||
$this->query = $query;
|
||||
$this->search_term = urldecode( $query->get( 's' ) );
|
||||
$this->search_filters = $this->sanitize_search_filters( urldecode( $query->get( 'search_filter' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize search filter param.
|
||||
*
|
||||
* @param string $search_filter Search filter param.
|
||||
*
|
||||
* @return array Array of search filters.
|
||||
*/
|
||||
private function sanitize_search_filters( string $search_filter ) : array {
|
||||
$available_filters = array(
|
||||
'order_id',
|
||||
'customer_email',
|
||||
'customers', // customers also searches in meta.
|
||||
'products',
|
||||
);
|
||||
|
||||
if ( 'all' === $search_filter || '' === $search_filter ) {
|
||||
return $available_filters;
|
||||
} else {
|
||||
return array_intersect( $available_filters, array( $search_filter ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,12 +92,34 @@ class OrdersTableSearchQuery {
|
||||
* @return string
|
||||
*/
|
||||
private function generate_join(): string {
|
||||
$orders_table = $this->query->get_table_name( 'orders' );
|
||||
$items_table = $this->query->get_table_name( 'items' );
|
||||
$join = array();
|
||||
|
||||
return "
|
||||
LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
|
||||
";
|
||||
foreach ( $this->search_filters as $search_filter ) {
|
||||
$join[] = $this->generate_join_for_search_filter( $search_filter );
|
||||
}
|
||||
|
||||
return implode( ' ', $join );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JOIN clause for a given search filter.
|
||||
* Right now we only have the products filter that actually does a JOIN, but in the future we may add more -- for example, custom order fields, payment tokens, and so on. This function makes it easier to add more filters in the future.
|
||||
*
|
||||
* If a search filter needs a JOIN, it will also need a WHERE clause.
|
||||
*
|
||||
* @param string $search_filter Name of the search filter.
|
||||
*
|
||||
* @return string JOIN clause.
|
||||
*/
|
||||
private function generate_join_for_search_filter( $search_filter ) : string {
|
||||
if ( 'products' === $search_filter ) {
|
||||
$orders_table = $this->query->get_table_name( 'orders' );
|
||||
$items_table = $this->query->get_table_name( 'items' );
|
||||
return "
|
||||
LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
|
||||
";
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,27 +130,66 @@ class OrdersTableSearchQuery {
|
||||
* @return string
|
||||
*/
|
||||
private function generate_where(): string {
|
||||
global $wpdb;
|
||||
$where = '';
|
||||
$where = array();
|
||||
$possible_order_id = (string) absint( $this->search_term );
|
||||
$order_table = $this->query->get_table_name( 'orders' );
|
||||
|
||||
// Support the passing of an order ID as the search term.
|
||||
if ( (string) $this->query->get( 's' ) === $possible_order_id ) {
|
||||
$where = "`$order_table`.id = $possible_order_id OR ";
|
||||
$where[] = "`$order_table`.id = $possible_order_id";
|
||||
}
|
||||
|
||||
$meta_sub_query = $this->generate_where_for_meta_table();
|
||||
foreach ( $this->search_filters as $search_filter ) {
|
||||
$search_where = $this->generate_where_for_search_filter( $search_filter );
|
||||
if ( ! empty( $search_where ) ) {
|
||||
$where[] = $search_where;
|
||||
}
|
||||
}
|
||||
|
||||
$where .= $wpdb->prepare(
|
||||
"
|
||||
search_query_items.order_item_name LIKE %s
|
||||
OR `$order_table`.id IN ( $meta_sub_query )
|
||||
",
|
||||
'%' . $wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
$where_statement = implode( ' OR ', $where );
|
||||
|
||||
return " ( $where ) ";
|
||||
return " ( $where_statement ) ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates WHERE clause for a given search filter. Right now we only have the products and customers filters that actually use WHERE, but in the future we may add more -- for example, custom order fields, payment tokens and so on. This function makes it easier to add more filters in the future.
|
||||
*
|
||||
* @param string $search_filter Name of the search filter.
|
||||
*
|
||||
* @return string WHERE clause.
|
||||
*/
|
||||
private function generate_where_for_search_filter( string $search_filter ) : string {
|
||||
global $wpdb;
|
||||
|
||||
$order_table = $this->query->get_table_name( 'orders' );
|
||||
|
||||
if ( 'customer_email' === $search_filter ) {
|
||||
return $wpdb->prepare(
|
||||
"`$order_table`.billing_email LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
|
||||
$wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'order_id' === $search_filter && is_numeric( $this->search_term ) ) {
|
||||
return $wpdb->prepare(
|
||||
"`$order_table`.id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
|
||||
absint( $this->search_term )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'products' === $search_filter ) {
|
||||
return $wpdb->prepare(
|
||||
'search_query_items.order_item_name LIKE %s',
|
||||
'%' . $wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'customers' === $search_filter ) {
|
||||
$meta_sub_query = $this->generate_where_for_meta_table();
|
||||
return "`$order_table`.id IN ( $meta_sub_query ) ";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +205,12 @@ class OrdersTableSearchQuery {
|
||||
global $wpdb;
|
||||
$meta_table = $this->query->get_table_name( 'meta' );
|
||||
$meta_fields = $this->get_meta_fields_to_be_searched();
|
||||
|
||||
if ( '' === $meta_fields ) {
|
||||
return '-1';
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $meta_fields is already escaped before imploding, $meta_table is hardcoded.
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT search_query_meta.order_id
|
||||
@@ -124,6 +221,7 @@ GROUP BY search_query_meta.order_id
|
||||
",
|
||||
'%' . $wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +233,11 @@ GROUP BY search_query_meta.order_id
|
||||
* @return string
|
||||
*/
|
||||
private function get_meta_fields_to_be_searched(): string {
|
||||
$meta_fields_to_search = array(
|
||||
'_billing_address_index',
|
||||
'_shipping_address_index',
|
||||
);
|
||||
|
||||
/**
|
||||
* Controls the order meta keys to be included in search queries.
|
||||
*
|
||||
@@ -147,10 +250,7 @@ GROUP BY search_query_meta.order_id
|
||||
*/
|
||||
$meta_keys = apply_filters(
|
||||
'woocommerce_order_table_search_query_meta_keys',
|
||||
array(
|
||||
'_billing_address_index',
|
||||
'_shipping_address_index',
|
||||
)
|
||||
$meta_fields_to_search
|
||||
);
|
||||
|
||||
$meta_keys = (array) array_map(
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* BlockTemplatesServiceProvider class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\TemplateTransformer;
|
||||
|
||||
/**
|
||||
* Service provider for the block templates controller classes in the Automattic\WooCommerce\Internal\BlockTemplateRegistry namespace.
|
||||
*/
|
||||
class BlockTemplatesServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
BlockTemplateRegistry::class,
|
||||
BlockTemplatesController::class,
|
||||
TemplateTransformer::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( TemplateTransformer::class );
|
||||
$this->share( BlockTemplateRegistry::class );
|
||||
$this->share( BlockTemplatesController::class )->addArguments(
|
||||
array(
|
||||
BlockTemplateRegistry::class,
|
||||
TemplateTransformer::class,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* UtilsClassesServiceProvider class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
|
||||
|
||||
/**
|
||||
* Service provider for the engine classes in the Automattic\WooCommerce\src namespace.
|
||||
*/
|
||||
class EnginesServiceProvider extends AbstractInterfaceServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
TransientFilesEngine::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share_with_implements_tags( TransientFilesEngine::class )->addArgument( LegacyProxy::class );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\LayoutTemplates\LayoutTemplateRegistry;
|
||||
|
||||
/**
|
||||
* Service provider for layout templates.
|
||||
*/
|
||||
class LayoutTemplatesServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
LayoutTemplateRegistry::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( LayoutTemplateRegistry::class );
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\PageController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ FileController, ListTable };
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\{ PageController, Settings };
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
|
||||
/**
|
||||
@@ -18,6 +18,7 @@ class LoggingServiceProvider extends AbstractServiceProvider {
|
||||
protected $provides = array(
|
||||
FileController::class,
|
||||
PageController::class,
|
||||
Settings::class,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,10 @@ class LoggingServiceProvider extends AbstractServiceProvider {
|
||||
$this->share( PageController::class )->addArguments(
|
||||
array(
|
||||
FileController::class,
|
||||
Settings::class,
|
||||
)
|
||||
);
|
||||
|
||||
$this->share( Settings::class );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,13 @@ class OrderAttributionServiceProvider extends AbstractInterfaceServiceProvider {
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share_with_implements_tags( WPConsentAPI::class );
|
||||
$this->share_with_implements_tags( OrderAttributionController::class )
|
||||
->addArguments(
|
||||
array(
|
||||
LegacyProxy::class,
|
||||
FeaturesController::class,
|
||||
WPConsentAPI::class,
|
||||
)
|
||||
);
|
||||
$this->share_with_implements_tags( OrderAttributionBlocksController::class )
|
||||
@@ -48,6 +50,5 @@ class OrderAttributionServiceProvider extends AbstractInterfaceServiceProvider {
|
||||
OrderAttributionController::class,
|
||||
)
|
||||
);
|
||||
$this->share_with_implements_tags( WPConsentAPI::class );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use Automattic\WooCommerce\Utilities\TimeUtil;
|
||||
|
||||
/**
|
||||
* Service provider for the non-static utils classes in the Automattic\WooCommerce\src namespace.
|
||||
@@ -33,6 +34,7 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
|
||||
PluginUtil::class,
|
||||
COTMigrationUtil::class,
|
||||
WebhookUtil::class,
|
||||
TimeUtil::class,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -47,5 +49,6 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
|
||||
$this->share( COTMigrationUtil::class )
|
||||
->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
|
||||
$this->share( WebhookUtil::class );
|
||||
$this->share( TimeUtil::class );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
|
||||
|
||||
/**
|
||||
* Simple Product Template.
|
||||
* Product Variation Template.
|
||||
*/
|
||||
class ProductVariationTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
||||
const SINGLE_VARIATION_NOTICE_DISMISSED_OPTION = 'woocommerce_single_variation_notice_dismissed';
|
||||
|
||||
/**
|
||||
* SimpleProductTemplate constructor.
|
||||
* ProductVariationTemplate constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->add_group_blocks();
|
||||
|
||||
@@ -16,12 +16,13 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
* The context name used to identify the editor.
|
||||
*/
|
||||
const GROUP_IDS = array(
|
||||
'GENERAL' => 'general',
|
||||
'ORGANIZATION' => 'organization',
|
||||
'PRICING' => 'pricing',
|
||||
'INVENTORY' => 'inventory',
|
||||
'SHIPPING' => 'shipping',
|
||||
'VARIATIONS' => 'variations',
|
||||
'GENERAL' => 'general',
|
||||
'ORGANIZATION' => 'organization',
|
||||
'PRICING' => 'pricing',
|
||||
'INVENTORY' => 'inventory',
|
||||
'SHIPPING' => 'shipping',
|
||||
'VARIATIONS' => 'variations',
|
||||
'LINKED_PRODUCTS' => 'linked-products',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
$this->add_inventory_group_blocks();
|
||||
$this->add_shipping_group_blocks();
|
||||
$this->add_variation_group_blocks();
|
||||
$this->add_linked_products_group_blocks();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,10 +73,37 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Variations tab.
|
||||
if ( Features::is_enabled( 'product-variation-management' ) ) {
|
||||
$variations_hide_conditions = array();
|
||||
if ( Features::is_enabled( 'product-grouped' ) ) {
|
||||
$variations_hide_conditions[] = array(
|
||||
'expression' => 'editedProduct.type === "grouped"',
|
||||
);
|
||||
}
|
||||
if ( Features::is_enabled( 'product-external-affiliate' ) ) {
|
||||
$variations_hide_conditions[] = array(
|
||||
'expression' => 'editedProduct.type === "external"',
|
||||
);
|
||||
}
|
||||
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['VARIATIONS'],
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Variations', 'woocommerce' ),
|
||||
),
|
||||
'hideConditions' => $variations_hide_conditions,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['ORGANIZATION'],
|
||||
'order' => 15,
|
||||
'order' => 30,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Organization', 'woocommerce' ),
|
||||
),
|
||||
@@ -83,7 +112,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['PRICING'],
|
||||
'order' => 20,
|
||||
'order' => 40,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
),
|
||||
@@ -97,7 +126,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['INVENTORY'],
|
||||
'order' => 30,
|
||||
'order' => 50,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
),
|
||||
@@ -118,34 +147,23 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['SHIPPING'],
|
||||
'order' => 40,
|
||||
'order' => 60,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Shipping', 'woocommerce' ),
|
||||
),
|
||||
'hideConditions' => $shipping_hide_conditions,
|
||||
)
|
||||
);
|
||||
if ( Features::is_enabled( 'product-variation-management' ) ) {
|
||||
$variations_hide_conditions = array();
|
||||
if ( Features::is_enabled( 'product-grouped' ) ) {
|
||||
$variations_hide_conditions[] = array(
|
||||
'expression' => 'editedProduct.type === "grouped"',
|
||||
);
|
||||
}
|
||||
if ( Features::is_enabled( 'product-external-affiliate' ) ) {
|
||||
$variations_hide_conditions[] = array(
|
||||
'expression' => 'editedProduct.type === "external"',
|
||||
);
|
||||
}
|
||||
|
||||
// Linked Products tab.
|
||||
if ( Features::is_enabled( 'product-linked' ) ) {
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['VARIATIONS'],
|
||||
'order' => 50,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Variations', 'woocommerce' ),
|
||||
'id' => $this::GROUP_IDS['LINKED_PRODUCTS'],
|
||||
'order' => 70,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Linked products', 'woocommerce' ),
|
||||
),
|
||||
'hideConditions' => $variations_hide_conditions,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -179,6 +197,13 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
),
|
||||
)
|
||||
);
|
||||
$basic_details->add_block(
|
||||
array(
|
||||
'id' => 'product-details-section-description',
|
||||
'blockName' => 'woocommerce/product-details-section-description',
|
||||
'order' => 10,
|
||||
)
|
||||
);
|
||||
$basic_details->add_block(
|
||||
array(
|
||||
'id' => 'product-name',
|
||||
@@ -332,7 +357,6 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
'value' => 'url',
|
||||
'message' => __( 'Link to the external product is an invalid URL.', 'woocommerce' ),
|
||||
),
|
||||
'required' => __( 'Link to the external product is required.', 'woocommerce' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
@@ -431,10 +455,37 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
);
|
||||
// Downloads section.
|
||||
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
|
||||
$general_group->add_section(
|
||||
$product_downloads_section_group = $general_group->add_section(
|
||||
array(
|
||||
'id' => 'product-downloads-section-group',
|
||||
'order' => 50,
|
||||
'attributes' => array(
|
||||
'blockGap' => 'unit-40',
|
||||
),
|
||||
'hideConditions' => array(
|
||||
array(
|
||||
'expression' => 'editedProduct.type !== "simple"',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$product_downloads_section_group->add_block(
|
||||
array(
|
||||
'id' => 'product-downloadable',
|
||||
'blockName' => 'woocommerce/product-checkbox-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'property' => 'downloadable',
|
||||
'label' => __( 'Include downloads', 'woocommerce' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$product_downloads_section_group->add_section(
|
||||
array(
|
||||
'id' => 'product-downloads-section',
|
||||
'order' => 50,
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Downloads', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
@@ -446,7 +497,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
),
|
||||
'hideConditions' => array(
|
||||
array(
|
||||
'expression' => 'editedProduct.type !== "simple"',
|
||||
'expression' => 'editedProduct.downloadable !== true',
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1078,4 +1129,85 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the linked products group blocks to the template.
|
||||
*/
|
||||
private function add_linked_products_group_blocks() {
|
||||
$linked_products_group = $this->get_group_by_id( $this::GROUP_IDS['LINKED_PRODUCTS'] );
|
||||
if ( ! isset( $linked_products_group ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$linked_products_group->add_section(
|
||||
array(
|
||||
'id' => 'product-linked-upsells-section',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Upsells', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Learn more about linked products. %2$s: Learn more about linked products.*/
|
||||
__( 'Upsells are typically products that are extra profitable or better quality or more expensive. Experiment with combinations to boost sales. %1$sLearn more about linked products.%2$s', 'woocommerce' ),
|
||||
'<br /><a href="https://woo.com/document/related-products-up-sells-and-cross-sells/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
)
|
||||
)->add_block(
|
||||
array(
|
||||
'id' => 'product-linked-upsells',
|
||||
'blockName' => 'woocommerce/product-linked-list-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'property' => 'upsell_ids',
|
||||
'emptyState' => array(
|
||||
'image' => 'ShoppingBags',
|
||||
'tip' => __(
|
||||
'Tip: Upsells are products that are extra profitable or better quality or more expensive. Experiment with combinations to boost sales.',
|
||||
'woocommerce'
|
||||
),
|
||||
'isDismissible' => true,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$linked_products_group->add_section(
|
||||
array(
|
||||
'id' => 'product-linked-cross-sells-section',
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Cross-sells', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Learn more about linked products. %2$s: Learn more about linked products.*/
|
||||
__( 'By suggesting complementary products in the cart using cross-sells, you can significantly increase the average order value. %1$sLearn more about linked products.%2$s', 'woocommerce' ),
|
||||
'<br /><a href="https://woo.com/document/related-products-up-sells-and-cross-sells/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
'hideConditions' => array(
|
||||
array(
|
||||
'expression' => 'editedProduct.type === "external" || editedProduct.type === "grouped"',
|
||||
),
|
||||
),
|
||||
)
|
||||
)->add_block(
|
||||
array(
|
||||
'id' => 'product-linked-cross-sells',
|
||||
'blockName' => 'woocommerce/product-linked-list-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'property' => 'cross_sell_ids',
|
||||
'emptyState' => array(
|
||||
'image' => 'CashRegister',
|
||||
'tip' => __(
|
||||
'Tip: By suggesting complementary products in the cart using cross-sells, you can significantly increase the average order value.',
|
||||
'woocommerce'
|
||||
),
|
||||
'isDismissible' => true,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,14 @@ class WPConsentAPI {
|
||||
|
||||
use ScriptDebug;
|
||||
|
||||
|
||||
/**
|
||||
* Identifier of the consent category used for order attribution.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $consent_category = 'marketing';
|
||||
|
||||
/**
|
||||
* Register the consent API.
|
||||
*
|
||||
@@ -59,7 +67,7 @@ class WPConsentAPI {
|
||||
add_filter(
|
||||
'wc_order_attribution_allow_tracking',
|
||||
function() {
|
||||
return function_exists( 'wp_has_consent' ) && wp_has_consent( 'marketing' );
|
||||
return function_exists( 'wp_has_consent' ) && wp_has_consent( self::$consent_category );
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -80,14 +88,25 @@ class WPConsentAPI {
|
||||
*/
|
||||
private function enqueue_consent_api_scripts() {
|
||||
wp_enqueue_script(
|
||||
'wp-consent-api-integration-js',
|
||||
'wp-consent-api-integration',
|
||||
plugins_url(
|
||||
"assets/js/frontend/wp-consent-api-integration{$this->get_script_suffix()}.js",
|
||||
WC_PLUGIN_FILE
|
||||
),
|
||||
array( 'jquery', 'wp-consent-api' ),
|
||||
array( 'wp-consent-api', 'wc-order-attribution' ),
|
||||
Constants::get_constant( 'WC_VERSION' ),
|
||||
true
|
||||
);
|
||||
|
||||
// Add data for the script above. `wp_enqueue_script` API does not allow data attributes,
|
||||
// so we need a separate script tag and pollute the global scope.
|
||||
wp_add_inline_script(
|
||||
'wp-consent-api-integration',
|
||||
sprintf(
|
||||
'window.wc_order_attribution.params.consentCategory = %s;',
|
||||
wp_json_encode( self::$consent_category )
|
||||
),
|
||||
'before'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,50 +72,6 @@ class OrderAttributionBlocksController implements RegisterHooksInterface {
|
||||
}
|
||||
|
||||
$this->extend_api();
|
||||
|
||||
// Bail early on admin requests to avoid asset registration.
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'init',
|
||||
function() {
|
||||
$this->register_assets();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'wp_enqueue_scripts',
|
||||
function() {
|
||||
$this->enqueue_scripts();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register scripts.
|
||||
*/
|
||||
private function register_assets() {
|
||||
wp_register_script(
|
||||
'wc-order-attribution-blocks',
|
||||
plugins_url(
|
||||
"assets/js/frontend/order-attribution-blocks{$this->get_script_suffix()}.js",
|
||||
WC_PLUGIN_FILE
|
||||
),
|
||||
array( 'wc-order-attribution', 'wp-data', 'wc-blocks-checkout' ),
|
||||
Constants::get_constant( 'WC_VERSION' ),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the Order Attribution script.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function enqueue_scripts() {
|
||||
wp_enqueue_script( 'wc-order-attribution-blocks' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,8 +120,8 @@ class OrderAttributionBlocksController implements RegisterHooksInterface {
|
||||
*/
|
||||
private function get_schema_callback() {
|
||||
return function() {
|
||||
$schema = array();
|
||||
$fields = $this->order_attribution_controller->get_fields();
|
||||
$schema = array();
|
||||
$field_names = $this->order_attribution_controller->get_field_names();
|
||||
|
||||
$validate_callback = function( $value ) {
|
||||
if ( ! is_string( $value ) && null !== $value ) {
|
||||
@@ -186,12 +142,12 @@ class OrderAttributionBlocksController implements RegisterHooksInterface {
|
||||
return sanitize_text_field( $value );
|
||||
};
|
||||
|
||||
foreach ( $fields as $field ) {
|
||||
$schema[ $field ] = array(
|
||||
foreach ( $field_names as $field_name ) {
|
||||
$schema[ $field_name ] = array(
|
||||
'description' => sprintf(
|
||||
/* translators: %s is the field name */
|
||||
__( 'Order attribution field: %s', 'woocommerce' ),
|
||||
esc_html( $field )
|
||||
esc_html( $field_name )
|
||||
),
|
||||
'type' => array( 'string', 'null' ),
|
||||
'context' => array(),
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\Orders;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Integrations\WPConsentAPI;
|
||||
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
|
||||
use Automattic\WooCommerce\Internal\Traits\ScriptDebug;
|
||||
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
|
||||
@@ -26,9 +27,16 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
|
||||
use ScriptDebug;
|
||||
use OrderAttributionMeta {
|
||||
get_prefixed_field as public;
|
||||
get_prefixed_field_name as public;
|
||||
}
|
||||
|
||||
/**
|
||||
* The WPConsentAPI integration instance.
|
||||
*
|
||||
* @var WPConsentAPI
|
||||
*/
|
||||
private $consent;
|
||||
|
||||
/**
|
||||
* The FeatureController instance.
|
||||
*
|
||||
@@ -59,11 +67,13 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
*
|
||||
* @param LegacyProxy $proxy The legacy proxy.
|
||||
* @param FeaturesController $controller The feature controller.
|
||||
* @param WPConsentAPI $consent The WPConsentAPI integration.
|
||||
* @param WC_Logger_Interface $logger The logger object. If not provided, it will be obtained from the proxy.
|
||||
*/
|
||||
final public function init( LegacyProxy $proxy, FeaturesController $controller, ?WC_Logger_Interface $logger = null ) {
|
||||
final public function init( LegacyProxy $proxy, FeaturesController $controller, WPConsentAPI $consent, ?WC_Logger_Interface $logger = null ) {
|
||||
$this->proxy = $proxy;
|
||||
$this->feature_controller = $controller;
|
||||
$this->consent = $consent;
|
||||
$this->logger = $logger ?? $proxy->call_function( 'wc_get_logger' );
|
||||
$this->set_fields_and_prefix();
|
||||
}
|
||||
@@ -84,6 +94,9 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register WPConsentAPI integration.
|
||||
$this->consent->register();
|
||||
|
||||
add_action(
|
||||
'wp_enqueue_scripts',
|
||||
function() {
|
||||
@@ -98,13 +111,13 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
}
|
||||
);
|
||||
|
||||
// Include our hidden fields on order notes and registration form.
|
||||
$source_form_fields = function() {
|
||||
$this->source_form_fields();
|
||||
// Include our hidden `<input>` elements on order notes and registration form.
|
||||
$source_form_elements = function() {
|
||||
$this->source_form_elements();
|
||||
};
|
||||
|
||||
add_action( 'woocommerce_after_order_notes', $source_form_fields );
|
||||
add_action( 'woocommerce_register_form', $source_form_fields );
|
||||
add_action( 'woocommerce_after_order_notes', $source_form_elements );
|
||||
add_action( 'woocommerce_register_form', $source_form_elements );
|
||||
|
||||
// Update order based on submitted fields.
|
||||
add_action(
|
||||
@@ -112,7 +125,7 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
function( $order ) {
|
||||
// Nonce check is handled by WooCommerce before woocommerce_checkout_order_created hook.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
$params = $this->get_unprefixed_fields( $_POST );
|
||||
$params = $this->get_unprefixed_field_values( $_POST );
|
||||
/**
|
||||
* Run an action to save order attribution data.
|
||||
*
|
||||
@@ -175,18 +188,18 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
*/
|
||||
private function maybe_set_admin_source( WC_Order $order ) {
|
||||
if ( function_exists( 'is_admin' ) && is_admin() ) {
|
||||
$order->add_meta_data( $this->get_meta_prefixed_field( 'type' ), 'admin' );
|
||||
$order->add_meta_data( $this->get_meta_prefixed_field_name( 'source_type' ), 'admin' );
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the fields.
|
||||
* Get all of the field names.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_fields(): array {
|
||||
return $this->fields;
|
||||
public function get_field_names(): array {
|
||||
return $this->field_names;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,6 +226,9 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
wp_enqueue_script(
|
||||
'wc-order-attribution',
|
||||
plugins_url( "assets/js/frontend/order-attribution{$this->get_script_suffix()}.js", WC_PLUGIN_FILE ),
|
||||
// Technically we do depend on 'wp-data', 'wc-blocks-checkout' for blocks checkout,
|
||||
// but as implementing conditional dependency on the server-side would be too complex,
|
||||
// we resolve this condition at the client-side.
|
||||
array( 'sourcebuster-js' ),
|
||||
Constants::get_constant( 'WC_VERSION' ),
|
||||
true
|
||||
@@ -254,8 +270,9 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
'session' => $session_length,
|
||||
'ajaxurl' => admin_url( 'admin-ajax.php' ),
|
||||
'prefix' => $this->field_prefix,
|
||||
'allowTracking' => $allow_tracking,
|
||||
'allowTracking' => 'yes' === $allow_tracking,
|
||||
),
|
||||
'fields' => $this->fields,
|
||||
);
|
||||
|
||||
wp_localize_script( 'wc-order-attribution', 'wc_order_attribution', $namespace );
|
||||
@@ -308,8 +325,8 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
* @return void
|
||||
*/
|
||||
private function output_origin_column( WC_Order $order ) {
|
||||
$source_type = $order->get_meta( $this->get_meta_prefixed_field( 'type' ) );
|
||||
$source = $order->get_meta( $this->get_meta_prefixed_field( 'utm_source' ) );
|
||||
$source_type = $order->get_meta( $this->get_meta_prefixed_field_name( 'source_type' ) );
|
||||
$source = $order->get_meta( $this->get_meta_prefixed_field_name( 'utm_source' ) );
|
||||
$origin = $this->get_origin_label( $source_type, $source );
|
||||
if ( empty( $origin ) ) {
|
||||
$origin = __( 'Unknown', 'woocommerce' );
|
||||
@@ -318,11 +335,12 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add attribution hidden input fields for checkout & customer register froms.
|
||||
* Add `<input type="hidden">` elements for source fields.
|
||||
* Used for checkout & customer register froms.
|
||||
*/
|
||||
private function source_form_fields() {
|
||||
foreach ( $this->fields as $field ) {
|
||||
printf( '<input type="hidden" name="%s" value="" />', esc_attr( $this->get_prefixed_field( $field ) ) );
|
||||
private function source_form_elements() {
|
||||
foreach ( $this->field_names as $field_name ) {
|
||||
printf( '<input type="hidden" name="%s" value="" />', esc_attr( $this->get_prefixed_field_name( $field_name ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,8 +354,8 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
private function set_customer_source_data( WC_Customer $customer ) {
|
||||
// Nonce check is handled before user_register hook.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
foreach ( $this->get_source_values( $this->get_unprefixed_fields( $_POST ) ) as $key => $value ) {
|
||||
$customer->add_meta_data( $this->get_meta_prefixed_field( $key ), $value );
|
||||
foreach ( $this->get_source_values( $this->get_unprefixed_field_values( $_POST ) ) as $key => $value ) {
|
||||
$customer->add_meta_data( $this->get_meta_prefixed_field_name( $key ), $value );
|
||||
}
|
||||
|
||||
$customer->save_meta_data();
|
||||
@@ -352,8 +370,12 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
* @return void
|
||||
*/
|
||||
private function set_order_source_data( array $source_data, WC_Order $order ) {
|
||||
// If all the values are empty, bail.
|
||||
if ( empty( array_filter( $source_data ) ) ) {
|
||||
return;
|
||||
}
|
||||
foreach ( $source_data as $key => $value ) {
|
||||
$order->add_meta_data( $this->get_meta_prefixed_field( $key ), $value );
|
||||
$order->add_meta_data( $this->get_meta_prefixed_field_name( $key ), $value );
|
||||
}
|
||||
|
||||
$order->save_meta_data();
|
||||
@@ -394,28 +416,31 @@ class OrderAttributionController implements RegisterHooksInterface {
|
||||
* @return void
|
||||
*/
|
||||
private function send_order_tracks( array $source_data, WC_Order $order ) {
|
||||
$origin_label = $this->get_origin_label(
|
||||
$source_data['type'] ?? '',
|
||||
$origin_label = $this->get_origin_label(
|
||||
$source_data['source_type'] ?? '',
|
||||
$source_data['utm_source'] ?? '',
|
||||
false
|
||||
);
|
||||
$customer_identifier = $order->get_customer_id() ? $order->get_customer_id() : $order->get_billing_email();
|
||||
$customer_info = $this->get_customer_history( $customer_identifier );
|
||||
$tracks_data = array(
|
||||
'order_id' => $order->get_id(),
|
||||
'type' => $source_data['type'] ?? '',
|
||||
'medium' => $source_data['utm_medium'] ?? '',
|
||||
'source' => $source_data['utm_source'] ?? '',
|
||||
'device_type' => strtolower( $source_data['device_type'] ?? '(unknown)' ),
|
||||
'origin_label' => strtolower( $origin_label ),
|
||||
'session_pages' => $source_data['session_pages'] ?? 0,
|
||||
'session_count' => $source_data['session_count'] ?? 0,
|
||||
'order_total' => $order->get_total(),
|
||||
// Add 1 to include the current order (which is currently still Pending when the event is sent).
|
||||
'customer_order_count' => $customer_info['order_count'] + 1,
|
||||
'customer_registered' => $order->get_customer_id() ? 'yes' : 'no',
|
||||
|
||||
$tracks_data = array(
|
||||
'order_id' => $order->get_id(),
|
||||
'source_type' => $source_data['source_type'] ?? '',
|
||||
'medium' => $source_data['utm_medium'] ?? '',
|
||||
'source' => $source_data['utm_source'] ?? '',
|
||||
'device_type' => strtolower( $source_data['device_type'] ?? 'unknown' ),
|
||||
'origin_label' => strtolower( $origin_label ),
|
||||
'session_pages' => $source_data['session_pages'] ?? 0,
|
||||
'session_count' => $source_data['session_count'] ?? 0,
|
||||
'order_total' => $order->get_total(),
|
||||
'customer_registered' => $order->get_customer_id() ? 'yes' : 'no',
|
||||
);
|
||||
|
||||
$this->proxy->call_static(
|
||||
WC_Tracks::class,
|
||||
'record_event',
|
||||
'order_attribution',
|
||||
$tracks_data
|
||||
);
|
||||
$this->proxy->call_static( WC_Tracks::class, 'record_event', 'order_attribution', $tracks_data );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -215,7 +215,7 @@ class DataRegenerator {
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $product_ids ) {
|
||||
if ( ! is_array( $product_ids ) || empty( $product_ids ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class Filterer {
|
||||
}
|
||||
|
||||
$attribute_ids_for_and_filtering = array();
|
||||
|
||||
$clauses = array();
|
||||
foreach ( $attributes_to_filter_by as $taxonomy => $data ) {
|
||||
$all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
|
||||
$term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' );
|
||||
@@ -206,6 +206,7 @@ class Filterer {
|
||||
$hide_out_of_stock = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' );
|
||||
$in_stock_clause = $hide_out_of_stock ? ' AND in_stock = 1' : '';
|
||||
|
||||
$query = array();
|
||||
$query['select'] = 'SELECT COUNT(DISTINCT product_or_parent_id) as term_count, term_id as term_count_id';
|
||||
$query['from'] = "FROM {$this->lookup_table_name}";
|
||||
$query['join'] = "
|
||||
|
||||
@@ -4,7 +4,6 @@ declare( strict_types=1 );
|
||||
namespace Automattic\WooCommerce\Internal\Traits;
|
||||
|
||||
use Automattic\WooCommerce\Vendor\Detection\MobileDetect;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
|
||||
use Exception;
|
||||
use WC_Meta_Data;
|
||||
use WC_Order;
|
||||
@@ -19,31 +18,43 @@ use WP_Post;
|
||||
*/
|
||||
trait OrderAttributionMeta {
|
||||
|
||||
/** @var string[] */
|
||||
/**
|
||||
* The default fields and their sourcebuster accesors,
|
||||
* to show in the source data metabox.
|
||||
*
|
||||
* @var string[]
|
||||
* */
|
||||
private $default_fields = array(
|
||||
// main fields.
|
||||
'type',
|
||||
'url',
|
||||
'source_type' => 'current.typ',
|
||||
'referrer' => 'current_add.rf',
|
||||
|
||||
// utm fields.
|
||||
'utm_campaign',
|
||||
'utm_source',
|
||||
'utm_medium',
|
||||
'utm_content',
|
||||
'utm_id',
|
||||
'utm_term',
|
||||
'utm_campaign' => 'current.cmp',
|
||||
'utm_source' => 'current.src',
|
||||
'utm_medium' => 'current.mdm',
|
||||
'utm_content' => 'current.cnt',
|
||||
'utm_id' => 'current.id',
|
||||
'utm_term' => 'current.trm',
|
||||
|
||||
// additional fields.
|
||||
'session_entry',
|
||||
'session_start_time',
|
||||
'session_pages',
|
||||
'session_count',
|
||||
'user_agent',
|
||||
'session_entry' => 'current_add.ep',
|
||||
'session_start_time' => 'current_add.fd',
|
||||
'session_pages' => 'session.pgs',
|
||||
'session_count' => 'udata.vst',
|
||||
'user_agent' => 'udata.uag',
|
||||
);
|
||||
|
||||
/** @var array */
|
||||
private $fields = array();
|
||||
|
||||
/**
|
||||
* Cached `array_keys( $fields )`.
|
||||
*
|
||||
* @var array
|
||||
* */
|
||||
private $field_names = array();
|
||||
|
||||
/** @var string */
|
||||
private $field_prefix = '';
|
||||
|
||||
@@ -67,7 +78,7 @@ trait OrderAttributionMeta {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the meta fields and the field prefix.
|
||||
* Set the fields and the field prefix.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -79,7 +90,8 @@ trait OrderAttributionMeta {
|
||||
*
|
||||
* @param string[] $fields The fields to show.
|
||||
*/
|
||||
$this->fields = (array) apply_filters( 'wc_order_attribution_tracking_fields', $this->default_fields );
|
||||
$this->fields = (array) apply_filters( 'wc_order_attribution_tracking_fields', $this->default_fields );
|
||||
$this->field_names = array_keys( $this->fields );
|
||||
$this->set_field_prefix();
|
||||
}
|
||||
|
||||
@@ -119,11 +131,11 @@ trait OrderAttributionMeta {
|
||||
*/
|
||||
private function filter_meta_data( array $meta ): array {
|
||||
$return = array();
|
||||
$prefix = $this->get_meta_prefixed_field( '' );
|
||||
$prefix = $this->get_meta_prefixed_field_name( '' );
|
||||
|
||||
foreach ( $meta as $item ) {
|
||||
if ( str_starts_with( $item->key, $prefix ) ) {
|
||||
$return[ $this->unprefix_meta_field( $item->key ) ] = $item->value;
|
||||
$return[ $this->unprefix_meta_field_name( $item->key ) ] = $item->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +145,7 @@ trait OrderAttributionMeta {
|
||||
}
|
||||
|
||||
// Determine the origin based on source type and referrer.
|
||||
$source_type = $return['type'] ?? '';
|
||||
$source_type = $return['source_type'] ?? '';
|
||||
$source = $return['utm_source'] ?? '';
|
||||
$return['origin'] = $this->get_origin_label( $source_type, $source, true );
|
||||
|
||||
@@ -143,50 +155,34 @@ trait OrderAttributionMeta {
|
||||
/**
|
||||
* Get the field name with the appropriate prefix.
|
||||
*
|
||||
* @param string $field Field name.
|
||||
* @param string $name Field name.
|
||||
*
|
||||
* @return string The prefixed field name.
|
||||
*/
|
||||
private function get_prefixed_field( $field ): string {
|
||||
return "{$this->field_prefix}{$field}";
|
||||
private function get_prefixed_field_name( $name ): string {
|
||||
return "{$this->field_prefix}{$name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field name with the meta prefix.
|
||||
*
|
||||
* @param string $field The field name.
|
||||
* @param string $name The field name.
|
||||
*
|
||||
* @return string The prefixed field name.
|
||||
*/
|
||||
private function get_meta_prefixed_field( string $field ): string {
|
||||
// Map some of the fields to the correct meta name.
|
||||
if ( 'type' === $field ) {
|
||||
$field = 'source_type';
|
||||
} elseif ( 'url' === $field ) {
|
||||
$field = 'referrer';
|
||||
}
|
||||
|
||||
return "_{$this->get_prefixed_field( $field )}";
|
||||
private function get_meta_prefixed_field_name( string $name ): string {
|
||||
return "_{$this->get_prefixed_field_name( $name )}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the meta prefix from the field name.
|
||||
*
|
||||
* @param string $field The prefixed field.
|
||||
* @param string $name The prefixed fieldname .
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function unprefix_meta_field( string $field ): string {
|
||||
$return = str_replace( "_{$this->field_prefix}", '', $field );
|
||||
|
||||
// Map some of the fields to the correct meta name.
|
||||
if ( 'source_type' === $return ) {
|
||||
$return = 'type';
|
||||
} elseif ( 'referrer' === $return ) {
|
||||
$return = 'url';
|
||||
}
|
||||
|
||||
return $return;
|
||||
private function unprefix_meta_field_name( string $name ): string {
|
||||
return str_replace( "_{$this->field_prefix}", '', $name );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,19 +214,19 @@ trait OrderAttributionMeta {
|
||||
|
||||
|
||||
/**
|
||||
* Map posted, prefixed values to fields.
|
||||
* Map posted, prefixed values to field values.
|
||||
* Used for the classic forms.
|
||||
*
|
||||
* @param array $raw_values The raw values from the POST form.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_unprefixed_fields( array $raw_values = array() ): array {
|
||||
private function get_unprefixed_field_values( array $raw_values = array() ): array {
|
||||
$values = array();
|
||||
|
||||
// Look through each field in POST data.
|
||||
foreach ( $this->fields as $field ) {
|
||||
$values[ $field ] = $raw_values[ $this->get_prefixed_field( $field ) ] ?? '(none)';
|
||||
foreach ( $this->field_names as $field_name ) {
|
||||
$values[ $field_name ] = $raw_values[ $this->get_prefixed_field_name( $field_name ) ] ?? '(none)';
|
||||
}
|
||||
|
||||
return $values;
|
||||
@@ -247,13 +243,13 @@ trait OrderAttributionMeta {
|
||||
$values = array();
|
||||
|
||||
// Look through each field in given data.
|
||||
foreach ( $this->fields as $field ) {
|
||||
$value = sanitize_text_field( wp_unslash( $raw_values[ $field ] ) );
|
||||
foreach ( $this->field_names as $field_name ) {
|
||||
$value = sanitize_text_field( wp_unslash( $raw_values[ $field_name ] ) );
|
||||
if ( '(none)' === $value ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[ $field ] = $value;
|
||||
$values[ $field_name ] = $value;
|
||||
}
|
||||
|
||||
// Set the device type if possible using the user agent.
|
||||
@@ -310,7 +306,9 @@ trait OrderAttributionMeta {
|
||||
|
||||
default:
|
||||
$label = '';
|
||||
$source = __( 'Unknown', 'woocommerce' );
|
||||
$source = $translated ?
|
||||
__( 'Unknown', 'woocommerce' )
|
||||
: 'Unknown';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -359,13 +357,13 @@ trait OrderAttributionMeta {
|
||||
/**
|
||||
* Get the description for the order attribution field.
|
||||
*
|
||||
* @param string $field The field name.
|
||||
* @param string $field_name The field name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_field_description( string $field ): string {
|
||||
private function get_field_description( string $field_name ): string {
|
||||
/* translators: %s is the field name */
|
||||
$description = sprintf( __( 'Order attribution field: %s', 'woocommerce' ), $field );
|
||||
$description = sprintf( __( 'Order attribution field: %s', 'woocommerce' ), $field_name );
|
||||
|
||||
/**
|
||||
* Filter the description for the order attribution field.
|
||||
@@ -373,56 +371,8 @@ trait OrderAttributionMeta {
|
||||
* @since 8.5.0
|
||||
*
|
||||
* @param string $description The description for the order attribution field.
|
||||
* @param string $field The field name.
|
||||
* @param string $field_name The field name.
|
||||
*/
|
||||
return (string) apply_filters( 'wc_order_attribution_field_description', $description, $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order history for the customer (data matches Customers report).
|
||||
*
|
||||
* @param mixed $customer_identifier The customer ID or billing email.
|
||||
*
|
||||
* @return array Order count, total spend, and average spend per order.
|
||||
*/
|
||||
private function get_customer_history( $customer_identifier ): array {
|
||||
/*
|
||||
* Exclude the statuses that aren't valid for the Customers report.
|
||||
* 'checkout-draft' is the checkout block's draft order status. `any` is added by V2 Orders REST.
|
||||
* @see /Automattic/WooCommerce/Admin/API/Report/DataStore::get_excluded_report_order_statuses()
|
||||
*/
|
||||
$all_order_statuses = ReportsController::get_order_statuses();
|
||||
$excluded_statuses = array( 'pending', 'failed', 'cancelled', 'auto-draft', 'trash', 'checkout-draft', 'any' );
|
||||
|
||||
// Get the valid customer orders.
|
||||
$args = array(
|
||||
'limit' => - 1,
|
||||
'return' => 'objects',
|
||||
'status' => array_diff( $all_order_statuses, $excluded_statuses ),
|
||||
'type' => 'shop_order',
|
||||
);
|
||||
|
||||
// If the customer_identifier is a valid ID, use it. Otherwise, use the billing email.
|
||||
if ( is_numeric( $customer_identifier ) && $customer_identifier > 0 ) {
|
||||
$args['customer_id'] = $customer_identifier;
|
||||
} else {
|
||||
$args['billing_email'] = $customer_identifier;
|
||||
$args['customer_id'] = 0;
|
||||
}
|
||||
|
||||
$orders = wc_get_orders( $args );
|
||||
|
||||
// Populate the order_count and total_spent variables with the valid orders.
|
||||
$order_count = count( $orders );
|
||||
$total_spent = 0;
|
||||
foreach ( $orders as $order ) {
|
||||
$total_spent += $order->get_total() - $order->get_total_refunded();
|
||||
}
|
||||
|
||||
return array(
|
||||
'order_count' => $order_count,
|
||||
'total_spent' => $total_spent,
|
||||
'average_spent' => $order_count ? $total_spent / $order_count : 0,
|
||||
);
|
||||
return (string) apply_filters( 'wc_order_attribution_field_description', $description, $field_name );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\TransientFiles;
|
||||
|
||||
use \DateTime;
|
||||
use \Exception;
|
||||
use \InvalidArgumentException;
|
||||
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Utilities\TimeUtil;
|
||||
|
||||
/**
|
||||
* Transient files engine class.
|
||||
*
|
||||
* This class contains methods that allow creating files that have an expiration date.
|
||||
*
|
||||
* A transient file is created by invoking the create_transient_file method, which accepts the file contents
|
||||
* and the expiration date as arguments. Transient file names are composed by concatenating the expiration date
|
||||
* encoded in hexadecimal (3 digits for the year, 1 for the month and 2 for the day) and a random string
|
||||
* of hexadecimal digits.
|
||||
*
|
||||
* Transient files are stored in a directory whose default route is
|
||||
* wp-content/uploads/woocommerce_transient_files/yyyy-mm-dd, where "yyyy-mm-dd" is the expiration date
|
||||
* (year, month and day). The base route (minus the expiration date part) can be changed via a dedicated hook.
|
||||
*
|
||||
* Transient files that haven't expired (the expiration date is today or in the future) can be obtained remotely
|
||||
* via a dedicated URL, <server root>/wc/file/transient/<file name>. This URL is public (no authentication is required).
|
||||
* The content type of the response will always be "text/html".
|
||||
*
|
||||
* Cleanup of expired files is handled by the delete_expired_files method, which can be invoked manually
|
||||
* but there's a dedicated scheduled action that will invoke it that can be started and stopped via a dedicated tool
|
||||
* available in the WooCommerce tools page. The action runs once per day but this can be customized
|
||||
* via a dedicated hook.
|
||||
*/
|
||||
class TransientFilesEngine implements RegisterHooksInterface {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
private const CLEANUP_ACTION_NAME = 'woocommerce_expired_transient_files_cleanup';
|
||||
private const CLEANUP_ACTION_GROUP = 'wc_batch_processes';
|
||||
|
||||
/**
|
||||
* The instance of LegacyProxy to use.
|
||||
*
|
||||
* @var LegacyProxy
|
||||
*/
|
||||
private LegacyProxy $legacy_proxy;
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function register() {
|
||||
self::add_action( self::CLEANUP_ACTION_NAME, array( $this, 'handle_expired_files_cleanup_action' ) );
|
||||
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_debug_tools_entries' ), 999, 1 );
|
||||
|
||||
self::add_action( 'init', array( $this, 'handle_init' ), 0 );
|
||||
self::add_filter( 'query_vars', array( $this, 'handle_query_vars' ), 0 );
|
||||
self::add_action( 'parse_request', array( $this, 'handle_parse_request' ), 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class initialization, to be executed when the class is resolved by the container.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param LegacyProxy $legacy_proxy The instance of LegacyProxy to use.
|
||||
*/
|
||||
final public function init( LegacyProxy $legacy_proxy ) {
|
||||
$this->legacy_proxy = $legacy_proxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base directory where transient files are stored.
|
||||
*
|
||||
* The default base directory is the WordPress uploads directory plus "woocommerce_transient_files". This can
|
||||
* be changed by using the woocommerce_transient_files_directory filter.
|
||||
*
|
||||
* If the woocommerce_transient_files_directory filter is not used and the default base directory
|
||||
* doesn't exist, it will be created. If the filter is used it's the responsibility of the caller
|
||||
* to ensure that the custom directory exists, otherwise an exception will be thrown.
|
||||
*
|
||||
* The actual directory for each existing file will be the base directory plus the expiration date
|
||||
* of the file formatted as 'yyyy-mm-dd'.
|
||||
*
|
||||
* @return string Effective base directory where transient files are stored.
|
||||
* @throws Exception The custom base directory (as specified via filter) doesn't exist, or the default base directory can't be created.
|
||||
*/
|
||||
public function get_transient_files_directory(): string {
|
||||
$upload_dir_info = $this->legacy_proxy->call_function( 'wp_upload_dir' );
|
||||
$default_transient_files_directory = untrailingslashit( $upload_dir_info['basedir'] ) . '/woocommerce_transient_files';
|
||||
|
||||
/**
|
||||
* Filters the directory where transient files are stored.
|
||||
*
|
||||
* Note that this is used for both creating new files (with create_file_by_rendering_template)
|
||||
* and retrieving existing files (with get_file_by_*).
|
||||
*
|
||||
* @param string $transient_files_directory The default directory for transient files.
|
||||
* @return string The actual directory to use for storing transient files.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
$transient_files_directory = apply_filters( 'woocommerce_transient_files_directory', $default_transient_files_directory );
|
||||
|
||||
$realpathed_transient_files_directory = $this->legacy_proxy->call_function( 'realpath', $transient_files_directory );
|
||||
if ( false === $realpathed_transient_files_directory ) {
|
||||
if ( $transient_files_directory === $default_transient_files_directory ) {
|
||||
if ( ! $this->legacy_proxy->call_function( 'wp_mkdir_p', $transient_files_directory ) ) {
|
||||
throw new Exception( "Can't create directory: $transient_files_directory" );
|
||||
}
|
||||
$realpathed_transient_files_directory = $this->legacy_proxy->call_function( 'realpath', $transient_files_directory );
|
||||
} else {
|
||||
throw new Exception( "The base transient files directory doesn't exist: $transient_files_directory" );
|
||||
}
|
||||
}
|
||||
|
||||
return untrailingslashit( $realpathed_transient_files_directory );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a transient file.
|
||||
*
|
||||
* @param string $file_contents The contents of the file.
|
||||
* @param string|int $expiration_date A string representing the expiration date formatted as "yyyy-mm-dd", or a number representing the expiration date as a timestamp (the time of day part will be ignored).
|
||||
* @return string The name of the transient file created (without path information).
|
||||
* @throws \InvalidArgumentException Invalid expiration date (wrongly formatted, or it's a date in the past).
|
||||
* @throws \Exception The directory to store the file doesn't exist and can't be created.
|
||||
*/
|
||||
public function create_transient_file( string $file_contents, $expiration_date ): string {
|
||||
if ( is_numeric( $expiration_date ) ) {
|
||||
$expiration_date = gmdate( 'Y-m-d', $expiration_date );
|
||||
} elseif ( ! is_string( $expiration_date ) || ! TimeUtil::is_valid_date( $expiration_date, 'Y-m-d' ) ) {
|
||||
$expiration_date = is_scalar( $expiration_date ) ? $expiration_date : gettype( $expiration_date );
|
||||
throw new InvalidArgumentException( "$expiration_date is not a valid date, expected format: YYYY-MM-DD" );
|
||||
}
|
||||
|
||||
$expiration_date_object = DateTime::createFromFormat( 'Y-m-d', $expiration_date, TimeUtil::get_utc_date_time_zone() );
|
||||
$today_date_object = new DateTime( $this->legacy_proxy->call_function( 'gmdate', 'Y-m-d' ), TimeUtil::get_utc_date_time_zone() );
|
||||
|
||||
if ( $expiration_date_object < $today_date_object ) {
|
||||
throw new InvalidArgumentException( "The supplied expiration date, $expiration_date, is in the past" );
|
||||
}
|
||||
|
||||
$filename = bin2hex( $this->legacy_proxy->call_function( 'random_bytes', 16 ) );
|
||||
|
||||
$transient_files_directory = $this->get_transient_files_directory();
|
||||
$transient_files_directory .= '/' . $expiration_date_object->format( 'Y-m-d' );
|
||||
if ( ! $this->legacy_proxy->call_function( 'is_dir', $transient_files_directory ) ) {
|
||||
if ( ! $this->legacy_proxy->call_function( 'wp_mkdir_p', $transient_files_directory ) ) {
|
||||
throw new Exception( "Can't create directory: $transient_files_directory" );
|
||||
}
|
||||
}
|
||||
$filepath = $transient_files_directory . '/' . $filename;
|
||||
|
||||
WP_Filesystem();
|
||||
$wp_filesystem = $this->legacy_proxy->get_global( 'wp_filesystem' );
|
||||
if ( false === $wp_filesystem->put_contents( $filepath, $file_contents ) ) {
|
||||
throw new Exception( "Can't create file: $filepath" );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%03x%01x%02x%s',
|
||||
$expiration_date_object->format( 'Y' ),
|
||||
$expiration_date_object->format( 'm' ),
|
||||
$expiration_date_object->format( 'd' ),
|
||||
$filename
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full physical path of a transient file given its name.
|
||||
*
|
||||
* @param string $filename The name of the transient file to locate.
|
||||
* @return string|null The full physical path of the file, or null if the files doesn't exist.
|
||||
*/
|
||||
public function get_transient_file_path( string $filename ): ?string {
|
||||
if ( strlen( $filename ) < 7 || ! ctype_xdigit( $filename ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$expiration_date = sprintf(
|
||||
'%04d-%02d-%02d',
|
||||
hexdec( substr( $filename, 0, 3 ) ),
|
||||
hexdec( substr( $filename, 3, 1 ) ),
|
||||
hexdec( substr( $filename, 4, 2 ) )
|
||||
);
|
||||
if ( ! TimeUtil::is_valid_date( $expiration_date, 'Y-m-d' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file_path = $this->get_transient_files_directory() . '/' . $expiration_date . '/' . substr( $filename, 6 );
|
||||
|
||||
return is_file( $file_path ) ? $file_path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a file has expired, given its full physical file path.
|
||||
*
|
||||
* Given a file name returned by 'create_transient_file', the procedure to check if it has expired is as follows:
|
||||
*
|
||||
* 1. Use 'get_transient_file_path' to obtain the full file path.
|
||||
* 2. If the above returns null, the file doesn't exist anymore (likely it expired and was deleted by the cleanup process).
|
||||
* 3. Otherwise, use 'file_has_expired' passing the obtained full file path.
|
||||
*
|
||||
* @param string $file_path The full file path to check.
|
||||
* @return bool True if the file has expired, false otherwise.
|
||||
* @throws \Exception Thrown by DateTime if a wrong file path is passed.
|
||||
*/
|
||||
public function file_has_expired( string $file_path ): bool {
|
||||
$dirname = dirname( $file_path );
|
||||
$expiration_date = basename( $dirname );
|
||||
$expiration_date_object = new DateTime( $expiration_date, TimeUtil::get_utc_date_time_zone() );
|
||||
$today_date_object = new DateTime( $this->legacy_proxy->call_function( 'gmdate', 'Y-m-d' ), TimeUtil::get_utc_date_time_zone() );
|
||||
return $expiration_date_object < $today_date_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing transient file.
|
||||
*
|
||||
* @param string $filename The name of the file to delete.
|
||||
* @return bool True if the file has been deleted, false otherwise (the file didn't exist).
|
||||
*/
|
||||
public function delete_transient_file( string $filename ): bool {
|
||||
$file_path = $this->get_transient_file_path( $filename );
|
||||
if ( is_null( $file_path ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dirname = dirname( $file_path );
|
||||
wp_delete_file( $file_path );
|
||||
$this->delete_directory_if_not_empty( $dirname );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete expired transient files from the filesystem.
|
||||
*
|
||||
* @param int $limit Maximum number of files to delete.
|
||||
* @return array "deleted_count" with the number of files actually deleted, "files_remain" that will be true if there are still files left to delete.
|
||||
* @throws Exception The base directory for transient files (possibly changed via filter) doesn't exist.
|
||||
*/
|
||||
public function delete_expired_files( int $limit = 1000 ): array {
|
||||
$expiration_date_gmt = $this->legacy_proxy->call_function( 'gmdate', 'Y-m-d' );
|
||||
$base_dir = $this->get_transient_files_directory();
|
||||
$subdirs = glob( $base_dir . '/[2-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]', GLOB_ONLYDIR );
|
||||
if ( false === $subdirs ) {
|
||||
throw new Exception( "Error when getting the list of subdirectories of $base_dir" );
|
||||
}
|
||||
|
||||
$subdirs = array_map( fn( $name ) => substr( $name, strlen( $name ) - 10, 10 ), $subdirs );
|
||||
$expired_subdirs = array_filter( $subdirs, fn( $name ) => $name < $expiration_date_gmt );
|
||||
asort( $subdirs ); // We want to delete files starting with the oldest expiration month.
|
||||
|
||||
$remaining_limit = $limit;
|
||||
$limit_reached = false;
|
||||
foreach ( $expired_subdirs as $subdir ) {
|
||||
$full_dir_path = $base_dir . '/' . $subdir;
|
||||
$files_to_delete = glob( $full_dir_path . '/*' );
|
||||
if ( count( $files_to_delete ) > $remaining_limit ) {
|
||||
$limit_reached = true;
|
||||
$files_to_delete = array_slice( $files_to_delete, 0, $remaining_limit );
|
||||
}
|
||||
array_map( 'wp_delete_file', $files_to_delete );
|
||||
$remaining_limit -= count( $files_to_delete );
|
||||
$this->delete_directory_if_not_empty( $full_dir_path );
|
||||
|
||||
if ( $limit_reached ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'deleted_count' => $limit - $remaining_limit,
|
||||
'files_remain' => $limit_reached,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the expired files cleanup action currently scheduled?
|
||||
*
|
||||
* @return bool True if the expired files cleanup action is currently scheduled, false otherwise.
|
||||
*/
|
||||
public function expired_files_cleanup_is_scheduled(): bool {
|
||||
return as_has_scheduled_action( self::CLEANUP_ACTION_NAME, array(), self::CLEANUP_ACTION_GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action that will do one round of expired files cleanup.
|
||||
* The action is scheduled to run immediately. If a previous pending action exists, it's unscheduled first.
|
||||
*/
|
||||
public function schedule_expired_files_cleanup(): void {
|
||||
$this->unschedule_expired_files_cleanup();
|
||||
as_schedule_single_action( time() + 1, self::CLEANUP_ACTION_NAME, array(), self::CLEANUP_ACTION_GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the scheduled action that does the expired files cleanup, if it's scheduled.
|
||||
*/
|
||||
public function unschedule_expired_files_cleanup(): void {
|
||||
if ( $this->expired_files_cleanup_is_scheduled() ) {
|
||||
as_unschedule_action( self::CLEANUP_ACTION_NAME, array(), self::CLEANUP_ACTION_GROUP );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the expired files cleanup action and schedule a new one.
|
||||
*
|
||||
* If files are actually deleted then we assume that more files are pending deletion and schedule the next
|
||||
* action to run immediately. Otherwise (nothing was deleted) we schedule the next action for one day later
|
||||
* (but this can be changed via the 'woocommerce_delete_expired_transient_files_interval' filter).
|
||||
*
|
||||
* If the actual deletion process fails the next action is scheduled anyway for one day later
|
||||
* or for the interval given by the filter.
|
||||
*
|
||||
* NOTE: If the default interval is changed to something different from DAY_IN_SECONDS, please adjust the
|
||||
* "every 24h" text in add_debug_tools_entries too.
|
||||
*/
|
||||
private function handle_expired_files_cleanup_action(): void {
|
||||
$new_interval = null;
|
||||
|
||||
try {
|
||||
$result = $this->delete_expired_files();
|
||||
if ( $result['deleted_count'] > 0 ) {
|
||||
$new_interval = 1;
|
||||
}
|
||||
} finally {
|
||||
if ( is_null( $new_interval ) ) {
|
||||
|
||||
/**
|
||||
* Filter to alter the interval between the actions that delete expired transient files.
|
||||
*
|
||||
* @param int $interval The default time before the next action run, in seconds.
|
||||
* @return int The time to actually wait before the next action run, in seconds.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
$new_interval = apply_filters( 'woocommerce_delete_expired_transient_files_interval', DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
$next_time = $this->legacy_proxy->call_function( 'time' ) + $new_interval;
|
||||
$this->legacy_proxy->call_function( 'as_schedule_single_action', $next_time, self::CLEANUP_ACTION_NAME, array(), self::CLEANUP_ACTION_GROUP );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the tools to (re)schedule and un-schedule the expired files cleanup actions in the WooCommerce debug tools page.
|
||||
*
|
||||
* @param array $tools_array Original debug tools array.
|
||||
* @return array Updated debug tools array
|
||||
*/
|
||||
private function add_debug_tools_entries( array $tools_array ): array {
|
||||
$cleanup_is_scheduled = $this->expired_files_cleanup_is_scheduled();
|
||||
|
||||
$tools_array['schedule_expired_transient_files_cleanup'] = array(
|
||||
'name' => $cleanup_is_scheduled ?
|
||||
__( 'Re-schedule expired transient files cleanup', 'woocommerce' ) :
|
||||
__( 'Schedule expired transient files cleanup', 'woocommerce' ),
|
||||
'desc' => $cleanup_is_scheduled ?
|
||||
__( 'Remove the currently scheduled action to delete expired transient files, then schedule it again for running immediately. Subsequent actions will run once every 24h.', 'woocommerce' ) :
|
||||
__( 'Schedule the action to delete expired transient files for running immediately. Subsequent actions will run once every 24h.', 'woocommerce' ),
|
||||
'button' => $cleanup_is_scheduled ?
|
||||
__( 'Re-schedule', 'woocommerce' ) :
|
||||
__( 'Schedule', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => array( $this, 'schedule_expired_files_cleanup' ),
|
||||
);
|
||||
|
||||
if ( $cleanup_is_scheduled ) {
|
||||
$tools_array['unschedule_expired_transient_files_cleanup'] = array(
|
||||
'name' => __( 'Un-schedule expired transient files cleanup', 'woocommerce' ),
|
||||
'desc' => __( "Remove the currently scheduled action to delete expired transient files. Expired files won't be automatically deleted until the 'Schedule expired transient files cleanup' tool is run again.", 'woocommerce' ),
|
||||
'button' => __( 'Un-schedule', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => array( $this, 'unschedule_expired_files_cleanup' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $tools_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a directory if it isn't empty.
|
||||
*
|
||||
* @param string $directory Full directory path.
|
||||
*/
|
||||
private function delete_directory_if_not_empty( string $directory ) {
|
||||
if ( ! ( new \FilesystemIterator( $directory ) )->valid() ) {
|
||||
rmdir( $directory );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the "init" action, add rewrite rules for the "wc/file" endpoint.
|
||||
*/
|
||||
private function handle_init() {
|
||||
add_rewrite_rule( '^wc/file/transient/?$', 'index.php?wc-transient-file-name=', 'top' );
|
||||
add_rewrite_rule( '^wc/file/transient/(.+)$', 'index.php?wc-transient-file-name=$matches[1]', 'top' );
|
||||
add_rewrite_endpoint( 'wc/file/transient', EP_ALL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the "query_vars" action, add the "wc-transient-file-name" variable for the "wc/file/transient" endpoint.
|
||||
*
|
||||
* @param array $vars The original query variables.
|
||||
* @return array The updated query variables.
|
||||
*/
|
||||
private function handle_query_vars( $vars ) {
|
||||
$vars[] = 'wc-transient-file-name';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing, WordPress.WP.AlternativeFunctions
|
||||
|
||||
/**
|
||||
* Handle the "parse_request" action for the "wc/file/transient" endpoint.
|
||||
*
|
||||
* If the request is not for "/wc/file/transient/<filename>" or "index.php?wc-transient-file-name=filename",
|
||||
* it returns without doing anything. Otherwise, it will serve the contents of the file with the provided name
|
||||
* if it exists, is public and has not expired; or will return a "Not found" status otherwise.
|
||||
*
|
||||
* The file will be served with a content type header of "text/html".
|
||||
*/
|
||||
private function handle_parse_request() {
|
||||
global $wp;
|
||||
|
||||
// phpcs:ignore WordPress.Security
|
||||
$query_arg = wp_unslash( $_GET['wc-transient-file-name'] ?? null );
|
||||
if ( ! is_null( $query_arg ) ) {
|
||||
$wp->query_vars['wc-transient-file-name'] = $query_arg;
|
||||
}
|
||||
|
||||
if ( is_null( $wp->query_vars['wc-transient-file-name'] ?? null ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
||||
if ( 'GET' !== ( $_SERVER['REQUEST_METHOD'] ?? null ) ) {
|
||||
status_header( 405 );
|
||||
exit();
|
||||
}
|
||||
|
||||
$this->serve_file_contents( $wp->query_vars['wc-transient-file-name'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method to serve the contents of a transient file.
|
||||
*
|
||||
* @param string $file_name Transient file id or filename.
|
||||
*/
|
||||
private function serve_file_contents( string $file_name ) {
|
||||
$legacy_proxy = wc_get_container()->get( LegacyProxy::class );
|
||||
|
||||
try {
|
||||
$file_path = $this->get_transient_file_path( $file_name );
|
||||
if ( is_null( $file_path ) ) {
|
||||
$legacy_proxy->call_function( 'status_header', 404 );
|
||||
$legacy_proxy->exit();
|
||||
}
|
||||
|
||||
if ( $this->file_has_expired( $file_path ) ) {
|
||||
$legacy_proxy->call_function( 'status_header', 404 );
|
||||
$legacy_proxy->exit();
|
||||
}
|
||||
|
||||
$file_length = filesize( $file_path );
|
||||
if ( false === $file_length ) {
|
||||
throw new Exception( "Can't retrieve file size: $file_path" );
|
||||
}
|
||||
|
||||
$file_handle = fopen( $file_path, 'r' );
|
||||
} catch ( Exception $ex ) {
|
||||
$error_message = "Error serving transient file $file_name: {$ex->getMessage()}";
|
||||
wc_get_logger()->error( $error_message );
|
||||
|
||||
$legacy_proxy->call_function( 'status_header', 500 );
|
||||
$legacy_proxy->exit();
|
||||
}
|
||||
|
||||
$legacy_proxy->call_function( 'status_header', 200 );
|
||||
$legacy_proxy->call_function( 'header', 'Content-Type: text/html' );
|
||||
$legacy_proxy->call_function( 'header', "Content-Length: $file_length" );
|
||||
|
||||
try {
|
||||
while ( ! feof( $file_handle ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo fread( $file_handle, 1024 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that fires after a transient file has been successfully served, right before terminating the request.
|
||||
*
|
||||
* @param array $transient_file_info Information about the served file, as returned by get_file_by_name.
|
||||
* @param bool $is_json_rest_api_request True if the request came from the JSON API endpoint, false if it came from the authenticated endpoint.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
do_action( 'woocommerce_transient_file_contents_served', $file_name );
|
||||
} catch ( Exception $e ) {
|
||||
wc_get_logger()->error( "Error serving transient file $file_name: {$e->getMessage()}" );
|
||||
// We can't change the response status code at this point.
|
||||
} finally {
|
||||
fclose( $file_handle );
|
||||
$legacy_proxy->exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,4 +25,85 @@ class Users {
|
||||
|
||||
return is_multisite() ? $user->has_cap( 'manage_sites' ) : $user->has_cap( 'manage_options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the email is valid.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $supplied_email Supplied email.
|
||||
* @param string $context Context in which we are checking the email.
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_user_verify_order_email( $order_id, $supplied_email = null, $context = 'view' ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
$billing_email = $order->get_billing_email();
|
||||
$customer_id = $order->get_customer_id();
|
||||
|
||||
// If we do not have a billing email for the order (could happen in the order is created manually, or if the
|
||||
// requirement for this has been removed from the checkout flow), email verification does not make sense.
|
||||
if ( empty( $billing_email ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No verification step is needed if the user is logged in and is already associated with the order.
|
||||
if ( $customer_id && get_current_user_id() === $customer_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the grace period within which we do not require any sort of email verification step before rendering
|
||||
* the 'order received' or 'order pay' pages.
|
||||
*
|
||||
* To eliminate the grace period, set to zero (or to a negative value). Note that this filter is not invoked
|
||||
* at all if email verification is deemed to be unnecessary (in other words, it cannot be used to force
|
||||
* verification in *all* cases).
|
||||
*
|
||||
* @since 8.0.0
|
||||
*
|
||||
* @param int $grace_period Time in seconds after an order is placed before email verification may be required.
|
||||
* @param WC_Order $this The order for which this grace period is being assessed.
|
||||
* @param string $context Indicates the context in which we might verify the email address. Typically 'order-pay' or 'order-received'.
|
||||
*/
|
||||
$verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order, $context );
|
||||
$date_created = $order->get_date_created();
|
||||
|
||||
// We do not need to verify the email address if we are within the grace period immediately following order creation.
|
||||
if (
|
||||
is_a( $date_created, \WC_DateTime::class, true )
|
||||
&& time() - $date_created->getTimestamp() <= $verification_grace_period
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$session = wc()->session;
|
||||
$session_email = '';
|
||||
|
||||
if ( is_a( $session, \WC_Session::class ) ) {
|
||||
$customer = $session->get( 'customer' );
|
||||
$session_email = is_array( $customer ) && isset( $customer['email'] ) ? $customer['email'] : '';
|
||||
}
|
||||
|
||||
// Email verification is required if the user cannot be identified, or if they supplied an email address but the nonce check failed.
|
||||
$can_view_orders = current_user_can( 'read_private_shop_orders' );
|
||||
$session_email_match = $session_email === $billing_email;
|
||||
$supplied_email_match = $supplied_email === $billing_email;
|
||||
|
||||
$email_verification_required = ! $session_email_match && ! $supplied_email_match && ! $can_view_orders;
|
||||
|
||||
/**
|
||||
* Provides an opportunity to override the (potential) requirement for shoppers to verify their email address
|
||||
* before we show information such as the order summary, or order payment page.
|
||||
*
|
||||
* Note that this hook is not always triggered, therefore it is (for example) unsuitable as a way of forcing
|
||||
* email verification across all order confirmation/order payment scenarios. Instead, the filter primarily
|
||||
* exists as a way to *remove* the email verification step.
|
||||
*
|
||||
* @since 7.9.0
|
||||
*
|
||||
* @param bool $email_verification_required If email verification is required.
|
||||
* @param WC_Order $order The relevant order.
|
||||
* @param string $context The context under which we are performing this check.
|
||||
*/
|
||||
return (bool) apply_filters( 'woocommerce_order_email_verification_required', $email_verification_required, $order, $context );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user