auto-patch 638-dev-dev01-2024-05-14T20_44_36

This commit is contained in:
root
2024-05-14 20:44:36 +00:00
parent a941559057
commit 5dbb0b284e
1812 changed files with 29671 additions and 14588 deletions

View File

@@ -201,6 +201,16 @@ class AbstractBlock implements BlockInterface {
$this->attributes = $attributes;
}
/**
* Set a block attribute value without replacing the entire attributes object.
*
* @param string $key The attribute key.
* @param mixed $value The attribute value.
*/
public function set_attribute( string $key, $value ) {
$this->attributes[ $key ] = $value;
}
/**
* Get the template that this block belongs to.
*/

View File

@@ -73,7 +73,7 @@ class Coupons {
__( 'Coupons', 'woocommerce' ),
'manage_options',
'coupons-moved',
[ $this, 'coupon_menu_moved' ]
array( $this, 'coupon_menu_moved' )
);
}
@@ -117,15 +117,7 @@ class Coupons {
return;
}
$rtl = is_rtl() ? '-rtl' : '';
wp_enqueue_style(
'wc-admin-marketing-coupons',
WCAdminAssets::get_url( "marketing-coupons/style{$rtl}", 'css' ),
array(),
WCAdminAssets::get_file_version( 'css' )
);
WCAdminAssets::register_style( 'marketing-coupons', 'style' );
WCAdminAssets::register_script( 'wp-admin-scripts', 'marketing-coupons', true );
}
}

View File

@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\Admin;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\DataSourcePoller;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsDataSourcePoller;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RemoteInboxNotificationsEngine;
use Automattic\WooCommerce\Internal\Admin\Notes\AddFirstProduct;
use Automattic\WooCommerce\Internal\Admin\Notes\ChoosingTheme;
@@ -146,7 +146,7 @@ class Events {
$this->possibly_refresh_data_source_pollers();
if ( $this->is_remote_inbox_notifications_enabled() ) {
DataSourcePoller::get_instance()->read_specs_from_data_sources();
RemoteInboxNotificationsDataSourcePoller::get_instance()->read_specs_from_data_sources();
RemoteInboxNotificationsEngine::run();
}

View File

@@ -124,12 +124,25 @@ class Loader {
$sections = self::get_embed_breadcrumbs();
$sections = is_array( $sections ) ? $sections : array( $sections );
$page_title = '';
$pages_with_tabs = array( 'Settings', 'Reports', 'Status' );
if (
count( $sections ) > 2 &&
is_array( $sections[1] ) &&
in_array( $sections[1][1], $pages_with_tabs, true )
) {
$page_title = $sections[1][1];
} else {
$page_title = end( $sections );
}
?>
<div id="woocommerce-embedded-root" class="is-embed-loading">
<div class="woocommerce-layout">
<div class="woocommerce-layout__header is-embed-loading">
<h1 class="woocommerce-layout__header-heading">
<?php self::output_heading( end( $sections ) ); ?>
<?php self::output_heading( $page_title ); ?>
</h1>
</div>
</div>
@@ -293,7 +306,7 @@ class Loader {
$settings['orderStatuses'] = self::get_order_statuses( wc_get_order_statuses() );
$settings['stockStatuses'] = self::get_order_statuses( wc_get_product_stock_status_options() );
$settings['currency'] = self::get_currency_settings();
$settings['locale'] = [
$settings['locale'] = array(
'siteLocale' => isset( $settings['siteLocale'] )
? $settings['siteLocale']
: get_locale(),
@@ -303,7 +316,7 @@ class Loader {
'weekdaysShort' => isset( $settings['l10n']['weekdaysShort'] )
? $settings['l10n']['weekdaysShort']
: array_values( $wp_locale->weekday_abbrev ),
];
);
}
$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
@@ -327,7 +340,7 @@ class Loader {
$setting_options = new \WC_REST_Setting_Options_V2_Controller();
foreach ( $preload_settings as $group ) {
$group_settings = $setting_options->get_group_settings( $group );
$preload_settings = [];
$preload_settings = array();
foreach ( $group_settings as $option ) {
if ( array_key_exists( 'id', $option ) && array_key_exists( 'value', $option ) ) {
$preload_settings[ $option['id'] ] = $option['value'];
@@ -374,7 +387,7 @@ class Loader {
if ( ! empty( $preload_data_endpoints ) ) {
$settings['dataEndpoints'] = isset( $settings['dataEndpoints'] )
? $settings['dataEndpoints']
: [];
: array();
foreach ( $preload_data_endpoints as $key => $endpoint ) {
// Handle error case: rest_do_request() doesn't guarantee success.
if ( empty( $preload_data[ $endpoint ] ) ) {

View File

@@ -4,7 +4,8 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
use Automattic\Jetpack\Constants;
use WP_Filesystem_Direct;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Exception;
/**
* File class.
@@ -60,14 +61,6 @@ class File {
* @param string $path The absolute path of the file.
*/
public function __construct( $path ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
global $wp_filesystem;
if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) {
WP_Filesystem();
}
$this->path = $path;
$this->ingest_path();
}
@@ -237,27 +230,33 @@ class File {
/**
* Check if the file represented by the class instance is a file and is readable.
*
* @global WP_Filesystem_Direct $wp_filesystem
*
* @return bool
*/
public function is_readable(): bool {
global $wp_filesystem;
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$is_readable = $filesystem->is_file( $this->path ) && $filesystem->is_readable( $this->path );
} catch ( Exception $exception ) {
return false;
}
return $wp_filesystem->is_file( $this->path ) && $wp_filesystem->is_readable( $this->path );
return $is_readable;
}
/**
* Check if the file represented by the class instance is a file and is writable.
*
* @global WP_Filesystem_Direct $wp_filesystem
*
* @return bool
*/
public function is_writable(): bool {
global $wp_filesystem;
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$is_writable = $filesystem->is_file( $this->path ) && $filesystem->is_writable( $this->path );
} catch ( Exception $exception ) {
return false;
}
return $wp_filesystem->is_file( $this->path ) && $wp_filesystem->is_writable( $this->path );
return $is_writable;
}
/**
@@ -372,31 +371,38 @@ class File {
/**
* Get the time of the last modification of the file, as a Unix timestamp. Or false if the file isn't readable.
*
* @global WP_Filesystem_Direct $wp_filesystem
*
* @return int|false
*/
public function get_modified_timestamp() {
global $wp_filesystem;
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$timestamp = $filesystem->mtime( $this->path );
} catch ( Exception $exception ) {
return false;
}
return $wp_filesystem->mtime( $this->path );
return $timestamp;
}
/**
* Get the size of the file in bytes. Or false if the file isn't readable.
*
* @global WP_Filesystem_Direct $wp_filesystem
*
* @return int|false
*/
public function get_file_size() {
global $wp_filesystem;
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
if ( ! $wp_filesystem->is_readable( $this->path ) ) {
if ( ! $filesystem->is_readable( $this->path ) ) {
return false;
}
$size = $filesystem->size( $this->path );
} catch ( Exception $exception ) {
return false;
}
return $wp_filesystem->size( $this->path );
return $size;
}
/**
@@ -405,10 +411,13 @@ class File {
* @return bool
*/
protected function create(): bool {
global $wp_filesystem;
$created = $wp_filesystem->touch( $this->path );
$modded = $wp_filesystem->chmod( $this->path );
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$created = $filesystem->touch( $this->path );
$modded = $filesystem->chmod( $this->path );
} catch ( Exception $exception ) {
return false;
}
return $created && $modded;
}
@@ -463,8 +472,6 @@ class File {
return false;
}
global $wp_filesystem;
$created = 0;
if ( $this->has_standard_filename() ) {
$created = $this->get_created_timestamp();
@@ -489,7 +496,13 @@ class File {
$new_filename = str_replace( $search, $replace, $old_filename );
$new_path = str_replace( $old_filename, $new_filename, $this->path );
$moved = $wp_filesystem->move( $this->path, $new_path, true );
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$moved = $filesystem->move( $this->path, $new_path, true );
} catch ( Exception $exception ) {
return false;
}
if ( ! $moved ) {
return false;
}
@@ -503,13 +516,16 @@ class File {
/**
* Delete the file from the filesystem.
*
* @global WP_Filesystem_Direct $wp_filesystem
*
* @return bool True on success, false on failure.
*/
public function delete(): bool {
global $wp_filesystem;
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$deleted = $filesystem->delete( $this->path, false, 'f' );
} catch ( Exception $exception ) {
return false;
}
return $wp_filesystem->delete( $this->path, false, 'f' );
return $deleted;
}
}

View File

@@ -4,6 +4,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Logging\Settings;
use PclZip;
use WC_Cache_Helper;
use WP_Error;
@@ -75,20 +76,6 @@ class FileController {
*/
private const SEARCH_CACHE_KEY = 'logs_previous_search';
/**
* The absolute path to the log directory.
*
* @var string
*/
private $log_directory;
/**
* Class FileController
*/
public function __construct() {
$this->log_directory = trailingslashit( Constants::get_constant( 'WC_LOG_DIR' ) );
}
/**
* Get the file size limit that determines when to rotate a file.
*
@@ -141,7 +128,7 @@ class FileController {
}
if ( ! $file instanceof File ) {
$new_path = $this->log_directory . $this->generate_filename( $source, $time );
$new_path = Settings::get_log_directory() . $this->generate_filename( $source, $time );
$file = new File( $new_path );
}
@@ -217,7 +204,7 @@ class FileController {
$args = wp_parse_args( $args, self::DEFAULTS_GET_FILES );
$pattern = $args['source'] . '*.log';
$paths = glob( $this->log_directory . $pattern );
$paths = glob( Settings::get_log_directory() . $pattern );
if ( false === $paths ) {
return new WP_Error(
@@ -332,14 +319,15 @@ class FileController {
* @return File[]
*/
public function get_files_by_id( array $file_ids ): array {
$paths = array();
$log_directory = Settings::get_log_directory();
$paths = array();
foreach ( $file_ids as $file_id ) {
// Look for the standard filename format first, which includes a hash.
$glob = glob( $this->log_directory . $file_id . '-*.log' );
$glob = glob( $log_directory . $file_id . '-*.log' );
if ( ! $glob ) {
$glob = glob( $this->log_directory . $file_id . '.log' );
$glob = glob( $log_directory . $file_id . '.log' );
}
if ( is_array( $glob ) ) {
@@ -423,7 +411,7 @@ class FileController {
$created_pattern = $created ? '-' . gmdate( 'Y-m-d', $created ) . '-' : '';
$rotation_pattern = $this->log_directory . $source . $rotations_pattern . $created_pattern . '*.log';
$rotation_pattern = Settings::get_log_directory() . $source . $rotations_pattern . $created_pattern . '*.log';
$rotation_paths = glob( $rotation_pattern );
$rotation_files = $this->convert_paths_to_objects( $rotation_paths );
foreach ( $rotation_files as $rotation_file ) {
@@ -462,7 +450,7 @@ class FileController {
* @return array|WP_Error
*/
public function get_file_sources() {
$paths = glob( $this->log_directory . '*.log' );
$paths = glob( Settings::get_log_directory() . '*.log' );
if ( false === $paths ) {
return new WP_Error(
'wc_log_directory_error',
@@ -493,10 +481,7 @@ class FileController {
$files = $this->get_files_by_id( $file_ids );
foreach ( $files as $file ) {
$result = false;
if ( $file->is_writable() ) {
$result = $file->delete();
}
$result = $file->delete();
if ( true === $result ) {
$deleted ++;
@@ -671,10 +656,10 @@ class FileController {
*/
public function get_log_directory_size(): int {
$bytes = 0;
$path = realpath( $this->log_directory );
$path = realpath( Settings::get_log_directory() );
if ( wp_is_writable( $path ) ) {
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ) );
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ), \RecursiveIteratorIterator::CATCH_GET_CHILD );
foreach ( $iterator as $file ) {
$bytes += $file->getSize();

View File

@@ -3,8 +3,9 @@ declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Admin\Logging\FileV2;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Exception;
use WP_Error;
use WP_Filesystem_Direct;
/**
* FileExport class.
@@ -39,11 +40,6 @@ class FileExporter {
* part of the path.
*/
public function __construct( string $path, string $alternate_filename = '' ) {
global $wp_filesystem;
if ( ! $wp_filesystem instanceof WP_Filesystem_Direct ) {
WP_Filesystem();
}
$this->path = $path;
$this->alternate_filename = $alternate_filename;
}
@@ -54,8 +50,14 @@ class FileExporter {
* @return WP_Error|void Only returns something if there is an error.
*/
public function emit_file() {
global $wp_filesystem;
if ( ! $wp_filesystem->is_file( $this->path ) || ! $wp_filesystem->is_readable( $this->path ) ) {
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$is_readable = $filesystem->is_file( $this->path ) && $filesystem->is_readable( $this->path );
} catch ( Exception $exception ) {
$is_readable = false;
}
if ( ! $is_readable ) {
return new WP_Error(
'wc_logs_invalid_file',
__( 'Could not access file.', 'woocommerce' )
@@ -104,11 +106,11 @@ class FileExporter {
* @return void
*/
private function send_contents(): void {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- No suitable alternative.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- No suitable alternative.
$stream = fopen( $this->path, 'rb' );
while ( is_resource( $stream ) && ! feof( $stream ) ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread -- No suitable alternative.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- No suitable alternative.
$chunk = fread( $stream, self::CHUNK_SIZE );
if ( is_string( $chunk ) ) {
@@ -117,7 +119,7 @@ class FileExporter {
}
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- No suitable alternative.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- No suitable alternative.
fclose( $stream );
}

View File

@@ -104,7 +104,7 @@ class PageController {
if ( ! $this->settings->logging_is_enabled() ) {
add_action(
'admin_notices',
function() {
function () {
?>
<div class="notice notice-warning">
<p>
@@ -395,7 +395,7 @@ class PageController {
if ( is_string( $line ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- format_line does the escaping.
echo $this->format_line( $line, $line_number );
$line_number ++;
++$line_number;
}
?>
<?php endwhile; ?>
@@ -464,7 +464,7 @@ class PageController {
array(
'file_id' => array(
'filter' => FILTER_CALLBACK,
'options' => function( $file_id ) {
'options' => function ( $file_id ) {
return sanitize_file_name( wp_unslash( $file_id ) );
},
),
@@ -484,13 +484,13 @@ class PageController {
),
'search' => array(
'filter' => FILTER_CALLBACK,
'options' => function( $search ) {
'options' => function ( $search ) {
return esc_html( wp_unslash( $search ) );
},
),
'source' => array(
'filter' => FILTER_CALLBACK,
'options' => function( $source ) {
'options' => function ( $source ) {
return File::sanitize_source( wp_unslash( $source ) );
},
),
@@ -624,7 +624,7 @@ class PageController {
}
if ( is_wp_error( $export_error ) ) {
wp_die( wp_kses_post( $export_error ) );
wp_die( wp_kses_post( $export_error->get_error_message() ) );
}
break;
case 'delete':
@@ -654,7 +654,7 @@ class PageController {
if ( is_numeric( $deleted ) ) {
add_action(
'admin_notices',
function() use ( $deleted ) {
function () use ( $deleted ) {
?>
<div class="notice notice-info is-dismissible">
<p>

View File

@@ -8,8 +8,12 @@ use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\File;
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Exception;
use WC_Admin_Settings;
use WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
use WP_Filesystem_Direct;
/**
* Settings class.
@@ -44,6 +48,51 @@ class Settings {
self::add_action( 'wc_logs_load_tab', array( $this, 'save_settings' ) );
}
/**
* Get the directory for storing log files.
*
* The `wp_upload_dir` function takes into account the possibility of multisite, and handles changing
* the directory if the context is switched to a different site in the network mid-request.
*
* @return string The full directory path, with trailing slash.
*/
public static function get_log_directory(): string {
if ( true === Constants::get_constant( 'WC_LOG_DIR_CUSTOM' ) ) {
$dir = Constants::get_constant( 'WC_LOG_DIR' );
} else {
$upload_dir = wc_get_container()->get( LegacyProxy::class )->call_function( 'wp_upload_dir' );
/**
* Filter to change the directory for storing WooCommerce's log files.
*
* @param string $dir The full directory path, with trailing slash.
*
* @since 8.8.0
*/
$dir = apply_filters( 'woocommerce_log_directory', $upload_dir['basedir'] . '/wc-logs/' );
}
$dir = trailingslashit( $dir );
$realpath = realpath( $dir );
if ( false === $realpath ) {
$result = wp_mkdir_p( $dir );
if ( true === $result ) {
// Create infrastructure to prevent listing contents of the logs directory.
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$filesystem->put_contents( $dir . '.htaccess', 'deny from all' );
$filesystem->put_contents( $dir . 'index.html', '' );
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Creation failed.
}
}
}
return $dir;
}
/**
* The definitions used by WC_Admin_Settings to render and save settings controls.
*
@@ -242,7 +291,21 @@ class Settings {
*/
private function get_filesystem_settings_definitions(): array {
$location_info = array();
$directory = trailingslashit( Constants::get_constant( 'WC_LOG_DIR' ) );
$directory = self::get_log_directory();
$status_info = array();
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
if ( $filesystem instanceof WP_Filesystem_Direct ) {
$status_info[] = __( '✅ Ready', 'woocommerce' );
} else {
$status_info[] = __( '⚠️ The file system is not configured for direct writes. This could cause problems for the logger.', 'woocommerce' );
$status_info[] = __( 'You may want to switch to the database for log storage.', 'woocommerce' );
}
} catch ( Exception $exception ) {
$status_info[] = __( '⚠️ The file system connection could not be initialized.', 'woocommerce' );
$status_info[] = __( 'You may want to switch to the database for log storage.', 'woocommerce' );
}
$location_info[] = sprintf(
// translators: %s is a location in the filesystem.
@@ -257,13 +320,6 @@ class Settings {
$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>'
);
$location_info[] = sprintf(
// translators: %s is an amount of computer disk space, e.g. 5 KB.
__( 'Directory size: %s', 'woocommerce' ),
@@ -276,6 +332,11 @@ class Settings {
'id' => self::PREFIX . 'settings',
'type' => 'title',
),
'file_status' => array(
'title' => __( 'Status', 'woocommerce' ),
'type' => 'info',
'text' => implode( "\n\n", $status_info ),
),
'log_directory' => array(
'title' => __( 'Location', 'woocommerce' ),
'type' => 'info',
@@ -299,7 +360,7 @@ class Settings {
$table = "{$wpdb->prefix}woocommerce_log";
$location_info = sprintf(
// translators: %s is a location in the filesystem.
// translators: %s is the name of a table in the database.
__( 'Log entries are stored in this database table: %s', 'woocommerce' ),
"<code>$table</code>"
);

View File

@@ -2,7 +2,7 @@
/**
* Marketing Specs Handler
*
* Fetches the specifications for the marketing feature from Woo.com API.
* Fetches the specifications for the marketing feature from WooCommerce.com API.
*/
namespace Automattic\WooCommerce\Internal\Admin\Marketing;
@@ -22,7 +22,7 @@ class MarketingSpecs {
const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base';
/**
* Load knowledge base posts from Woo.com
* Load knowledge base posts from WooCommerce.com
*
* @param string|null $topic The topic of marketing knowledgebase to retrieve.
* @return array

View File

@@ -7,6 +7,8 @@ namespace Automattic\WooCommerce\Internal\Admin;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use WC_Helper_Updater;
use WC_Woo_Update_Manager_Plugin;
/**
* Contains backend logic for the Marketplace feature.
@@ -17,6 +19,8 @@ class Marketplace {
/**
* Class initialization, to be executed when the class is resolved by the container.
*
* @internal
*/
final public function init() {
if ( false === FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
@@ -55,10 +59,11 @@ class Marketplace {
public static function get_marketplace_pages() {
$marketplace_pages = array(
array(
'id' => 'woocommerce-marketplace',
'parent' => 'woocommerce',
'title' => __( 'Extensions', 'woocommerce' ),
'path' => '/extensions',
'id' => 'woocommerce-marketplace',
'parent' => 'woocommerce',
'title' => __( 'Extensions', 'woocommerce' ) . WC_Helper_Updater::get_updates_count_html(),
'page_title' => __( 'Extensions', 'woocommerce' ),
'path' => '/extensions',
),
);
@@ -79,7 +84,7 @@ class Marketplace {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( 'woocommerce_page_wc-admin' !== $hook_suffix ) {
return;
};
}
if ( ! isset( $_GET['path'] ) || '/extensions' !== $_GET['path'] ) {
return;

View File

@@ -67,7 +67,7 @@ class AddFirstProduct {
sprintf( __( 'Nice one; you\'ve created a WooCommerce store! Now it\'s time to add your first product and get ready to start selling.%s', 'woocommerce' ), '<br/><br/>' ),
__( 'There are three ways to add your products: you can <strong>create products manually, import them at once via CSV file</strong>, or <strong>migrate them from another service</strong>.<br/><br/>', 'woocommerce' ),
/* translators: %1$s is an open anchor tag (<a>) and %2$s is a close link tag (</a>). */
sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '<a href="https://woo.com/document/managing-products/?utm_source=help_panel&utm_medium=product">', '</a>' ),
sprintf( __( '%1$1sExplore our docs%2$2s for more information, or just get started!', 'woocommerce' ), '<a href="https://woocommerce.com/document/managing-products/?utm_source=help_panel&utm_medium=product">', '</a>' ),
);
$additional_data = array(

View File

@@ -48,7 +48,7 @@ class ChoosingTheme {
$note->add_action(
'visit-the-theme-marketplace',
__( 'Visit the theme marketplace', 'woocommerce' ),
'https://woo.com/product-category/themes/?utm_source=inbox&utm_medium=product'
'https://woocommerce.com/product-category/themes/?utm_source=inbox&utm_medium=product'
);
return $note;
}

View File

@@ -78,7 +78,7 @@ class CustomizeStoreWithBlocks {
$note->add_action(
'customize-store-with-blocks',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/posts/how-to-customize-your-online-store-with-woocommerce-blocks/?utm_source=inbox&utm_medium=product',
'https://woocommerce.com/posts/how-to-customize-your-online-store-with-woocommerce-blocks/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;

View File

@@ -75,7 +75,7 @@ class CustomizingProductCatalog {
$note->add_action(
'day-after-first-product',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/document/woocommerce-customizer/?utm_source=inbox&utm_medium=product'
'https://woocommerce.com/document/woocommerce-customizer/?utm_source=inbox&utm_medium=product'
);
return $note;

View File

@@ -54,7 +54,7 @@ class EUVATNumber {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/products/eu-vat-number/?utm_medium=product',
'https://woocommerce.com/products/eu-vat-number/?utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;

View File

@@ -63,7 +63,7 @@ class EditProductsOnTheMove {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/mobile/?utm_source=inbox&utm_medium=product'
'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
);
return $note;

View File

@@ -54,7 +54,7 @@ class LaunchChecklist {
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/posts/pre-launch-checklist-the-essentials/?utm_source=inbox&utm_medium=product' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/posts/pre-launch-checklist-the-essentials/?utm_source=inbox&utm_medium=product' );
return $note;
}
}

View File

@@ -92,7 +92,7 @@ class MagentoMigration {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/posts/how-migrate-from-magento-to-woocommerce/?utm_source=inbox'
'https://woocommerce.com/posts/how-migrate-from-magento-to-woocommerce/?utm_source=inbox'
);
return $note;

View File

@@ -56,7 +56,7 @@ class ManageOrdersOnTheGo {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/mobile/?utm_source=inbox&utm_medium=product'
'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
);
return $note;

View File

@@ -20,7 +20,7 @@ use Automattic\WooCommerce\Admin\PluginsHelper;
*
* Note: This should probably live in the Jetpack plugin in the future.
*
* @see https://developer.woo.com/2020/10/16/using-the-admin-notes-inbox-in-woocommerce/
* @see https://developer.woocommerce.com/2020/10/16/using-the-admin-notes-inbox-in-woocommerce/
*/
class MarketingJetpack {
// Shared Note Traits.

View File

@@ -71,7 +71,7 @@ class MigrateFromShopify {
$note->add_action(
'migrate-from-shopify',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/posts/migrate-from-shopify-to-woocommerce/?utm_source=inbox&utm_medium=product',
'https://woocommerce.com/posts/migrate-from-shopify-to-woocommerce/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;

View File

@@ -47,7 +47,7 @@ class MobileApp {
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_medium=product' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/mobile/?utm_medium=product' );
return $note;
}
}

View File

@@ -59,7 +59,7 @@ class OnboardingPayments {
$note->add_action(
'view-payment-gateways',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=product',
'https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED,
true
);

View File

@@ -90,7 +90,7 @@ class OnlineClothingStore {
$note->add_action(
'online-clothing-store',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/posts/starting-an-online-clothing-store/?utm_source=inbox&utm_medium=product',
'https://woocommerce.com/posts/starting-an-online-clothing-store/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);
return $note;

View File

@@ -239,13 +239,13 @@ class OrderMilestones {
return array(
'name' => 'learn-more',
'label' => __( 'Learn more', 'woocommerce' ),
'query' => 'https://woo.com/document/managing-orders/?utm_source=inbox&utm_medium=product',
'query' => 'https://woocommerce.com/document/managing-orders/?utm_source=inbox&utm_medium=product',
);
case 10:
return array(
'name' => 'browse',
'label' => __( 'Browse', 'woocommerce' ),
'query' => 'https://woo.com/success-stories/?utm_source=inbox&utm_medium=product',
'query' => 'https://woocommerce.com/success-stories/?utm_source=inbox&utm_medium=product',
);
case 100:
case 250:

View File

@@ -72,7 +72,7 @@ class PaymentsMoreInfoNeeded {
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action( 'learn-more', __( 'Learn more here', 'woocommerce' ), 'https://woo.com/payments/' );
$note->add_action( 'learn-more', __( 'Learn more here', 'woocommerce' ), 'https://woocommerce.com/payments/' );
return $note;
}
}

View File

@@ -60,7 +60,7 @@ class PerformanceOnMobile {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/mobile/?utm_source=inbox&utm_medium=product'
'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product'
);
return $note;

View File

@@ -51,7 +51,7 @@ class RealTimeOrderAlerts {
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/mobile/?utm_source=inbox&utm_medium=product' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/mobile/?utm_source=inbox&utm_medium=product' );
return $note;
}
}

View File

@@ -76,7 +76,7 @@ class SellingOnlineCourses {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://woo.com/posts/how-to-sell-online-courses-wordpress/?utm_source=inbox&utm_medium=product',
'https://woocommerce.com/posts/how-to-sell-online-courses-wordpress/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_ACTIONED
);

View File

@@ -49,7 +49,7 @@ class TrackingOptIn {
return;
}
/* translators: 1: open link to Woo.com settings, 2: open link to Woo.com tracking documentation, 3: close link tag. */
/* translators: 1: open link to WooCommerce.com settings, 2: open link to WooCommerce.com tracking documentation, 3: close link tag. */
$content_format = __(
'Gathering usage data allows us to improve WooCommerce. Your store will be considered as we evaluate new features, judge the quality of an update, or determine if an improvement makes sense. You can always visit the %1$sSettings%3$s and choose to stop sharing data. %2$sRead more%3$s about what data we collect.',
'woocommerce'
@@ -58,7 +58,7 @@ class TrackingOptIn {
$note_content = sprintf(
$content_format,
'<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=advanced&section=woocommerce_com' ) ) . '" target="_blank">',
'<a href="https://woo.com/usage-tracking?utm_medium=product" target="_blank">',
'<a href="https://woocommerce.com/usage-tracking?utm_medium=product" target="_blank">',
'</a>'
);

View File

@@ -37,7 +37,7 @@ class UnsecuredReportFiles {
sprintf(
/* translators: 1: opening analytics docs link tag. 2: closing link tag */
__( 'Files that may contain %1$sstore analytics%2$s reports were found in your uploads directory - we recommend assessing and deleting any such files.', 'woocommerce' ),
'<a href="https://woo.com/document/woocommerce-analytics/" target="_blank">',
'<a href="https://woocommerce.com/document/woocommerce-analytics/" target="_blank">',
'</a>'
)
);
@@ -48,7 +48,7 @@ class UnsecuredReportFiles {
$note->add_action(
'learn-more',
__( 'Learn more', 'woocommerce' ),
'https://developer.woo.com/2021/09/22/important-security-patch-released-in-woocommerce/',
'https://developer.woocommerce.com/2021/09/22/important-security-patch-released-in-woocommerce/',
Note::E_WC_ADMIN_NOTE_UNACTIONED,
true
);

View File

@@ -118,7 +118,7 @@ class WooCommercePayments {
$note->set_type( Note::E_WC_ADMIN_NOTE_MARKETING );
$note->set_name( self::NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woo.com/payments/?utm_medium=product', Note::E_WC_ADMIN_NOTE_UNACTIONED );
$note->add_action( 'learn-more', __( 'Learn more', 'woocommerce' ), 'https://woocommerce.com/payments/?utm_medium=product', Note::E_WC_ADMIN_NOTE_UNACTIONED );
$note->add_action( 'get-started', __( 'Get started', 'woocommerce' ), wc_admin_url( '&action=setup-woocommerce-payments' ), Note::E_WC_ADMIN_NOTE_ACTIONED, true );
$note->add_nonce_to_action( 'get-started', 'setup-woocommerce-payments', '' );

View File

@@ -53,7 +53,7 @@ class WooCommerceSubscriptions {
$note->add_action(
'learn-more',
__( 'Learn More', 'woocommerce' ),
'https://woo.com/products/woocommerce-subscriptions/?utm_source=inbox&utm_medium=product',
'https://woocommerce.com/products/woocommerce-subscriptions/?utm_source=inbox&utm_medium=product',
Note::E_WC_ADMIN_NOTE_UNACTIONED,
true
);

View File

@@ -1,8 +1,8 @@
<?php
/**
* WooCommerce Admin (Dashboard) Woo.com Extension Subscriptions Note Provider.
* WooCommerce Admin (Dashboard) WooCommerce.com Extension Subscriptions Note Provider.
*
* Adds notes to the merchant's inbox concerning Woo.com extension subscriptions.
* Adds notes to the merchant's inbox concerning WooCommerce.com extension subscriptions.
*/
namespace Automattic\WooCommerce\Internal\Admin\Notes;
@@ -129,7 +129,7 @@ class WooSubscriptionsNotes {
}
/**
* Whether or not we think the site is currently connected to Woo.com.
* Whether or not we think the site is currently connected to WooCommerce.com.
*
* @return bool
*/
@@ -139,7 +139,7 @@ class WooSubscriptionsNotes {
}
/**
* Returns the Woo.com provided site ID for this site.
* Returns the WooCommerce.com provided site ID for this site.
*
* @return int|false
*/
@@ -187,7 +187,7 @@ class WooSubscriptionsNotes {
}
/**
* Adds a note prompting to connect to Woo.com.
* Adds a note prompting to connect to WooCommerce.com.
*/
public function add_no_connection_note() {
$note = self::get_note();
@@ -195,11 +195,11 @@ class WooSubscriptionsNotes {
}
/**
* Get the Woo.com connection note
* Get the WooCommerce.com connection note
*/
public static function get_note() {
$note = new Note();
$note->set_title( __( 'Connect to Woo.com', 'woocommerce' ) );
$note->set_title( __( 'Connect to WooCommerce.com', 'woocommerce' ) );
$note->set_content( __( 'Connect to get important product notifications and updates.', 'woocommerce' ) );
$note->set_content_data( (object) array() );
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
@@ -221,6 +221,9 @@ class WooSubscriptionsNotes {
* @return int|false
*/
public function get_product_id_from_subscription_note( &$note ) {
if ( ! is_object( $note ) ) {
return false;
}
$content_data = $note->get_content_data();
if ( property_exists( $content_data, 'product_id' ) ) {
@@ -358,7 +361,7 @@ class WooSubscriptionsNotes {
$note->add_action(
'enable-autorenew',
__( 'Enable Autorenew', 'woocommerce' ),
'https://woo.com/my-account/my-subscriptions/?utm_medium=product'
'https://woocommerce.com/my-account/my-subscriptions/?utm_medium=product'
);
$note->set_content( $note_content );
$note->set_content_data( $note_content_data );

View File

@@ -44,7 +44,7 @@ class OnboardingSync {
}
/**
* Send profile data to Woo.com.
* Send profile data to WooCommerce.com.
*/
private function send_profile_data() {
if ( 'yes' !== get_option( 'woocommerce_allow_tracking', 'no' ) ) {
@@ -142,7 +142,7 @@ class OnboardingSync {
! $task_list ||
$task_list->is_hidden() ||
! isset( $_SERVER['HTTP_REFERER'] ) ||
0 !== strpos( $_SERVER['HTTP_REFERER'], 'https://woo.com/checkout?utm_medium=product' ) // phpcs:ignore sanitization ok.
0 !== strpos( wp_unslash( $_SERVER['HTTP_REFERER'] ), 'https://woocommerce.com/checkout?utm_medium=product' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
) {
return;
}

View File

@@ -69,9 +69,9 @@ class OnboardingThemes {
}
/**
* Sort themes returned from Woo.com
* Sort themes returned from WooCommerce.com
*
* @param array $themes Array of themes from Woo.com.
* @param array $themes Array of themes from WooCommerce.com.
* @return array
*/
public static function sort_woocommerce_themes( $themes ) {

View File

@@ -282,7 +282,7 @@ class ListTable extends WP_List_Table {
</h2>
<div class="woocommerce-BlankState-buttons">
<a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://woo.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin"><?php esc_html_e( 'Learn more about orders', 'woocommerce' ); ?></a>
<a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://woocommerce.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin"><?php esc_html_e( 'Learn more about orders', 'woocommerce' ); ?></a>
</div>
<?php
@@ -982,7 +982,7 @@ class ListTable extends WP_List_Table {
*
* @return string Edit link for the order.
*/
private function get_order_edit_link( WC_Order $order ) : string {
private function get_order_edit_link( WC_Order $order ): string {
return $this->page_controller->get_edit_url( $order->get_id() );
}
@@ -1352,7 +1352,7 @@ class ListTable extends WP_List_Table {
}
do_action( 'woocommerce_remove_order_personal_data', $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$changed++;
++$changed;
}
return $changed;
@@ -1380,7 +1380,7 @@ class ListTable extends WP_List_Table {
$order->update_status( $new_status, __( 'Order status changed by bulk edit.', 'woocommerce' ), true );
do_action( 'woocommerce_order_edit_status', $id, $new_status ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$changed++;
++$changed;
}
return $changed;
@@ -1403,7 +1403,7 @@ class ListTable extends WP_List_Table {
$updated_order = wc_get_order( $id );
if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) {
$changed++;
++$changed;
}
}
@@ -1423,7 +1423,7 @@ class ListTable extends WP_List_Table {
foreach ( $ids as $id ) {
if ( $orders_store->untrash_order( wc_get_order( $id ) ) ) {
$changed++;
++$changed;
}
}
@@ -1547,6 +1547,11 @@ class ListTable extends WP_List_Table {
<a href="{{ data.shipping_address_map_url }}" target="_blank">{{{ data.formatted_shipping_address }}}</a>
<# } #>
<# if ( data.data.shipping.phone ) { #>
<strong><?php esc_html_e( 'Phone', 'woocommerce' ); ?></strong>
<a href="tel:{{ data.data.shipping.phone }}">{{ data.data.shipping.phone }}</a>
<# } #>
<# if ( data.shipping_via ) { #>
<strong><?php esc_html_e( 'Shipping method', 'woocommerce' ); ?></strong>
{{ data.shipping_via }}
@@ -1629,10 +1634,25 @@ class ListTable extends WP_List_Table {
'products' => __( 'Products', 'woocommerce' ),
'all' => __( 'All', 'woocommerce' ),
);
/**
* Filters the search filters available in the admin order search. Can be used to add new or remove existing filters.
* When adding new filters, `woocommerce_hpos_generate_where_for_search_filter` should also be used to generate the WHERE clause for the new filter
*
* @since 8.9.0.
*
* @param $options array List of available filters.
*/
$options = apply_filters( 'woocommerce_hpos_admin_search_filters', $options );
$saved_setting = get_user_setting( 'wc-search-filter-hpos-admin', 'all' );
$selected = sanitize_text_field( wp_unslash( $_REQUEST['search-filter'] ?? $saved_setting ) );
if ( $saved_setting !== $selected ) {
set_user_setting( 'wc-search-filter-hpos-admin', $selected );
}
?>
<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>
<option value="<?php echo esc_attr( wp_unslash( sanitize_text_field( $value ) ) ); ?>" <?php selected( $value, sanitize_text_field( wp_unslash( $selected ) ) ); ?>><?php echo esc_html( $label ); ?></option>
<?php
}
}

View File

@@ -5,8 +5,8 @@
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
use WC_Data_Store;
use WC_Meta_Data;
use Automattic\WooCommerce\Internal\DataStores\CustomMetaDataStore;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStoreMeta;
use WC_Order;
use WP_Ajax_Response;
@@ -92,16 +92,44 @@ class CustomMetaBox {
* Compute keys to display in autofill when adding new meta key entry in custom meta box.
* Currently, returns empty keys, will be implemented after caching is merged.
*
* @param array|null $keys Keys to display in autofill.
* @param \WP_Post|\WC_Order $order Order object.
* @param mixed $deprecated Unused argument. For backwards compatibility.
* @param \WP_Post|\WC_Order $order Order object.
*
* @return array|mixed Array of keys to display in autofill.
* @return array Array of keys to display in autofill.
*/
public function order_meta_keys_autofill( $keys, $order ) {
if ( is_a( $order, \WC_Order::class ) ) {
public function order_meta_keys_autofill( $deprecated, $order ) {
if ( ! is_a( $order, \WC_Order::class ) ) {
return array();
}
/**
* Filters values for the meta key dropdown in the Custom Fields meta box.
*
* Compatibility filter for `postmeta_form_keys` filter.
*
* @since 6.9.0
*
* @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null.
* @param \WC_Order $order The current post object.
*/
$keys = apply_filters( 'postmeta_form_keys', null, $order );
if ( null === $keys || ! is_array( $keys ) ) {
/**
* Compatibility filter for 'postmeta_form_limit', which filters the number of custom fields to retrieve
* for the drop-down in the Custom Fields meta box.
*
* @since 8.8.0
*
* @param int $limit Number of custom fields to retrieve. Default 30.
*/
$limit = apply_filters( 'postmeta_form_limit', 30 );
$keys = wc_get_container()->get( OrdersTableDataStoreMeta::class )->get_meta_keys( $limit );
}
if ( $keys ) {
natcasesort( $keys );
}
return $keys;
}
@@ -116,17 +144,6 @@ class CustomMetaBox {
$meta_key_input_id = 'metakeyselect';
$keys = $this->order_meta_keys_autofill( null, $order );
/**
* Filters values for the meta key dropdown in the Custom Fields meta box.
*
* Compatibility filter for `postmeta_form_keys` filter.
*
* @since 6.9.0
*
* @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null.
* @param \WC_Order $order The current post object.
*/
$keys = apply_filters( 'postmeta_form_keys', $keys, $order );
?>
<p><strong><?php esc_html_e( 'Add New Custom Field:', 'woocommerce' ); ?></strong></p>
<table id="newmeta">
@@ -145,43 +162,42 @@ class CustomMetaBox {
<option value="#NONE#"><?php esc_html_e( '&mdash; Select &mdash;', 'woocommerce' ); ?></option>
<?php
foreach ( $keys as $key ) {
if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'edit_others_shop_order', $order->get_id() ) ) {
if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'edit_others_shop_orders' ) ) {
continue;
}
echo "\n<option value='" . esc_attr( $key ) . "'>" . esc_html( $key ) . '</option>';
}
?>
</select>
<input class="hide-if-js" type="text" id="metakeyinput" name="metakeyinput" value="" />
<a href="#postcustomstuff" class="hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggle();return false;">
<span id="enternew"><?php esc_html_e( 'Enter new', 'woocommerce' ); ?></span>
<span id="cancelnew" class="hidden"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></span></a>
<input class="hidden" type="text" id="metakeyinput" name="metakeyinput" value="" aria-label="<?php esc_attr_e( 'New custom field name', 'woocommerce' ); ?>" />
<button type="button" id="newmeta-button" class="button button-small hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggleClass('hidden');jQuery('#metakeyinput, #metakeyselect').filter(':visible').trigger('focus');">
<span id="enternew"><?php esc_html_e( 'Enter new', 'woocommerce' ); ?></span>
<span id="cancelnew" class="hidden"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></span>
<?php } else { ?>
<input type="text" id="metakeyinput" name="metakeyinput" value="" />
<?php } ?>
</td>
<td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea></td>
<td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea>
<?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?>
</td>
</tr>
<tr><td colspan="2">
<div class="submit">
<?php
submit_button(
__( 'Add Custom Field', 'woocommerce' ),
'',
'addmeta',
false,
array(
'id' => 'newmeta-submit',
'data-wp-lists' => 'add:the-list:newmeta',
)
);
?>
</div>
<?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?>
</td></tr>
</tbody>
</table>
<div class="submit add-custom-field">
<?php
submit_button(
__( 'Add Custom Field', 'woocommerce' ),
'',
'addmeta',
false,
array(
'id' => 'newmeta-submit',
'data-wp-lists' => 'add:the-list:newmeta',
)
);
?>
</div>
<?php
}
@@ -218,17 +234,20 @@ class CustomMetaBox {
$order_id = (int) $_POST['order_id'] ?? 0;
$order = $this->verify_order_edit_permission_for_ajax( $order_id );
if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) {
$select_meta_key = trim( sanitize_text_field( wp_unslash( $_POST['metakeyselect'] ?? '' ) ) );
$input_meta_key = trim( sanitize_text_field( wp_unslash( $_POST['metakeyinput'] ?? '' ) ) );
if ( empty( $_POST['meta'] ) && in_array( $select_meta_key, array( '', '#NONE#' ), true ) && ! $input_meta_key ) {
wp_die( 1 );
}
if ( isset( $_POST['metakeyinput'] ) ) { // add meta.
$meta_key = sanitize_text_field( wp_unslash( $_POST['metakeyinput'] ) );
$meta_value = sanitize_text_field( wp_unslash( $_POST['metavalue'] ?? '' ) );
$this->handle_add_meta( $order, $meta_key, $meta_value );
} else { // update.
$meta = wp_unslash( $_POST['meta'] ?? array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitization done below in array_walk.
if ( ! empty( $_POST['meta'] ) ) { // update.
$meta = wp_unslash( $_POST['meta'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitization done below in array_walk.
$this->handle_update_meta( $order, $meta );
} else { // add meta.
$meta_value = sanitize_text_field( wp_unslash( $_POST['metavalue'] ?? '' ) );
$meta_key = $input_meta_key ? $input_meta_key : $select_meta_key;
$this->handle_add_meta( $order, $meta_key, $meta_value );
}
}

View File

@@ -66,18 +66,20 @@ class OrderAttribution {
public function output( WC_Order $order ) {
$meta = $this->filter_meta_data( $order->get_meta_data() );
// If we don't have any meta to show, return.
if ( empty( $meta ) ) {
esc_html_e( 'No order source data available.', 'woocommerce' );
return;
}
$this->format_meta_data( $meta );
// No more details if there is only the origin value - this is for unknown source types.
$has_more_details = array( 'origin' ) !== array_keys( $meta );
// For direct, web admin, or mobile app orders, also don't show more details.
$simple_sources = array( 'typein', 'admin', 'mobile_app' );
if ( isset( $meta['source_type'] ) && in_array( $meta['source_type'], $simple_sources, true ) ) {
$has_more_details = false;
}
$template_data = array(
'meta' => $meta,
// Only show more details toggle if there is more than just the origin.
'has_more_details' => array( 'origin' ) !== array_keys( $meta ),
'has_more_details' => $has_more_details,
);
wc_get_template( 'order/attribution-details.php', $template_data );
}

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* When {@see OrdersTableDataStore} is in use, this class takes care of redirecting admins from CPT-based URLs
@@ -126,11 +127,16 @@ class PostsRedirectionController {
*/
private function maybe_redirect_to_edit_order_page(): void {
$post_id = absint( $_GET['post'] ?? 0 );
if ( ! $post_id ) {
return;
}
$redirect_from_types = wc_get_order_types( 'admin-menu' );
$redirect_from_types[] = 'shop_order_placehold';
if ( ! $post_id || ! in_array( get_post_type( $post_id ), $redirect_from_types, true ) || ! isset( $_GET['action'] ) ) {
$post_type = get_post_type( $post_id );
$order_type = $post_type ? $post_type : OrderUtil::get_order_type( $post_id );
if ( ! in_array( $order_type, $redirect_from_types, true ) || ! isset( $_GET['action'] ) ) {
return;
}

View File

@@ -97,7 +97,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/google-listings-and-ads" target="_blank">',
'<a href="https://woocommerce.com/products/google-listings-and-ads" target="_blank">',
'</a>'
),
'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
@@ -124,7 +124,7 @@ class DefaultFreeExtensions {
),
'facebook-for-woocommerce' => array(
'name' => __( 'Facebook for WooCommerce', 'woocommerce' ),
'description' => __( 'List products and create ads on Facebook and Instagram with <a href="https://woo.com/products/facebook/">Facebook for WooCommerce</a>', 'woocommerce' ),
'description' => __( 'List products and create ads on Facebook and Instagram with <a href="https://woocommerce.com/products/facebook/">Facebook for WooCommerce</a>', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/onboarding/facebook.png', WC_PLUGIN_FILE ),
'manage_url' => 'admin.php?page=wc-facebook',
'is_visible' => false,
@@ -180,7 +180,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Accept credit cards and other popular payment methods with %1$sWooPayments%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/woocommerce-payments" target="_blank">',
'<a href="https://woocommerce.com/products/woocommerce-payments" target="_blank">',
'</a>'
),
'is_visible' => array(
@@ -395,7 +395,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Print shipping labels with %1$sWooCommerce Shipping%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/shipping" target="_blank">',
'<a href="https://woocommerce.com/products/shipping" target="_blank">',
'</a>'
),
'is_visible' => array(
@@ -464,7 +464,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Get automated sales tax with %1$sWooCommerce Tax%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/tax" target="_blank">',
'<a href="https://woocommerce.com/products/tax" target="_blank">',
'</a>'
),
'is_visible' => array(
@@ -546,7 +546,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Enhance speed and security with %1$sJetpack%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/jetpack" target="_blank">',
'<a href="https://woocommerce.com/products/jetpack" target="_blank">',
'</a>'
),
'is_visible' => array(
@@ -569,7 +569,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Level up your email marketing with %1$sMailPoet%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/mailpoet" target="_blank">',
'<a href="https://woocommerce.com/products/mailpoet" target="_blank">',
'</a>'
),
'manage_url' => 'admin.php?page=mailpoet-newsletters',
@@ -814,7 +814,7 @@ class DefaultFreeExtensions {
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Create ad campaigns and reach one billion global users with %1$sTikTok for WooCommerce%2$s', 'woocommerce' ),
'<a href="https://woo.com/products/tiktok-for-woocommerce" target="_blank">',
'<a href="https://woocommerce.com/products/tiktok-for-woocommerce" target="_blank">',
'</a>'
),
'manage_url' => 'admin.php?page=tiktok',
@@ -846,56 +846,56 @@ class DefaultFreeExtensions {
'label' => __( 'Get paid with WooPayments', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
'description' => __( "Securely accept payments and manage payment activity straight from your store's dashboard", 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/woocommerce-payments',
'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments',
'install_priority' => 5,
),
'woocommerce-services:shipping' => array(
'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/woocommerce-shipping',
'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping',
'install_priority' => 3,
),
'jetpack' => array(
'label' => __( 'Boost content creation with Jetpack AI Assistant', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ),
'description' => __( 'Save time on content creation — unlock high-quality blog posts and pages using AI.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/jetpack',
'learn_more_link' => 'https://woocommerce.com/products/jetpack',
'install_priority' => 8,
),
'pinterest-for-woocommerce' => array(
'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-pinterest.svg', WC_PLUGIN_FILE ),
'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/pinterest-for-woocommerce',
'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce',
'install_priority' => 2,
),
'mailpoet' => array(
'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-mailpoet.svg', WC_PLUGIN_FILE ),
'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/mailpoet',
'learn_more_link' => 'https://woocommerce.com/products/mailpoet',
'install_priority' => 7,
),
'tiktok-for-business' => array(
'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-tiktok.svg', WC_PLUGIN_FILE ),
'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/tiktok-for-woocommerce',
'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce',
'install_priority' => 1,
),
'google-listings-and-ads' => array(
'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ),
'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/google-listings-and-ads',
'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads',
'install_priority' => 6,
),
'woocommerce-services:tax' => array(
'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
'description' => __( 'Automatically calculate how much sales tax should be collected by city, country, or state.', 'woocommerce' ),
'learn_more_link' => 'https://woo.com/products/tax',
'learn_more_link' => 'https://woocommerce.com/products/tax',
'install_priority' => 4,
),
);

View File

@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\RuleEvaluator;
/**
* Evaluates the extension and returns it.

View File

@@ -2,15 +2,16 @@
namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
use Automattic\WooCommerce\Admin\RemoteSpecs\DataSourcePoller;
/**
* Specs data source poller class for remote free extensions.
*/
class RemoteFreeExtensionsDataSourcePoller extends \Automattic\WooCommerce\Admin\DataSourcePoller {
class RemoteFreeExtensionsDataSourcePoller extends DataSourcePoller {
const ID = 'remote_free_extensions';
const DATA_SOURCES = array(
'https://woocommerce.com/wp-json/wccom/obw-free-extensions/3.0/extensions.json',
'https://woocommerce.com/wp-json/wccom/obw-free-extensions/4.0/extensions.json',
);
/**

View File

@@ -128,14 +128,7 @@ class ShippingLabelBanner {
* @param string $hook current page hook.
*/
public function add_print_shipping_label_script( $hook ) {
$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style(
'print-shipping-label-banner-style',
WCAdminAssets::get_url( "print-shipping-label-banner/style{$rtl}", 'css' ),
array( 'wp-components' ),
WCAdminAssets::get_file_version( 'css' )
);
WCAdminAssets::register_style( 'print-shipping-label-banner', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'print-shipping-label-banner', true );
$payload = array(

View File

@@ -98,15 +98,22 @@ class WCAdminAssets {
}
/**
* Gets the file modified time as a cache buster if we're in dev mode, or the plugin version otherwise.
* Gets the file modified time as a cache buster if we're in dev mode,
* or the asset version (file content hash) if exists, or the WooCommerce version.
*
* @param string $ext File extension.
* @param string $ext File extension.
* @param string|null $asset_version Optional. The version from the asset file.
* @return string The cache buster value to use for the given file.
*/
public static function get_file_version( $ext ) {
public static function get_file_version( $ext, $asset_version = null ) {
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
return filemtime( WC_ADMIN_ABSPATH . self::get_path( $ext ) );
}
if ( ! empty( $asset_version ) ) {
return $asset_version;
}
return WC_VERSION;
}
@@ -253,9 +260,7 @@ class WCAdminAssets {
return;
}
$js_file_version = self::get_file_version( 'js' );
$css_file_version = self::get_file_version( 'css' );
// Register the JS scripts.
$scripts = array(
'wc-admin-layout',
'wc-explat',
@@ -299,6 +304,7 @@ class WCAdminAssets {
try {
$script_assets_filename = self::get_script_asset_filename( $script_path_name, 'index' );
$script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename;
$script_version = self::get_file_version( 'js', $script_assets['version'] );
global $wp_version;
if ( 'app' === $script_path_name && version_compare( $wp_version, '6.3', '<' ) ) {
@@ -320,92 +326,83 @@ class WCAdminAssets {
wp_register_script(
$script,
self::get_url( $script_path_name . '/index', 'js' ),
$script_assets ['dependencies'],
$js_file_version,
$script_assets['dependencies'],
$script_version,
true
);
if ( in_array( $script, $translated_scripts, true ) ) {
wp_set_script_translations( $script, 'woocommerce' );
}
if ( WC_ADMIN_APP === $script ) {
wp_localize_script(
WC_ADMIN_APP,
'wcAdminAssets',
array(
'path' => plugins_url( self::get_path( 'js' ), WC_ADMIN_PLUGIN_FILE ),
'version' => $script_version,
)
);
}
} catch ( \Exception $e ) {
// Avoid crashing WordPress if an asset file could not be loaded.
wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__, $script_path_name );
}
}
wp_register_style(
'wc-admin-layout',
self::get_url( 'admin-layout/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-admin-layout', 'rtl', 'replace' );
wp_register_style(
'wc-components',
self::get_url( 'components/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-components', 'rtl', 'replace' );
wp_register_style(
'wc-block-templates',
self::get_url( 'block-templates/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-block-templates', 'rtl', 'replace' );
wp_register_style(
'wc-product-editor',
self::get_url( 'product-editor/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-product-editor', 'rtl', 'replace' );
wp_register_style(
'wc-customer-effort-score',
self::get_url( 'customer-effort-score/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-customer-effort-score', 'rtl', 'replace' );
wp_register_style(
'wc-experimental',
self::get_url( 'experimental/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-experimental', 'rtl', 'replace' );
wp_localize_script(
WC_ADMIN_APP,
'wcAdminAssets',
// Register the CSS styles.
$styles = array(
array(
'path' => plugins_url( self::get_path( 'js' ), WC_ADMIN_PLUGIN_FILE ),
'version' => $js_file_version,
)
'handle' => 'wc-admin-layout',
),
array(
'handle' => 'wc-components',
),
array(
'handle' => 'wc-block-templates',
),
array(
'handle' => 'wc-product-editor',
),
array(
'handle' => 'wc-customer-effort-score',
),
array(
'handle' => 'wc-experimental',
),
array(
'handle' => WC_ADMIN_APP,
'dependencies' => array( 'wc-components', 'wc-admin-layout', 'wc-customer-effort-score', 'wc-product-editor', 'wp-components', 'wc-experimental' ),
),
array(
'handle' => 'wc-onboarding',
),
);
wp_register_style(
WC_ADMIN_APP,
self::get_url( 'app/style', 'css' ),
array( 'wc-components', 'wc-admin-layout', 'wc-customer-effort-score', 'wc-product-editor', 'wp-components', 'wc-experimental' ),
$css_file_version
);
wp_style_add_data( WC_ADMIN_APP, 'rtl', 'replace' );
$css_file_version = self::get_file_version( 'css' );
foreach ( $styles as $style ) {
$handle = $style['handle'];
$style_path_name = isset( $scripts_map[ $handle ] ) ? $scripts_map[ $handle ] : str_replace( 'wc-', '', $handle );
wp_register_style(
'wc-onboarding',
self::get_url( 'onboarding/style', 'css' ),
array(),
$css_file_version
);
wp_style_add_data( 'wc-onboarding', 'rtl', 'replace' );
try {
$style_assets_filename = self::get_script_asset_filename( $style_path_name, 'style' );
$style_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $style_path_name . '/' . $style_assets_filename;
$version = $style_assets['version'];
} catch ( \Throwable $e ) {
// Use the default version if the asset file could not be loaded.
$version = $css_file_version;
}
$dependencies = isset( $style['dependencies'] ) ? $style['dependencies'] : array();
wp_register_style(
$handle,
self::get_url( $style_path_name . '/style', 'css' ),
$dependencies,
self::get_file_version( 'css', $version ),
);
wp_style_add_data( $handle, 'rtl', 'replace' );
}
}
/**
@@ -475,11 +472,32 @@ class WCAdminAssets {
'wc-admin-' . $script_name,
self::get_url( $script_path_name . '/' . $script_name, 'js' ),
array_merge( array( WC_ADMIN_APP ), $script_assets ['dependencies'], $dependencies ),
self::get_file_version( 'js' ),
self::get_file_version( 'js', $script_assets['version'] ),
true
);
if ( $need_translation ) {
wp_set_script_translations( 'wc-admin-' . $script_name, 'woocommerce' );
}
}
/**
* Loads a style
*
* @param string $style_path_name The style path name.
* @param string $style_name Filename of the style to load.
* @param array $dependencies Array of any extra dependencies.
*/
public static function register_style( $style_path_name, $style_name, $dependencies = array() ) {
$style_assets_filename = self::get_script_asset_filename( $style_path_name, $style_name );
$style_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_CSS_FOLDER . $style_path_name . '/' . $style_assets_filename;
$handle = 'wc-admin-' . $style_name;
wp_enqueue_style(
$handle,
self::get_url( $style_path_name . '/' . $style_name, 'css' ),
$dependencies,
self::get_file_version( 'css', $style_assets['version'] ),
);
wp_style_add_data( $handle, 'rtl', 'replace' );
}
}

View File

@@ -59,8 +59,7 @@ class WCAdminSharedSettings {
$this->settings_prefix,
function() {
return apply_filters( 'woocommerce_admin_shared_settings', array() );
},
true
}
);
}
}

View File

@@ -41,8 +41,12 @@ class WCAdminUser {
'user',
'is_super_admin',
array(
'get_callback' => function() {
return is_super_admin();
'get_callback' => function( $user ) {
if ( ! isset( $user['id'] ) || 0 === $user['id'] ) {
return false;
}
return is_super_admin( $user['id'] );
},
'schema' => null,
)

View File

@@ -31,17 +31,7 @@ class Init extends RemoteSpecsEngine {
add_filter( 'woocommerce_payment_gateways', array( __CLASS__, 'possibly_register_pre_install_wc_pay_promotion_gateway' ) );
add_filter( 'option_woocommerce_gateway_order', array( __CLASS__, 'set_gateway_top_of_list' ) );
add_filter( 'default_option_woocommerce_gateway_order', array( __CLASS__, 'set_gateway_top_of_list' ) );
$rtl = is_rtl() ? '.rtl' : '';
wp_enqueue_style(
'wc-admin-payment-method-promotions',
WCAdminAssets::get_url( "payment-method-promotions/style{$rtl}", 'css' ),
array( 'wp-components' ),
WCAdminAssets::get_file_version( 'css' )
);
WCAdminAssets::register_script( 'wp-admin-scripts', 'payment-method-promotions', true );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'load_payment_method_promotions' ) );
}
/**
@@ -161,5 +151,13 @@ class Init extends RemoteSpecsEngine {
}
return WCPayPromotionDataSourcePoller::get_instance()->get_specs_from_data_sources();
}
/**
* Loads the payment method promotions scripts and styles.
*/
public static function load_payment_method_promotions() {
WCAdminAssets::register_style( 'payment-method-promotions', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'payment-method-promotions', true );
}
}

View File

@@ -1,4 +1,5 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion;
use Automattic\WooCommerce\Admin\DataSourcePoller;
@@ -14,13 +15,13 @@ class WCPayPromotionDataSourcePoller extends DataSourcePoller {
* Default data sources array.
*/
const DATA_SOURCES = array(
'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/1.0/payment-method/promotions.json',
'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/2.0/payment-method/promotions.json',
);
/**
* Class instance.
*
* @var Analytics instance
* @var WCPayPromotionDataSourcePoller instance
*/
protected static $instance = null;

View File

@@ -0,0 +1,43 @@
<?php
namespace Automattic\WooCommerce\Internal\ComingSoon;
/**
* Adds hooks to invalidate caches when the coming soon settings are changed.
*/
class ComingSoonCacheInvalidator {
/**
* Sets up the hooks.
*
* @internal
*/
final public function init() {
add_action( 'update_option_woocommerce_coming_soon', array( $this, 'invalidate_caches' ) );
add_action( 'update_option_woocommerce_store_pages_only', array( $this, 'invalidate_caches' ) );
}
/**
* Invalidate the WordPress object cache and other known caches.
*
* @internal
*/
public function invalidate_caches() {
// Standard WordPress object cache invalidation.
wp_cache_flush();
/**
* Temporary solution to invalidate the WordPress.com Edge Cache. We can trigger
* invalidation by publishing any post. It should be refactored with a supported integration.
*/
$cart_page_id = get_option( 'woocommerce_cart_page_id' ) ?? null;
if ( $cart_page_id ) {
// Re-publish the coming soon page. Has the side-effect of invalidating the Edge Cache.
wp_update_post(
array(
'ID' => $cart_page_id,
'post_status' => 'publish',
)
);
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Automattic\WooCommerce\Internal\ComingSoon;
use Automattic\WooCommerce\Admin\WCAdminHelper;
/**
* Provides helper methods for coming soon functionality.
*/
class ComingSoonHelper {
/**
* Returns true when the entire site is live.
*/
public function is_site_live(): bool {
return 'yes' !== get_option( 'woocommerce_coming_soon' );
}
/**
* Returns true when the entire site is coming soon mode.
*/
public function is_site_coming_soon(): bool {
return 'yes' === get_option( 'woocommerce_coming_soon' ) && 'yes' !== get_option( 'woocommerce_store_pages_only' );
}
/**
* Returns true when only the store pages are in coming soon mode.
*/
public function is_store_coming_soon(): bool {
return 'yes' === get_option( 'woocommerce_coming_soon' ) && 'yes' === get_option( 'woocommerce_store_pages_only' );
}
/**
* Returns true when the provided URL is behind a coming soon screen.
*
* @param string $url The URL to check.
*/
public function is_url_coming_soon( string $url ): bool {
// Early exit if coming soon mode not active.
if ( $this->is_site_live() ) {
return false;
}
if ( $this->is_site_coming_soon() ) {
return true;
}
// Check the URL is a store page when in "store coming soon" mode.
if ( $this->is_store_coming_soon() && WCAdminHelper::is_store_page( $url ) ) {
return true;
}
// Default to false.
return false;
}
/**
* Builds the relative URL from the WP instance.
*
* @internal
* @link https://wordpress.stackexchange.com/a/274572
* @param \WP $wp WordPress environment instance.
*/
public function get_url_from_wp( \WP $wp ) {
// Special case for plain permalinks.
if ( empty( get_option( 'permalink_structure' ) ) ) {
return '/' . add_query_arg( $wp->query_vars, $wp->request );
}
return trailingslashit( '/' . $wp->request );
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Automattic\WooCommerce\Internal\ComingSoon;
use Automattic\WooCommerce\Admin\Features\Features;
/**
* Handles the parse_request hook to determine whether the current page needs
* to be replaced with a comiing soon screen.
*/
class ComingSoonRequestHandler {
/**
* Coming soon helper.
*
* @var ComingSoonHelper
*/
private $coming_soon_helper = null;
/**
* Sets up the hook.
*
* @internal
*
* @param ComingSoonHelper $coming_soon_helper Dependency.
*/
final public function init( ComingSoonHelper $coming_soon_helper ) {
$this->coming_soon_helper = $coming_soon_helper;
add_filter( 'template_include', array( $this, 'handle_template_include' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'deregister_unnecessary_styles' ), 100 );
}
/**
* Deregisters unnecessary styles for the coming soon page.
*
* @return void
*/
public function deregister_unnecessary_styles() {
global $wp;
if ( ! $this->should_show_coming_soon( $wp ) ) {
return;
}
if ( $this->coming_soon_helper->is_site_coming_soon() ) {
global $wp_styles;
foreach ( $wp_styles->registered as $handle => $registered_style ) {
// Deregister all styles except for block styles.
if (
strpos( $handle, 'wp-block' ) !== 0 &&
strpos( $handle, 'core-block' ) !== 0
) {
wp_deregister_style( $handle );
}
}
}
}
/**
* Replaces the page template with a 'coming soon' when the site is in coming soon mode.
*
* @internal
*
* @param string $template The path to the previously determined template.
* @return string|null The path to the 'coming soon' template or null to prevent further template loading in FSE themes.
*/
public function handle_template_include( $template ) {
global $wp;
if ( ! $this->should_show_coming_soon( $wp ) ) {
return $template;
}
// A coming soon page needs to be displayed. Don't cache this response.
nocache_headers();
// Optimize search engine by returning 503 status code and set retry-after header to 12 hours.
status_header( 503 );
header( 'Retry-After: ' . 12 * HOUR_IN_SECONDS );
add_theme_support( 'block-templates' );
wp_dequeue_style( 'global-styles' );
$coming_soon_template = get_query_template( 'coming-soon' );
if ( ! wc_current_theme_is_fse_theme() && $this->coming_soon_helper->is_store_coming_soon() ) {
get_header();
}
include $coming_soon_template;
if ( ! wc_current_theme_is_fse_theme() && $this->coming_soon_helper->is_store_coming_soon() ) {
get_footer();
}
if ( wc_current_theme_is_fse_theme() ) {
// Since we've already rendered a template, return null to ensure no other template is rendered.
return null;
} else {
// In non-FSE themes, other templates will still be rendered.
// We need to exit to prevent further processing.
exit();
}
}
/**
* Determines whether the coming soon screen should be shown.
*
* @param \WP $wp Current WordPress environment instance.
*
* @return bool
*/
private function should_show_coming_soon( \WP &$wp ) {
// Early exit if LYS feature is disabled.
if ( ! Features::is_enabled( 'launch-your-store' ) ) {
return false;
}
// Early exit if the user is logged in as administrator / shop manager.
if ( current_user_can( 'manage_woocommerce' ) ) {
return false;
}
// Do not show coming soon on 404 pages when restrict to store pages only.
if ( $this->coming_soon_helper->is_store_coming_soon() && is_404() ) {
return false;
}
// Early exit if the URL doesn't need a coming soon screen.
$url = $this->coming_soon_helper->get_url_from_wp( $wp );
if ( ! $this->coming_soon_helper->is_url_coming_soon( $url ) ) {
return false;
}
// Exclude users with a private link.
if ( isset( $_GET['woo-share'] ) && get_option( 'woocommerce_share_key' ) === $_GET['woo-share'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
// Persist the share link with a cookie for 90 days.
setcookie( 'woo-share', sanitize_text_field( wp_unslash( $_GET['woo-share'] ) ), time() + 60 * 60 * 24 * 90, '/' ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
return false;
}
if ( isset( $_COOKIE['woo-share'] ) && get_option( 'woocommerce_share_key' ) === $_COOKIE['woo-share'] ) {
return false;
}
return true;
}
}

View File

@@ -232,4 +232,37 @@ abstract class CustomMetaDataStore {
return $meta;
}
/**
* Returns distinct meta keys in use.
*
* @since 8.8.0
*
* @param int $limit Maximum number of meta keys to return. Defaults to 100.
* @param string $order Order to use for the results. Either 'ASC' or 'DESC'. Defaults to 'ASC'.
* @param bool $include_private Whether to include private meta keys in the results. Defaults to FALSE.
* @return string[]
*/
public function get_meta_keys( $limit = 100, $order = 'ASC', $include_private = false ) {
global $wpdb;
$db_info = $this->get_db_info();
$query = "SELECT DISTINCT meta_key FROM {$db_info['table']} ";
if ( ! $include_private ) {
$query .= $wpdb->prepare( 'WHERE meta_key != \'\' AND meta_key NOT LIKE %s ', $wpdb->esc_like( '_' ) . '%' );
} else {
$query .= "WHERE meta_key != '' ";
}
$order = in_array( strtoupper( $order ), array( 'ASC', 'DESC' ), true ) ? $order : 'ASC';
$query .= 'ORDER BY meta_key ' . $order . ' ';
if ( $limit ) {
$query .= $wpdb->prepare( 'LIMIT %d ', $limit );
}
return $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared.
}
}

View File

@@ -311,6 +311,10 @@ class CustomOrdersTableController {
return $value;
}
if ( $old_value === $value ) {
return $value;
}
$this->order_cache->flush();
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
$this->data_synchronizer->create_database_tables();
@@ -433,7 +437,7 @@ class CustomOrdersTableController {
return array();
}
$get_value = function() {
$get_value = function () {
return $this->custom_orders_table_usage_is_enabled() ? 'yes' : 'no';
};
@@ -442,18 +446,20 @@ class CustomOrdersTableController {
* gets called while it's still being instantiated and creates and endless loop.
*/
$get_desc = function() {
$get_desc = function () {
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
return $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_compatibility );
};
$get_disabled = function() {
$get_disabled = function () {
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$sync_complete = 0 === $this->get_orders_pending_sync_count();
$disabled = array();
// Changing something here? might also want to look at `enable|disable` functions in CLIRunner.
if ( count( array_merge( $plugin_compatibility['uncertain'], $plugin_compatibility['incompatible'] ) ) > 0 ) {
$incompatible_plugins = array_merge( $plugin_compatibility['uncertain'], $plugin_compatibility['incompatible'] );
$incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() );
if ( count( $incompatible_plugins ) > 0 ) {
$disabled = array( 'yes' );
}
if ( ! $sync_complete && ! $this->changing_data_source_with_sync_pending_is_allowed() ) {
@@ -489,11 +495,11 @@ class CustomOrdersTableController {
return array();
}
$get_value = function() {
$get_value = function () {
return get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
};
$get_sync_message = function() {
$get_sync_message = function () {
$orders_pending_sync_count = $this->get_orders_pending_sync_count();
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
$sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
@@ -572,7 +578,7 @@ class CustomOrdersTableController {
return implode( '<br />', $sync_message );
};
$get_description_is_error = function() {
$get_description_is_error = function () {
$sync_is_pending = $this->get_orders_pending_sync_count() > 0;
return $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed();

View File

@@ -105,6 +105,7 @@ class DataSynchronizer implements BatchProcessorInterface {
* Class constructor.
*/
public function __construct() {
self::add_filter( 'pre_delete_post', array( $this, 'maybe_prevent_deletion_of_post' ), 10, 2 );
self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 );
self::add_action( 'woocommerce_new_order', array( $this, 'handle_updated_order' ), 100 );
self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 );
@@ -441,7 +442,7 @@ class DataSynchronizer implements BatchProcessorInterface {
*
* @return int
*/
public function get_current_orders_pending_sync_count_cached() : int {
public function get_current_orders_pending_sync_count_cached(): int {
return $this->get_current_orders_pending_sync_count( true );
}
@@ -482,14 +483,14 @@ class DataSynchronizer implements BatchProcessorInterface {
return 0;
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQL.NotPrepared --
// -- $order_post_type_placeholder, $orders_table, self::PLACEHOLDER_ORDER_POST_TYPE are all safe to use in queries.
if ( ! $this->get_table_exists() ) {
$count = $wpdb->get_var(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
$wpdb->prepare(
"SELECT COUNT(*) FROM $wpdb->posts where post_type in ( $order_post_type_placeholder )",
$order_post_types
)
// phpcs:enable
);
return $count;
}
@@ -498,30 +499,28 @@ class DataSynchronizer implements BatchProcessorInterface {
$missing_orders_count_sql = $wpdb->prepare(
"
SELECT COUNT(1) FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
AND orders.status not in ( 'auto-draft' )
RIGHT JOIN $orders_table orders ON posts.ID=orders.id
WHERE (posts.post_type IS NULL OR posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "')
AND orders.status NOT IN ( 'auto-draft' )
AND orders.type IN ($order_post_type_placeholder)",
$order_post_types
);
$operator = '>';
} else {
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
$missing_orders_count_sql = $wpdb->prepare(
"
SELECT COUNT(1) FROM $wpdb->posts posts
LEFT JOIN $orders_table orders ON posts.id=orders.id
LEFT JOIN $orders_table orders ON posts.ID=orders.id
WHERE
posts.post_type in ($order_post_type_placeholder)
AND posts.post_status != 'auto-draft'
AND orders.id IS NULL",
$order_post_types
);
// phpcs:enable
$operator = '<';
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $missing_orders_count_sql is prepared.
$sql = $wpdb->prepare(
"
SELECT(
@@ -603,10 +602,9 @@ SELECT(
$order_post_types = wc_get_order_types( 'cot-migration' );
$order_post_type_placeholders = implode( ', ', array_fill( 0, count( $order_post_types ), '%s' ) );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQL.NotPrepared
switch ( $type ) {
case self::ID_TYPE_MISSING_IN_ORDERS_TABLE:
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared.
$sql = $wpdb->prepare(
"
SELECT posts.ID FROM $wpdb->posts posts
@@ -618,23 +616,22 @@ WHERE
ORDER BY posts.ID ASC",
$order_post_types
);
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
break;
case self::ID_TYPE_MISSING_IN_POSTS_TABLE:
$sql = $wpdb->prepare(
"
SELECT posts.ID FROM $wpdb->posts posts
INNER JOIN $orders_table orders ON posts.id=orders.id
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
AND orders.status not in ( 'auto-draft' )
SELECT orders.id FROM $wpdb->posts posts
RIGHT JOIN $orders_table orders ON posts.ID=orders.id
WHERE (posts.post_type IS NULL OR posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "')
AND orders.status NOT IN ( 'auto-draft' )
AND orders.type IN ($order_post_type_placeholders)
ORDER BY posts.id ASC",
ORDER BY posts.ID ASC",
$order_post_types
);
break;
case self::ID_TYPE_DIFFERENT_UPDATE_DATE:
$operator = $this->custom_orders_table_is_authoritative() ? '>' : '<';
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholders is prepared.
$sql = $wpdb->prepare(
"
SELECT orders.id FROM $orders_table orders
@@ -646,7 +643,6 @@ ORDER BY orders.id ASC
",
$order_post_types
);
// phpcs:enable
break;
case self::ID_TYPE_DELETED_FROM_ORDERS_TABLE:
return $this->get_deleted_order_ids( true, $limit );
@@ -655,7 +651,7 @@ ORDER BY orders.id ASC
default:
throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' );
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:enable
// phpcs:ignore WordPress.DB
return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) );
@@ -700,7 +696,7 @@ ORDER BY orders.id ASC
*
* @param array $batch Batch details.
*/
public function process_batch( array $batch ) : void {
public function process_batch( array $batch ): void {
if ( empty( $batch ) ) {
return;
}
@@ -901,6 +897,26 @@ ORDER BY orders.id ASC
return 'Synchronizes orders between posts and custom order tables.';
}
/**
* Prevents deletion of order backup posts (regardless of sync setting) when HPOS is authoritative and the order
* still exists in HPOS.
* This should help with edge cases where wp_delete_post() would delete the HPOS record too or backfill would sync
* incorrect data from an order with no metadata from the posts table.
*
* @since 8.8.0
*
* @param WP_Post|false|null $delete Whether to go forward with deletion.
* @param WP_Post $post Post object.
* @return WP_Post|false|null
*/
private function maybe_prevent_deletion_of_post( $delete, $post ) {
if ( self::PLACEHOLDER_ORDER_POST_TYPE !== $post->post_type && $this->custom_orders_table_is_authoritative() && $this->data_store->order_exists( $post->ID ) ) {
$delete = false;
}
return $delete;
}
/**
* Handle the 'deleted_post' action.
*

View File

@@ -6,7 +6,9 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use WC_Abstract_Order;
defined( 'ABSPATH' ) || exit;
@@ -57,7 +59,7 @@ class LegacyDataHandler {
* @param array $order_ids If provided, total is computed only among IDs in this array, which can be either individual IDs or ranges like "100-200".
* @return int Number of orders.
*/
public function count_orders_for_cleanup( $order_ids = array() ) : int {
public function count_orders_for_cleanup( $order_ids = array() ): int {
global $wpdb;
return (int) $wpdb->get_var( $this->build_sql_query_for_cleanup( $order_ids, 'count' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepared in build_sql_query_for_cleanup().
}
@@ -151,21 +153,26 @@ class LegacyDataHandler {
public function cleanup_post_data( int $order_id, bool $skip_checks = false ): void {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
// translators: %d is an order ID.
throw new \Exception( sprintf( __( '%d is not a valid order ID.', 'woocommerce' ), $order_id ) );
}
$post_is_placeholder = get_post_type( $order_id ) === $this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE;
if ( ! $post_is_placeholder ) {
$order = wc_get_order( $order_id );
if ( ! $skip_checks && ! $this->is_order_newer_than_post( $order ) ) {
throw new \Exception( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables.', 'woocommerce' ) ) );
if ( ! $order ) {
// translators: %d is an order ID.
throw new \Exception( esc_html( sprintf( __( '%d is not a valid order ID.', 'woocommerce' ), $order_id ) ) );
}
if ( ! $skip_checks && ! $this->is_order_newer_than_post( $order ) ) {
// translators: %1 is an order ID.
throw new \Exception( esc_html( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables. Compare order data with `wp wc hpos diff %1$d` and use `wp wc hpos backfill %1$d --from=posts --to=hpos` to fix.', 'woocommerce' ), $order_id ) ) );
}
}
// Delete all metadata.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->postmeta} WHERE post_id = %d",
$order->get_id()
$order_id
)
);
@@ -176,11 +183,11 @@ class LegacyDataHandler {
"UPDATE {$wpdb->posts} SET post_type = %s, post_status = %s WHERE ID = %d",
$this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE,
'draft',
$order->get_id()
$order_id
)
);
clean_post_cache( $order->get_id() );
clean_post_cache( $order_id );
}
/**
@@ -192,7 +199,7 @@ class LegacyDataHandler {
*/
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' ) );
throw new \Exception( esc_html__( 'Order is not an HPOS order.', 'woocommerce' ) );
}
$post = get_post( $order->get_id() );
@@ -251,7 +258,7 @@ class LegacyDataHandler {
$val2 = get_post_meta( $order_id, $key, true );
}
if ( $val1 != $val2 ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
if ( $val1 != $val2 ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison,Universal.Operators.StrictComparisons.LooseNotEqual
$diff[ $key ] = array( $val1, $val2 );
}
}
@@ -283,7 +290,7 @@ class LegacyDataHandler {
if ( ! $order_type ) {
// translators: %d is an order ID.
throw new \Exception( sprintf( __( '%d is not an order or has an invalid order type.', 'woocommerce' ), $order_id ) );
throw new \Exception( esc_html( sprintf( __( '%d is not an order or has an invalid order type.', 'woocommerce' ), $order_id ) ) );
}
$classname = $order_type['class_name'];
@@ -321,27 +328,85 @@ class LegacyDataHandler {
* @param int $order_id Order ID.
* @param string $source_data_store Datastore to use as source. Should be either 'hpos' or 'posts'.
* @param string $destination_data_store Datastore to use as destination. Should be either 'hpos' or 'posts'.
* @param array $fields List of metakeys or order properties to limit the backfill to.
* @return void
* @throws \Exception When an error occurs.
*/
public function backfill_order_to_datastore( int $order_id, string $source_data_store, string $destination_data_store ) {
public function backfill_order_to_datastore( int $order_id, string $source_data_store, string $destination_data_store, array $fields = array() ) {
$valid_data_stores = array( 'posts', 'hpos' );
if ( ! in_array( $source_data_store, $valid_data_stores, true ) || ! in_array( $destination_data_store, $valid_data_stores, true ) || $destination_data_store === $source_data_store ) {
throw new \Exception( sprintf( 'Invalid datastore arguments: %1$s -> %2$s.', $source_data_store, $destination_data_store ) );
throw new \Exception( esc_html( sprintf( 'Invalid datastore arguments: %1$s -> %2$s.', $source_data_store, $destination_data_store ) ) );
}
$order = $this->get_order_from_datastore( $order_id, $source_data_store );
$fields = array_filter( $fields );
$src_order = $this->get_order_from_datastore( $order_id, $source_data_store );
switch ( $destination_data_store ) {
case 'posts':
$order->get_data_store()->backfill_post_record( $order );
break;
case 'hpos':
$this->posts_to_cot_migrator->migrate_orders( array( $order_id ) );
break;
default:
break;
// Backfill entire orders.
if ( ! $fields ) {
if ( 'posts' === $destination_data_store ) {
$src_order->get_data_store()->backfill_post_record( $src_order );
} elseif ( 'hpos' === $destination_data_store ) {
$this->posts_to_cot_migrator->migrate_orders( array( $src_order->get_id() ) );
}
return;
}
$this->validate_backfill_fields( $fields, $src_order );
$dest_order = $this->get_order_from_datastore( $src_order->get_id(), $destination_data_store );
if ( 'posts' === $destination_data_store ) {
$datastore = $this->data_store->get_cpt_data_store_instance();
} elseif ( 'hpos' === $destination_data_store ) {
$datastore = $this->data_store;
}
if ( ! $datastore || ! method_exists( $datastore, 'update_order_from_object' ) ) {
throw new \Exception( esc_html__( 'The backup datastore does not support updating orders.', 'woocommerce' ) );
}
// Backfill meta.
if ( ! empty( $fields['meta_keys'] ) ) {
foreach ( $fields['meta_keys'] as $meta_key ) {
$dest_order->delete_meta_data( $meta_key );
foreach ( $src_order->get_meta( $meta_key, false, 'edit' ) as $meta ) {
$dest_order->add_meta_data( $meta_key, $meta->value );
}
}
}
// Backfill props.
if ( ! empty( $fields['props'] ) ) {
$new_values = array_combine(
$fields['props'],
array_map(
fn( $prop_name ) => $src_order->{"get_{$prop_name}"}(),
$fields['props']
)
);
$dest_order->set_props( $new_values );
if ( 'hpos' === $destination_data_store ) {
$dest_order->apply_changes();
$limit_cb = function ( $rows, $order ) use ( $dest_order, $fields ) {
if ( $dest_order->get_id() === $order->get_id() ) {
$rows = $this->limit_hpos_update_to_props( $rows, $fields['props'] );
}
return $rows;
};
add_filter( 'woocommerce_orders_table_datastore_db_rows_for_order', $limit_cb, 10, 2 );
}
}
$datastore->update_order_from_object( $dest_order );
if ( 'hpos' === $destination_data_store && isset( $limit_cb ) ) {
remove_filter( 'woocommerce_orders_table_datastore_db_rows_for_order', $limit_cb );
}
}
@@ -372,13 +437,121 @@ class LegacyDataHandler {
* @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'
);
$base_props = array();
foreach ( $this->data_store->get_all_order_column_mappings() as $mapping ) {
$base_props = array_merge( $base_props, array_column( $mapping, 'name' ) );
}
return $base_props;
}
/**
* Filters a set of HPOS row updates to those matching a specific set of order properties.
* Called via the `woocommerce_orders_table_datastore_db_rows_for_order` filter in `backfill_order_to_datastore`.
*
* @param array $rows Details for the db update.
* @param string[] $props Order property names.
* @return array
* @see OrdersTableDataStore::get_db_rows_for_order()
*/
private function limit_hpos_update_to_props( array $rows, array $props ) {
// Determine HPOS columns corresponding to the props in the $props array.
$allowed_columns = array();
foreach ( $this->data_store->get_all_order_column_mappings() as &$mapping ) {
foreach ( $mapping as $column_name => &$column_data ) {
if ( ! isset( $column_data['name'] ) || ! in_array( $column_data['name'], $props, true ) ) {
continue;
}
$allowed_columns[ $column_data['name'] ] = $column_name;
}
}
foreach ( $rows as $i => &$db_update ) {
// Prevent accidental update of another prop by limiting columns to explicitly requested props.
if ( ! array_intersect_key( $db_update['data'], array_flip( $allowed_columns ) ) ) {
unset( $rows[ $i ] );
continue;
}
$allowed_column_names_with_ids = array_merge(
$allowed_columns,
array( 'id', 'order_id', 'address_type' )
);
$db_update['data'] = array_intersect_key( $db_update['data'], array_flip( $allowed_column_names_with_ids ) );
$db_update['format'] = array_intersect_key( $db_update['format'], array_flip( $allowed_column_names_with_ids ) );
}
return $rows;
}
/**
* Validates meta_keys and property names for a partial order backfill.
*
* @param array $fields An array possibly having entries with index 'meta_keys' and/or 'props',
* corresponding to an array of order meta keys and/or order properties.
* @param \WC_Abstract_Order $order The order being validated.
* @throws \Exception When a validation error occurs.
* @return void
*/
private function validate_backfill_fields( array $fields, \WC_Abstract_Order $order ) {
if ( ! $fields ) {
return;
}
if ( ! empty( $fields['meta_keys'] ) ) {
$internal_meta_keys = array_unique(
array_merge(
$this->data_store->get_internal_meta_keys(),
$this->data_store->get_cpt_data_store_instance()->get_internal_meta_keys()
)
);
$possibly_internal_keys = array_intersect( $internal_meta_keys, $fields['meta_keys'] );
if ( ! empty( $possibly_internal_keys ) ) {
throw new \Exception(
esc_html(
sprintf(
// translators: %s is a comma separated list of metakey names.
_n(
'%s is an internal meta key. Use --props to set it.',
'%s are internal meta keys. Use --props to set them.',
count( $possibly_internal_keys ),
'woocommerce'
),
implode( ', ', $possibly_internal_keys )
)
)
);
}
}
if ( ! empty( $fields['props'] ) ) {
$invalid_props = array_filter(
$fields['props'],
function ( $prop_name ) use ( $order ) {
return ! method_exists( $order, "get_{$prop_name}" );
}
);
if ( ! empty( $invalid_props ) ) {
throw new \Exception(
esc_html(
sprintf(
// translators: %s is a list of order property names.
_n(
'%s is not a valid order property.',
'%s are not valid order properties.',
count( $invalid_props ),
'woocommerce'
),
implode( ', ', $invalid_props )
)
)
);
}
}
}
}

View File

@@ -531,7 +531,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
*
* @return string Alias.
*/
private function get_order_table_alias() : string {
private function get_order_table_alias(): string {
return 'o';
}
@@ -540,7 +540,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
*
* @return string Alias.
*/
private function get_op_table_alias() : string {
private function get_op_table_alias(): string {
return 'p';
}
@@ -551,7 +551,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
*
* @return string Alias.
*/
private function get_address_table_alias( string $type ) : string {
private function get_address_table_alias( string $type ): string {
return 'billing' === $type ? 'b' : 's';
}
@@ -589,11 +589,24 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
}
self::$backfilling_order_ids[] = $order->get_id();
// Attempt to create the backup post if missing.
if ( $order->get_id() && is_null( get_post( $order->get_id() ) ) ) {
if ( ! $this->maybe_create_backup_post( $order, 'backfill' ) ) {
// translators: %d is an order ID.
$this->error_logger->warning( sprintf( __( 'Unable to create backup post for order %d.', 'woocommerce' ), $order->get_id() ) );
return;
}
}
$this->update_order_meta_from_object( $order );
$order_class = get_class( $order );
$post_order = new $order_class();
$post_order->set_id( $order->get_id() );
$cpt_data_store->read( $post_order );
if ( $cpt_data_store->order_exists( $order->get_id() ) ) {
$cpt_data_store->read( $post_order );
}
// This compares the order data to the post data and set changes array for props that are changed.
$post_order->set_props( $order->get_data() );
@@ -629,6 +642,41 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
do_action( 'woocommerce_hpos_post_record_backfilled', $order );
}
/**
* Updates an order (in this datastore) from another order object.
*
* @param \WC_Abstract_Order $order Source order.
* @return bool Whether the order was updated.
*/
public function update_order_from_object( $order ) {
$hpos_order = new \WC_Order();
$hpos_order->set_id( $order->get_id() );
$this->read( $hpos_order );
$hpos_order->set_props( $order->get_data() );
// Meta keys.
foreach ( $hpos_order->get_meta_data() as &$meta ) {
$hpos_order->delete_meta_data( $meta->key );
}
foreach ( $order->get_meta_data() as &$meta ) {
$hpos_order->add_meta_data( $meta->key, $meta->value );
}
add_filter( 'woocommerce_orders_table_datastore_should_save_after_meta_change', '__return_false' );
$hpos_order->save_meta_data();
remove_filter( 'woocommerce_orders_table_datastore_should_save_after_meta_change', '__return_false' );
$db_rows = $this->get_db_rows_for_order( $hpos_order, 'update', true );
foreach ( $db_rows as $db_update ) {
ksort( $db_update['data'] );
ksort( $db_update['format'] );
$this->database_util->insert_on_duplicate_key_update( $db_update['table'], $db_update['data'], array_values( $db_update['format'] ) );
}
return true;
}
/**
* Get information about whether permissions are granted yet.
*
@@ -1105,7 +1153,7 @@ WHERE
* @param int $order_id The order id to check.
* @return bool True if an order exists with the given name.
*/
public function order_exists( $order_id ) : bool {
public function order_exists( $order_id ): bool {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@@ -1144,7 +1192,7 @@ WHERE
$data = $this->get_order_data_for_ids( $order_ids );
if ( count( $data ) !== count( $order_ids ) ) {
throw new \Exception( __( 'Invalid order IDs in call to read_multiple()', 'woocommerce' ) );
throw new \Exception( esc_html__( 'Invalid order IDs in call to read_multiple()', 'woocommerce' ) );
}
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
@@ -1188,7 +1236,7 @@ WHERE
*
* @return bool Whether the order should be synced.
*/
private function should_sync_order( \WC_Abstract_Order $order ) : bool {
private function should_sync_order( \WC_Abstract_Order $order ): bool {
$draft_order = in_array( $order->get_status(), array( 'draft', 'auto-draft' ), true );
$already_synced = in_array( $order->get_id(), self::$reading_order_ids, true );
return ! $draft_order && ! $already_synced;
@@ -1225,7 +1273,7 @@ WHERE
*
* @return array Filtered meta data.
*/
public function filter_raw_meta_data( &$object, $raw_meta_data ) {
public function filter_raw_meta_data( &$object, $raw_meta_data ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
$filtered_meta_data = parent::filter_raw_meta_data( $object, $raw_meta_data );
$allowed_keys = array(
'_billing_address_index',
@@ -1233,7 +1281,7 @@ WHERE
);
$allowed_meta = array_filter(
$raw_meta_data,
function( $meta ) use ( $allowed_keys ) {
function ( $meta ) use ( $allowed_keys ) {
return in_array( $meta->meta_key, $allowed_keys, true );
}
);
@@ -1583,15 +1631,7 @@ WHERE
);
// phpcs:enable
$meta_data_query = $this->get_order_meta_select_statement();
$order_data = array();
$meta_data = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_data_query and $order_meta_table is autogenerated and should already be prepared. $id_placeholder is already prepared.
"$meta_data_query WHERE $order_meta_table.order_id in ( $id_placeholder )",
$ids
)
);
$order_data = array();
foreach ( $table_data as $table_datum ) {
$id = $table_datum->{"{$order_table_alias}_id"};
@@ -1615,14 +1655,27 @@ WHERE
$order_data[ $id ]->meta_data = array();
}
foreach ( $meta_data as $meta_datum ) {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Not a meta query.
$order_data[ $meta_datum->order_id ]->meta_data[] = (object) array(
'meta_id' => $meta_datum->id,
'meta_key' => $meta_datum->meta_key,
'meta_value' => $meta_datum->meta_value,
if ( count( $order_data ) > 0 ) {
$meta_order_ids = array_keys( $order_data );
$meta_order_id_placeholder = implode( ', ', array_fill( 0, count( $meta_order_ids ), '%d' ) );
$meta_data_query = $this->get_order_meta_select_statement();
$meta_data = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_data_query and $order_meta_table is autogenerated and should already be prepared. $id_placeholder is already prepared.
"$meta_data_query WHERE $order_meta_table.order_id in ( $meta_order_id_placeholder )",
$ids
)
);
// phpcs:enable
foreach ( $meta_data as $meta_datum ) {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Not a meta query.
$order_data[ $meta_datum->order_id ]->meta_data[] = (object) array(
'meta_id' => $meta_datum->id,
'meta_key' => $meta_datum->meta_key,
'meta_value' => $meta_datum->meta_value,
);
// phpcs:enable
}
}
return $order_data;
}
@@ -1787,22 +1840,12 @@ FROM $order_meta_table
* @since 6.8.0
*/
protected function persist_order_to_db( &$order, bool $force_all_fields = false ) {
$context = ( 0 === absint( $order->get_id() ) ) ? 'create' : 'update';
$data_sync = wc_get_container()->get( DataSynchronizer::class );
$context = ( 0 === absint( $order->get_id() ) ) ? 'create' : 'update';
if ( 'create' === $context ) {
$post_id = wp_insert_post(
array(
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
'post_status' => 'draft',
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
)
);
$post_id = $this->maybe_create_backup_post( $order, 'create' );
if ( ! $post_id ) {
throw new \Exception( __( 'Could not create order in posts table.', 'woocommerce' ) );
throw new \Exception( esc_html__( 'Could not create order in posts table.', 'woocommerce' ) );
}
$order->set_id( $post_id );
@@ -1826,7 +1869,7 @@ FROM $order_meta_table
if ( false === $result ) {
// translators: %s is a table name.
throw new \Exception( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) );
throw new \Exception( esc_html( sprintf( __( 'Could not persist order to database table "%s".', 'woocommerce' ), $update['table'] ) ) );
}
}
@@ -1836,6 +1879,37 @@ FROM $order_meta_table
$this->set_custom_taxonomies( $order, $default_taxonomies );
}
/**
* Takes care of creating the backup post in the posts table (placeholder or actual order post, depending on sync settings).
*
* @since 8.8.0
*
* @param \WC_Abstract_Order $order The order.
* @param string $context The context: either 'create' or 'backfill'.
* @return int The new post ID.
*/
protected function maybe_create_backup_post( &$order, string $context ): int {
$data_sync = wc_get_container()->get( DataSynchronizer::class );
$data = array(
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
'post_status' => 'draft',
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
);
if ( 'backfill' === $context ) {
if ( ! $order->get_id() ) {
return 0;
}
$data['import_id'] = $order->get_id();
}
return wp_insert_post( $data );
}
/**
* Set default taxonomies for the order.
*
@@ -1999,7 +2073,24 @@ FROM $order_meta_table
*/
$ext_rows = apply_filters( 'woocommerce_orders_table_datastore_extra_db_rows_for_order', array(), $order, $context );
return array_merge( $result, $ext_rows );
/**
* Filters the rows that are going to be inserted or updated during an order save.
*
* @since 8.8.0
* @internal Use 'woocommerce_orders_table_datastore_extra_db_rows_for_order' for adding rows to the database save.
*
* @param array $rows Array of rows to be inserted/updated. See 'woocommerce_orders_table_datastore_extra_db_rows_for_order' for exact format.
* @param \WC_Order $order The order object.
* @param string $context The context of the operation: 'create' or 'update'.
*/
$result = apply_filters(
'woocommerce_orders_table_datastore_db_rows_for_order',
array_merge( $result, $ext_rows ),
$order,
$context
);
return $result;
}
/**
@@ -2112,7 +2203,7 @@ FROM $order_meta_table
/**
* Fires immediately after an order is deleted.
*
* @since
* @since 2.7.0
*
* @param int $order_id ID of the order that has been deleted.
*/
@@ -2137,7 +2228,7 @@ FROM $order_meta_table
/**
* Fires immediately after an order is trashed.
*
* @since
* @since 2.7.0
*
* @param int $order_id ID of the order that has been trashed.
*/
@@ -2198,7 +2289,7 @@ FROM $order_meta_table
*
* @return void
*/
private function upshift_or_delete_child_orders( $order ) : void {
private function upshift_or_delete_child_orders( $order ): void {
global $wpdb;
$order_table = self::get_orders_table_name();
@@ -2703,7 +2794,6 @@ FROM $order_meta_table
if ( $save ) {
$order->save_meta_data();
}
}
/**
@@ -2871,7 +2961,7 @@ CREATE TABLE $meta_table (
* @param WC_Data $object WC_Data object.
* @return array
*/
public function read_meta( &$object ) {
public function read_meta( &$object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
$raw_meta_data = $this->data_store_meta->read_meta( $object );
return $this->filter_raw_meta_data( $object, $raw_meta_data );
}
@@ -2884,7 +2974,7 @@ CREATE TABLE $meta_table (
*
* @return bool
*/
public function delete_meta( &$object, $meta ) {
public function delete_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
global $wpdb;
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
@@ -2932,7 +3022,7 @@ CREATE TABLE $meta_table (
*
* @return int|bool meta ID or false on failure
*/
public function add_meta( &$object, $meta ) {
public function add_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
$meta->id = $add_meta;
$changes_applied = $this->after_meta_change( $object, $meta );
@@ -2954,7 +3044,7 @@ CREATE TABLE $meta_table (
*
* @return bool The number of rows updated, or false on error.
*/
public function update_meta( &$object, $meta ) {
public function update_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
$changes_applied = $this->after_meta_change( $object, $meta );
@@ -3007,6 +3097,17 @@ CREATE TABLE $meta_table (
$current_time = $this->legacy_proxy->call_function( 'current_time', 'mysql', 1 );
$current_date_time = new \WC_DateTime( $current_time, new \DateTimeZone( 'GMT' ) );
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $this->ephemeral_meta_keys, true ) );
$should_save =
$order->get_date_modified() < $current_date_time && empty( $order->get_changes() )
&& ( ! is_object( $meta ) || ! in_array( $meta->key, $this->ephemeral_meta_keys, true ) );
/**
* Allows code to skip a full order save() when metadata is changed.
*
* @since 8.8.0
*
* @param bool $should_save Whether to trigger a full save after metadata is changed.
*/
return apply_filters( 'woocommerce_orders_table_datastore_should_save_after_meta_change', $should_save );
}
}

View File

@@ -27,7 +27,7 @@ class OrdersTableSearchQuery {
/**
* Limits the search to a specific field.
*
* @var string
* @var string[]
*/
private $search_filters;
@@ -40,8 +40,8 @@ class OrdersTableSearchQuery {
*/
public function __construct( OrdersTableQuery $query ) {
$this->query = $query;
$this->search_term = urldecode( $query->get( 's' ) );
$this->search_filters = $this->sanitize_search_filters( urldecode( $query->get( 'search_filter' ) ) );
$this->search_term = $query->get( 's' );
$this->search_filters = $this->sanitize_search_filters( $query->get( 'search_filter' ) ?? '' );
}
/**
@@ -52,17 +52,18 @@ class OrdersTableSearchQuery {
* @return array Array of search filters.
*/
private function sanitize_search_filters( string $search_filter ) : array {
$available_filters = array(
$core_filters = array(
'order_id',
'transaction_id',
'customer_email',
'customers', // customers also searches in meta.
'products',
);
if ( 'all' === $search_filter || '' === $search_filter ) {
return $available_filters;
return $core_filters;
} else {
return array_intersect( $available_filters, array( $search_filter ) );
return array( $search_filter );
}
}
@@ -119,7 +120,29 @@ class OrdersTableSearchQuery {
LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
";
}
return '';
/**
* Filter to support adding a custom order search filter.
* Provide a JOIN clause for a new search filter. This should be used along with `woocommerce_hpos_admin_search_filters`
* to declare a new custom filter, and `woocommerce_hpos_generate_where_for_search_filter` to generate the WHERE
* clause.
*
* Hardcoded JOINS (products) cannot be modified using this filter for consistency.
*
* @since 8.9.0
*
* @param string $join The JOIN clause.
* @param string $search_term The search term.
* @param string $search_filter The search filter. Use this to bail early if this is not filter you are interested in.
* @param OrdersTableQuery $query The order query object.
*/
return apply_filters(
'woocommerce_hpos_generate_join_for_search_filter',
'',
$this->search_term,
$search_filter,
$this->query
);
}
/**
@@ -177,6 +200,13 @@ class OrdersTableSearchQuery {
);
}
if ( 'transaction_id' === $search_filter ) {
return $wpdb->prepare(
"`$order_table`.transaction_id LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
'%' . $wpdb->esc_like( $this->search_term ) . '%'
);
}
if ( 'products' === $search_filter ) {
return $wpdb->prepare(
'search_query_items.order_item_name LIKE %s',
@@ -189,7 +219,28 @@ class OrdersTableSearchQuery {
return "`$order_table`.id IN ( $meta_sub_query ) ";
}
return '';
/**
* Filter to support adding a custom order search filter.
* Provide a WHERE clause for a custom search filter via this filter. This should be used with the
* `woocommerce_hpos_admin_search_filters` to declare a new custom filter, and optionally also with the
* `woocommerce_hpos_generate_join_for_search_filter` filter if a join is also needed.
*
* Hardcoded filters (products, customers, ID and email) cannot be modified using this filter for consistency.
*
* @since 8.9.0
*
* @param string $where WHERE clause to add to the search query.
* @param string $search_term The search term.
* @param string $search_filter Name of the search filter. Use this to bail early if this is not the filter you are looking for.
* @param OrdersTableQuery $query The order query object.
*/
return apply_filters(
'woocommerce_hpos_generate_where_for_search_filter',
'',
$this->search_term,
$search_filter,
$this->query
);
}
/**

View File

@@ -0,0 +1,34 @@
<?php
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonCacheInvalidator;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonRequestHandler;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonHelper;
/**
* Service provider for the Coming Soon mode.
*/
class ComingSoonServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
ComingSoonCacheInvalidator::class,
ComingSoonHelper::class,
ComingSoonRequestHandler::class,
);
/**
* Register the classes.
*/
public function register() {
$this->add( ComingSoonCacheInvalidator::class );
$this->add( ComingSoonHelper::class );
$this->add( ComingSoonRequestHandler::class )->addArgument( ComingSoonHelper::class );
}
}

View File

@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\Utilities\HtmlSanitizer;
use Automattic\WooCommerce\Internal\Utilities\PluginInstaller;
use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
@@ -20,7 +21,7 @@ use Automattic\WooCommerce\Utilities\TimeUtil;
/**
* Service provider for the non-static utils classes in the Automattic\WooCommerce\src namespace.
*/
class UtilsClassesServiceProvider extends AbstractServiceProvider {
class UtilsClassesServiceProvider extends AbstractInterfaceServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
@@ -35,6 +36,7 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
COTMigrationUtil::class,
WebhookUtil::class,
TimeUtil::class,
PluginInstaller::class,
);
/**
@@ -50,5 +52,6 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
$this->share( WebhookUtil::class );
$this->share( TimeUtil::class );
$this->share_with_implements_tags( PluginInstaller::class );
}
}

View File

@@ -76,6 +76,13 @@ class FeaturesController {
*/
private $force_allow_enabling_plugins = false;
/**
* List of plugins excluded from feature compatibility warnings in UI.
*
* @var string[]
*/
private $plugins_excluded_from_compatibility_ui;
/**
* Creates a new instance of the class.
*/
@@ -180,10 +187,10 @@ class FeaturesController {
'is_experimental' => true,
'disable_ui' => false,
'is_legacy' => true,
'disabled' => function() {
'disabled' => function () {
return version_compare( get_bloginfo( 'version' ), '6.2', '<' );
},
'desc_tip' => function() {
'desc_tip' => function () {
$string = '';
if ( version_compare( get_bloginfo( 'version' ), '6.2', '<' ) ) {
$string = __(
@@ -262,6 +269,8 @@ class FeaturesController {
final public function init( LegacyProxy $proxy, PluginUtil $plugin_util ) {
$this->proxy = $proxy;
$this->plugin_util = $plugin_util;
$this->plugins_excluded_from_compatibility_ui = $plugin_util->get_plugins_excluded_from_compatibility_ui();
}
/**
@@ -285,7 +294,7 @@ class FeaturesController {
if ( ! $include_experimental ) {
$features = array_filter(
$features,
function( $feature ) {
function ( $feature ) {
return ! $feature['is_experimental'];
}
);
@@ -422,7 +431,7 @@ class FeaturesController {
* @param bool $enabled_features_only True to return only names of enabled plugins.
* @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of feature ids.
*/
public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array {
public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ): array {
$this->verify_did_woocommerce_init( __FUNCTION__ );
$features = $this->get_feature_definitions();
@@ -457,7 +466,7 @@ class FeaturesController {
* @param bool $active_only True to return only active plugins.
* @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin names.
*/
public function get_compatible_plugins_for_feature( string $feature_id, bool $active_only = false ) : array {
public function get_compatible_plugins_for_feature( string $feature_id, bool $active_only = false ): array {
$this->verify_did_woocommerce_init( __FUNCTION__ );
$woo_aware_plugins = $this->plugin_util->get_woocommerce_aware_plugins( $active_only );
@@ -572,7 +581,7 @@ class FeaturesController {
$is_default_key = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );
$features_with_custom_keys = array_filter(
$this->get_feature_definitions(),
function( $feature ) {
function ( $feature ) {
return ! empty( $feature['option_key'] );
}
);
@@ -649,13 +658,16 @@ class FeaturesController {
$features = $this->get_features( true );
$feature_ids = array_keys( $features );
usort( $feature_ids, function( $feature_id_a, $feature_id_b ) use ( $features ) {
return ( $features[ $feature_id_b ]['order'] ?? 0 ) <=> ( $features[ $feature_id_a ]['order'] ?? 0 );
} );
$feature_ids = array_keys( $features );
usort(
$feature_ids,
function ( $feature_id_a, $feature_id_b ) use ( $features ) {
return ( $features[ $feature_id_b ]['order'] ?? 0 ) <=> ( $features[ $feature_id_a ]['order'] ?? 0 );
}
);
$experimental_feature_ids = array_filter(
$feature_ids,
function( $feature_id ) use ( $features ) {
function ( $feature_id ) use ( $features ) {
return $features[ $feature_id ]['is_experimental'] ?? false;
}
);
@@ -689,6 +701,10 @@ class FeaturesController {
continue;
}
if ( 'new_navigation' === $id && 'yes' !== get_option( $this->feature_enable_option_name( $id ), 'no' ) ) {
continue;
}
if ( isset( $features[ $id ]['disable_ui'] ) && $features[ $id ]['disable_ui'] ) {
continue;
}
@@ -709,7 +725,7 @@ class FeaturesController {
if ( $this->verify_did_woocommerce_init() ) {
// Allow feature setting properties to be determined dynamically just before being rendered.
$feature_settings = array_map(
function( $feature_setting ) {
function ( $feature_setting ) {
foreach ( $feature_setting as $prop => $value ) {
if ( is_callable( $value ) ) {
$feature_setting[ $prop ] = call_user_func( $value );
@@ -753,25 +769,15 @@ class FeaturesController {
$disabled = true;
$desc_tip = __( 'WooCommerce Admin has been disabled', 'woocommerce' );
} elseif ( 'new_navigation' === $feature_id ) {
$disabled = ! $this->feature_is_enabled( $feature_id );
if ( $disabled ) {
$update_text = sprintf(
// translators: 1: line break tag.
__( '%1$s The development of this feature is currently on hold.', 'woocommerce' ),
'<br/>'
);
} else {
$update_text = sprintf(
// translators: 1: line break tag.
__(
'%1$s This navigation will soon become unavailable while we make necessary improvements.
If you turn it off now, you will not be able to turn it back on.',
'woocommerce'
),
'<br/>'
);
}
$update_text = sprintf(
// translators: 1: line break tag.
__(
'%1$s This navigation will soon become unavailable while we make necessary improvements.
If you turn it off now, you will not be able to turn it back on.',
'woocommerce'
),
'<br/>'
);
$needs_update = version_compare( get_bloginfo( 'version' ), '5.6', '<' );
if ( $needs_update && current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
@@ -894,6 +900,8 @@ class FeaturesController {
private function get_incompatible_plugins( $feature_id, $list ) {
$incompatibles = array();
$list = array_diff_key( $list, array_flip( $this->plugins_excluded_from_compatibility_ui ) );
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
foreach ( array_keys( $list ) as $plugin_name ) {
if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) || ! $this->proxy->call_function( 'is_plugin_active', $plugin_name ) ) {
@@ -903,7 +911,7 @@ class FeaturesController {
$compatibility = $this->get_compatible_features_for_plugin( $plugin_name );
$incompatible_with = array_filter(
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
function( $feature_id ) {
function ( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
);
@@ -941,12 +949,13 @@ class FeaturesController {
}
$incompatible_plugins = false;
$relevant_plugins = array_diff( $this->plugin_util->get_woocommerce_aware_plugins( true ), $this->plugins_excluded_from_compatibility_ui );
foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) {
foreach ( $relevant_plugins as $plugin ) {
$compatibility = $this->get_compatible_features_for_plugin( $plugin, true );
$incompatible_with = array_filter(
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
function( $feature_id ) {
function ( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
);
@@ -1060,6 +1069,10 @@ class FeaturesController {
private function handle_plugin_list_rows( $plugin_file, $plugin_data ) {
global $wp_list_table;
if ( in_array( $plugin_file, $this->plugins_excluded_from_compatibility_ui, true ) ) {
return;
}
if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return;
}
@@ -1078,7 +1091,7 @@ class FeaturesController {
$incompatible_features = array_values(
array_filter(
$incompatible_features,
function( $feature_id ) {
function ( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
)

View File

@@ -1,6 +1,6 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\GroupInterface;

View File

@@ -3,7 +3,7 @@
* DownloadableProductTrait
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\GroupInterface;
@@ -38,7 +38,7 @@ trait DownloadableProductTrait {
$product_downloads_section_group->add_block(
array(
'id' => 'product-downloadable',
'blockName' => 'woocommerce/product-checkbox-field',
'blockName' => 'woocommerce/product-toggle-field',
'order' => 10,
'attributes' => array(
'property' => 'downloadable',

View File

@@ -3,7 +3,7 @@
* WooCommerce Product Group Block class.
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;

View File

@@ -3,7 +3,7 @@
* WooCommerce Product Block class.
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;

View File

@@ -3,11 +3,10 @@
* ProductVariationTemplate
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\DownloadableProductTrait;
/**
* Product Variation Template.
@@ -65,7 +64,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the group blocks to the template.
*/
private function add_group_blocks() {
protected function add_group_blocks() {
$this->add_group(
array(
'id' => $this::GROUP_IDS['GENERAL'],
@@ -107,7 +106,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the general group blocks to the template.
*/
private function add_general_group_blocks() {
protected function add_general_group_blocks() {
$general_group = $this->get_group_by_id( $this::GROUP_IDS['GENERAL'] );
$general_group->add_block(
array(
@@ -169,7 +168,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'description' => sprintf(
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag. */
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
'<a href="https://woo.com/posts/how-to-take-professional-product-photos-top-tips" target="_blank" rel="noreferrer">',
'<a href="https://woocommerce.com/posts/how-to-take-professional-product-photos-top-tips" target="_blank" rel="noreferrer">',
'</a>'
),
),
@@ -194,7 +193,9 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the pricing group blocks to the template.
*/
private function add_pricing_group_blocks() {
protected function add_pricing_group_blocks() {
$is_calc_taxes_enabled = wc_tax_enabled();
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
$pricing_group->add_block(
array(
@@ -219,7 +220,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'description' => sprintf(
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
'<a href="https://woo.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
'</a>'
),
'blockGap' => 'unit-40',
@@ -252,6 +253,12 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'name' => 'regular_price',
'label' => __( 'Regular price', 'woocommerce' ),
'isRequired' => true,
'help' => $is_calc_taxes_enabled ? null : sprintf(
/* translators: %1$s: store settings link opening tag. %2$s: store settings link closing tag.*/
__( 'Per your %1$sstore settings%2$s, taxes are not enabled.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=general' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
),
)
);
@@ -283,47 +290,32 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
)
);
$product_pricing_section->add_block(
array(
'id' => 'product-tax-class',
'blockName' => 'woocommerce/product-radio-field',
'order' => 40,
'attributes' => array(
'title' => __( 'Tax class', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
'<a href="https://woo.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
'</a>'
if ( $is_calc_taxes_enabled ) {
$product_pricing_section->add_block(
array(
'id' => 'product-tax-class',
'blockName' => 'woocommerce/product-select-field',
'order' => 40,
'attributes' => array(
'label' => __( 'Tax class', 'woocommerce' ),
'help' => sprintf(
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
'</a>'
),
'property' => 'tax_class',
'options' => SimpleProductTemplate::get_tax_classes( 'product_variation' ),
),
'property' => 'tax_class',
'options' => array(
array(
'label' => __( 'Same as main product', 'woocommerce' ),
'value' => 'parent',
),
array(
'label' => __( 'Standard', 'woocommerce' ),
'value' => '',
),
array(
'label' => __( 'Reduced rate', 'woocommerce' ),
'value' => 'reduced-rate',
),
array(
'label' => __( 'Zero rate', 'woocommerce' ),
'value' => 'zero-rate',
),
),
),
)
);
)
);
}
}
/**
* Adds the inventory group blocks to the template.
*/
private function add_inventory_group_blocks() {
protected function add_inventory_group_blocks() {
$inventory_group = $this->get_group_by_id( $this::GROUP_IDS['INVENTORY'] );
$inventory_group->add_block(
array(
@@ -374,7 +366,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'blockName' => 'woocommerce/product-toggle-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
'label' => __( 'Track inventory', 'woocommerce' ),
'property' => 'manage_stock',
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
'disabledCopy' => sprintf(
@@ -433,7 +425,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the shipping group blocks to the template.
*/
private function add_shipping_group_blocks() {
protected function add_shipping_group_blocks() {
$shipping_group = $this->get_group_by_id( $this::GROUP_IDS['SHIPPING'] );
$shipping_group->add_block(
array(
@@ -465,7 +457,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'checkedValue' => false,
'uncheckedValue' => true,
'label' => __( 'This variation requires shipping or pickup', 'woocommerce' ),
'uncheckedHelp' => __( 'This variation will not trigger your customer\'s shipping calculator in cart or at checkout. This product also won\'t require your customers to enter their shipping details at checkout. <a href="https://woo.com/document/managing-products/#adding-a-virtual-product" target="_blank" rel="noreferrer">Read more about virtual products</a>.', 'woocommerce' ),
'uncheckedHelp' => __( 'This variation will not trigger your customer\'s shipping calculator in cart or at checkout. This product also won\'t require your customers to enter their shipping details at checkout. <a href="https://woocommerce.com/document/managing-products/#adding-a-virtual-product" target="_blank" rel="noreferrer">Read more about virtual products</a>.', 'woocommerce' ),
),
)
);
@@ -479,8 +471,8 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'title' => __( 'Fees & dimensions', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
'<a href="https://woo.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
'</a>'
),
),

View File

@@ -3,7 +3,7 @@
* WooCommerce Section Block class.
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;

View File

@@ -3,11 +3,11 @@
* SimpleProductTemplate
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\DownloadableProductTrait;
use WC_Tax;
/**
* Simple Product Template.
@@ -215,6 +215,16 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'attributes' => array(
'name' => 'Product name',
'autoFocus' => true,
'metadata' => array(
'bindings' => array(
'value' => array(
'source' => 'woocommerce/entity-product',
'args' => array(
'prop' => 'name',
),
),
),
),
),
)
);
@@ -235,72 +245,6 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
)
);
// This is needed until hide conditions can be applied to core blocks.
$pricing_conditional_wrapper = $basic_details->add_block(
array(
'id' => 'product-pricing-conditional-wrapper',
'blockName' => 'woocommerce/conditional',
'order' => 30,
'hideConditions' => array(
array(
'expression' => 'editedProduct.type === "grouped"',
),
),
)
);
$pricing_wrapper = Features::is_enabled( 'product-grouped' ) ? $pricing_conditional_wrapper : $basic_details;
$pricing_columns = $pricing_wrapper->add_block(
array(
'id' => 'product-pricing-columns',
'blockName' => 'core/columns',
'order' => 30,
)
);
$pricing_column_1 = $pricing_columns->add_block(
array(
'id' => 'product-pricing-column-1',
'blockName' => 'core/column',
'order' => 10,
'attributes' => array(
'templateLock' => 'all',
),
)
);
$pricing_column_1->add_block(
array(
'id' => 'product-regular-price',
'blockName' => 'woocommerce/product-regular-price-field',
'order' => 10,
'attributes' => array(
'name' => 'regular_price',
'label' => __( 'List price', 'woocommerce' ),
/* translators: PricingTab: This is a link tag to the pricing tab. */
'help' => __( 'Manage more settings in <PricingTab>Pricing.</PricingTab>', 'woocommerce' ),
),
)
);
$pricing_column_2 = $pricing_columns->add_block(
array(
'id' => 'product-pricing-column-2',
'blockName' => 'core/column',
'order' => 20,
'attributes' => array(
'templateLock' => 'all',
),
)
);
$pricing_column_2->add_block(
array(
'id' => 'product-sale-price',
'blockName' => 'woocommerce/product-sale-price-field',
'order' => 10,
'attributes' => array(
'label' => __( 'Sale price', 'woocommerce' ),
),
)
);
// Description section.
$description_section = $general_group->add_section(
array(
@@ -445,7 +389,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'description' => sprintf(
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag. */
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
'<a href="https://woo.com/posts/how-to-take-professional-product-photos-top-tips" target="_blank" rel="noreferrer">',
'<a href="https://woocommerce.com/posts/how-to-take-professional-product-photos-top-tips" target="_blank" rel="noreferrer">',
'</a>'
),
),
@@ -551,29 +495,72 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
)
);
// Attributes section.
$product_catalog_section = $organization_group->add_section(
$product_attributes_section = $organization_group->add_section(
array(
'id' => 'product-attributes-section',
'order' => 20,
'attributes' => array(
'title' => __( 'Attributes', 'woocommerce' ),
'description' => __( 'Add descriptive pieces of information that customers can use to filter and search for this product. <a href="https://woo.com/document/managing-product-taxonomies/#product-attributes" target="_blank" rel="noreferrer">Learn more</a>.', 'woocommerce' ),
'description' => __( 'Use global attributes to allow shoppers to filter and search for this product. Use custom attributes to provide detailed product information.', 'woocommerce' ),
'blockGap' => 'unit-40',
),
)
);
$product_catalog_section->add_block(
$product_attributes_section->add_block(
array(
'id' => 'product-attributes',
'blockName' => 'woocommerce/product-attributes-field',
'order' => 10,
)
);
if ( Features::is_enabled( 'product-custom-fields' ) ) {
$organization_group->add_section(
array(
'id' => 'product-custom-fields-wrapper-section',
'order' => 30,
)
)->add_block(
array(
'id' => 'product-custom-fields-toggle',
'blockName' => 'woocommerce/product-custom-fields-toggle-field',
'order' => 10,
'attributes' => array(
'label' => __( 'Show custom fields', 'woocommerce' ),
),
)
)->add_block(
array(
'id' => 'product-custom-fields-section',
'blockName' => 'woocommerce/product-section',
'order' => 10,
'attributes' => array(
'blockGap' => 'unit-30',
'title' => __( 'Custom fields', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Custom fields guide link opening tag. %2$s: Custom fields guide link closing tag. */
__( 'Custom fields can be used in a variety of ways, such as sharing more detailed product information, showing more input fields, or for internal inventory organization. %1$sRead more about custom fields%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/document/custom-product-fields/" target="_blank" rel="noreferrer">',
'</a>'
),
),
)
)->add_block(
array(
'id' => 'product-custom-fields',
'blockName' => 'woocommerce/product-custom-fields',
'order' => 10,
)
);
}
}
/**
* Adds the pricing group blocks to the template.
*/
private function add_pricing_group_blocks() {
$is_calc_taxes_enabled = wc_tax_enabled();
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
$pricing_group->add_block(
array(
@@ -597,7 +584,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'description' => sprintf(
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
'<a href="https://woo.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
'</a>'
),
'blockGap' => 'unit-40',
@@ -623,12 +610,23 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$pricing_column_1->add_block(
array(
'id' => 'product-pricing-regular-price',
'blockName' => 'woocommerce/product-regular-price-field',
'order' => 10,
'attributes' => array(
'id' => 'product-pricing-regular-price',
'blockName' => 'woocommerce/product-regular-price-field',
'order' => 10,
'attributes' => array(
'name' => 'regular_price',
'label' => __( 'List price', 'woocommerce' ),
'help' => $is_calc_taxes_enabled ? null : sprintf(
/* translators: %1$s: store settings link opening tag. %2$s: store settings link closing tag.*/
__( 'Per your %1$sstore settings%2$s, taxes are not enabled.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=general' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
),
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
@@ -644,12 +642,17 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$pricing_column_2->add_block(
array(
'id' => 'product-pricing-sale-price',
'blockName' => 'woocommerce/product-sale-price-field',
'order' => 10,
'attributes' => array(
'id' => 'product-pricing-sale-price',
'blockName' => 'woocommerce/product-sale-price-field',
'order' => 10,
'attributes' => array(
'label' => __( 'Sale price', 'woocommerce' ),
),
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_pricing_section->add_block(
@@ -659,74 +662,98 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'order' => 20,
)
);
$product_pricing_section->add_block(
array(
'id' => 'product-sale-tax',
'blockName' => 'woocommerce/product-radio-field',
'order' => 30,
'attributes' => array(
'title' => __( 'Charge sales tax on', 'woocommerce' ),
'property' => 'tax_status',
'options' => array(
array(
'label' => __( 'Product and shipping', 'woocommerce' ),
'value' => 'taxable',
),
array(
'label' => __( 'Only shipping', 'woocommerce' ),
'value' => 'shipping',
),
array(
'label' => __( "Don't charge tax", 'woocommerce' ),
'value' => 'none',
if ( $is_calc_taxes_enabled ) {
$product_pricing_section->add_block(
array(
'id' => 'product-sale-tax',
'blockName' => 'woocommerce/product-radio-field',
'order' => 30,
'attributes' => array(
'title' => __( 'Charge sales tax on', 'woocommerce' ),
'property' => 'tax_status',
'options' => array(
array(
'label' => __( 'Product and shipping', 'woocommerce' ),
'value' => 'taxable',
),
array(
'label' => __( 'Only shipping', 'woocommerce' ),
'value' => 'shipping',
),
array(
'label' => __( "Don't charge tax", 'woocommerce' ),
'value' => 'none',
),
),
),
),
)
);
$pricing_advanced_block = $product_pricing_section->add_block(
array(
'id' => 'product-pricing-advanced',
'blockName' => 'woocommerce/product-collapsible',
'order' => 40,
'attributes' => array(
'toggleText' => __( 'Advanced', 'woocommerce' ),
'initialCollapsed' => true,
'persistRender' => true,
),
)
);
$pricing_advanced_block->add_block(
array(
'id' => 'product-tax-class',
'blockName' => 'woocommerce/product-radio-field',
'order' => 10,
'attributes' => array(
'title' => __( 'Tax class', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
'<a href="https://woo.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
'</a>'
)
);
$pricing_advanced_block = $product_pricing_section->add_block(
array(
'id' => 'product-pricing-advanced',
'blockName' => 'woocommerce/product-collapsible',
'order' => 40,
'attributes' => array(
'toggleText' => __( 'Advanced', 'woocommerce' ),
'initialCollapsed' => true,
'persistRender' => true,
),
'property' => 'tax_class',
'options' => array(
array(
'label' => __( 'Standard', 'woocommerce' ),
'value' => '',
),
array(
'label' => __( 'Reduced rate', 'woocommerce' ),
'value' => 'reduced-rate',
),
array(
'label' => __( 'Zero rate', 'woocommerce' ),
'value' => 'zero-rate',
)
);
$pricing_advanced_block->add_block(
array(
'id' => 'product-tax-class',
'blockName' => 'woocommerce/product-select-field',
'order' => 10,
'attributes' => array(
'label' => __( 'Tax class', 'woocommerce' ),
'help' => sprintf(
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
'</a>'
),
'property' => 'tax_class',
'options' => self::get_tax_classes(),
),
),
)
)
);
}
}
/**
* Get the tax classes as select options.
*
* @param string $post_type The post type.
* @return array Array of options.
*/
public static function get_tax_classes( $post_type = 'product' ) {
$tax_classes = array();
if ( 'product_variation' === $post_type ) {
$tax_classes[] = array(
'label' => __( 'Same as main product', 'woocommerce' ),
'value' => 'parent',
);
}
// Add standard class.
$tax_classes[] = array(
'label' => __( 'Standard rate', 'woocommerce' ),
'value' => '',
);
$classes = WC_Tax::get_tax_rate_classes();
foreach ( $classes as $tax_class ) {
$tax_classes[] = array(
'label' => $tax_class->name,
'value' => $tax_class->slug,
);
}
return $tax_classes;
}
/**
@@ -771,32 +798,44 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$product_inventory_inner_section->add_block(
array(
'id' => 'product-sku-field',
'blockName' => 'woocommerce/product-sku-field',
'order' => 10,
'id' => 'product-sku-field',
'blockName' => 'woocommerce/product-sku-field',
'order' => 10,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$manage_stock = 'yes' === get_option( 'woocommerce_manage_stock' );
$product_inventory_inner_section->add_block(
array(
'id' => 'product-track-stock',
'blockName' => 'woocommerce/product-toggle-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
'id' => 'product-track-stock',
'blockName' => 'woocommerce/product-toggle-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Track inventory', 'woocommerce' ),
'property' => 'manage_stock',
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
'disabledCopy' => sprintf(
'disabled' => ! $manage_stock,
'disabledCopy' => ! $manage_stock ? sprintf(
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
__( 'Per your %1$sstore settings%2$s, inventory management is <strong>disabled</strong>.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=inventory' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
) : null,
),
'hideConditions' => Features::is_enabled( 'product-external-affiliate' ) || Features::is_enabled( 'product-grouped' ) ? array(
'hideConditions' => Features::is_enabled( 'product-external-affiliate' ) || Features::is_enabled( 'product-grouped' ) ? array(
array(
'expression' => 'editedProduct.type === "external" || editedProduct.type === "grouped"',
),
) : null,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_inventory_quantity_hide_conditions = array(
@@ -829,10 +868,10 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
}
$product_inventory_section->add_block(
array(
'id' => 'product-stock-status',
'blockName' => 'woocommerce/product-radio-field',
'order' => 10,
'attributes' => array(
'id' => 'product-stock-status',
'blockName' => 'woocommerce/product-radio-field',
'order' => 10,
'attributes' => array(
'title' => __( 'Stock status', 'woocommerce' ),
'property' => 'stock_status',
'options' => array(
@@ -850,9 +889,28 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
),
),
'hideConditions' => $product_stock_status_hide_conditions,
'hideConditions' => $product_stock_status_hide_conditions,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_inventory_section->add_block(
array(
'id' => 'product-purchase-note',
'blockName' => 'woocommerce/product-text-area-field',
'order' => 20,
'attributes' => array(
'property' => 'purchase_note',
'label' => __( 'Post-purchase note', 'woocommerce' ),
'placeholder' => __( 'Enter an optional note attached to the order confirmation message sent to the shopper.', 'woocommerce' ),
),
)
);
$product_inventory_advanced = $product_inventory_section->add_block(
array(
'id' => 'product-inventory-advanced',
@@ -988,7 +1046,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'checkedValue' => false,
'uncheckedValue' => true,
'label' => __( 'This product requires shipping or pickup', 'woocommerce' ),
'uncheckedHelp' => __( 'This product will not trigger your customer\'s shipping calculator in cart or at checkout. This product also won\'t require your customers to enter their shipping details at checkout. <a href="https://woo.com/document/managing-products/#adding-a-virtual-product" target="_blank" rel="noreferrer">Read more about virtual products</a>.', 'woocommerce' ),
'uncheckedHelp' => __( 'This product will not trigger your customer\'s shipping calculator in cart or at checkout. This product also won\'t require your customers to enter their shipping details at checkout. <a href="https://woocommerce.com/document/managing-products/#adding-a-virtual-product" target="_blank" rel="noreferrer">Read more about virtual products</a>.', 'woocommerce' ),
),
)
);
@@ -1002,8 +1060,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'title' => __( 'Fees & dimensions', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
'<a href="https://woo.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
'</a>'
),
),
@@ -1011,16 +1069,26 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$product_fee_and_dimensions_section->add_block(
array(
'id' => 'product-shipping-class',
'blockName' => 'woocommerce/product-shipping-class-field',
'order' => 10,
'id' => 'product-shipping-class',
'blockName' => 'woocommerce/product-shipping-class-field',
'order' => 10,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_fee_and_dimensions_section->add_block(
array(
'id' => 'product-shipping-dimensions',
'blockName' => 'woocommerce/product-shipping-dimensions-fields',
'order' => 20,
'id' => 'product-shipping-dimensions',
'blockName' => 'woocommerce/product-shipping-dimensions-fields',
'order' => 20,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
}
@@ -1085,9 +1153,9 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'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">',
/* translators: %1$s: "Learn more about linked products" link opening tag. %2$s: "Learn more about linked products" link closing tag. */
__( '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://woocommerce.com/document/related-products-up-sells-and-cross-sells/" target="_blank" rel="noreferrer">',
'</a>'
),
),
@@ -1118,9 +1186,9 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'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">',
/* translators: %1$s: "Learn more about linked products" link opening tag. %2$s: "Learn more about linked products" link closing tag. */
__( '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://woocommerce.com/document/related-products-up-sells-and-cross-sells/" target="_blank" rel="noreferrer">',
'</a>'
),
),

View File

@@ -3,7 +3,7 @@
* WooCommerce Subsection Block class.
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
namespace Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;

View File

@@ -76,14 +76,28 @@ class FontFace {
/**
* Handles the upload of a font file using wp_handle_upload().
*
* Copied from Gutenberg: https://github.com/WordPress/gutenberg/blob/b283c47dba96d74dd7589a823d8ab84c9e5a4765/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php/#L859-L883
* Copied from Gutenberg: https://github.com/WordPress/gutenberg/blob/f4889bf58ddeb8470c8d2a765f1b57229c515eda/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php/#L859-L896
*
* @param array $file Single file item from $_FILES.
* @return array Array containing uploaded file attributes on success, or error on failure.
*/
private static function handle_font_file_upload( $file ) {
add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
add_filter( 'upload_dir', 'wp_get_font_dir' );
/*
* Set the upload directory to the fonts directory.
*
* wp_get_font_dir() contains the 'font_dir' hook, whose callbacks are
* likely to call wp_get_upload_dir().
*
* To avoid an infinite loop, don't hook wp_get_font_dir() to 'upload_dir'.
* Instead, just pass its return value to the 'upload_dir' callback.
*/
$font_dir = wp_get_font_dir();
$set_upload_dir = function () use ( $font_dir ) {
return $font_dir;
};
add_filter( 'upload_dir', $set_upload_dir );
$overrides = array(
'upload_error_handler' => array( self::class, 'handle_font_file_upload_error' ),
@@ -100,8 +114,7 @@ class FontFace {
);
$uploaded_file = wp_handle_upload( $file, $overrides );
remove_filter( 'upload_dir', 'wp_get_font_dir' );
remove_filter( 'upload_dir', $set_upload_dir );
remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
return $uploaded_file;

View File

@@ -106,7 +106,7 @@ class MobileMessagingHandler {
),
self::prepare_utm_parameters( 'deeplinks_payments', $blog_id, $domain )
),
'https://woo.com/mobile/payments'
'https://woocommerce.com/mobile/payments'
);
return sprintf(
@@ -138,7 +138,7 @@ class MobileMessagingHandler {
),
self::prepare_utm_parameters( 'deeplinks_orders_details', $blog_id, $domain )
),
'https://woo.com/mobile/orders/details'
'https://woocommerce.com/mobile/orders/details'
);
return sprintf(
@@ -168,7 +168,7 @@ class MobileMessagingHandler {
),
self::prepare_utm_parameters( 'deeplinks_promote_app', $blog_id, $domain )
),
'https://woo.com/mobile'
'https://woocommerce.com/mobile'
);
return sprintf(
/* translators: 1: opening link tag 2: closing link tag. */
@@ -182,7 +182,7 @@ class MobileMessagingHandler {
}
/**
* Prepares array of parameters used by Woo.com for tracking.
* Prepares array of parameters used by WooCommerce.com for tracking.
*
* @param string $campaign name of the deep link campaign.
* @param int|null $blog_id blog id of the current site.

View File

@@ -111,8 +111,8 @@ class OrderAttributionController implements RegisterHooksInterface {
}
);
add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'source_form_elements' ) );
add_action( 'woocommerce_register_form', array( $this, 'source_form_elements' ) );
add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'stamp_html_element' ) );
add_action( 'woocommerce_register_form', array( $this, 'stamp_html_element' ) );
// Update order based on submitted fields.
add_action(
@@ -335,21 +335,15 @@ class OrderAttributionController implements RegisterHooksInterface {
$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' );
}
echo esc_html( $origin );
}
/**
* Print `<input type="hidden">` elements for source fields.
* To be picked up and populated with data by the JS.
* Add `<wc-order-attribution-inputs>` element that contributes the order attribution values to the enclosing form.
* Used for checkout & customer register forms.
*/
public 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 ) ) );
}
public function stamp_html_element() {
printf( '<wc-order-attribution-inputs></wc-order-attribution-inputs>' );
}
/**

View File

@@ -74,8 +74,12 @@ class COTMigrationUtil {
* @return bool
*/
public function is_custom_order_tables_in_sync() : bool {
if ( ! $this->data_synchronizer->data_sync_is_enabled() ) {
return false;
}
$sync_status = $this->data_synchronizer->get_sync_status();
return 0 === $sync_status['current_pending_count'] && $this->data_synchronizer->data_sync_is_enabled();
return 0 === $sync_status['current_pending_count'];
}
/**

View File

@@ -0,0 +1,64 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Utilities;
use Automattic\Jetpack\Constants;
use Exception;
use WP_Filesystem_Base;
/**
* FilesystemUtil class.
*/
class FilesystemUtil {
/**
* Wrapper to retrieve the class instance contained in the $wp_filesystem global, after initializing if necessary.
*
* @return WP_Filesystem_Base
* @throws Exception Thrown when the filesystem fails to initialize.
*/
public static function get_wp_filesystem(): WP_Filesystem_Base {
global $wp_filesystem;
if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
$initialized = self::initialize_wp_filesystem();
if ( false === $initialized ) {
throw new Exception( 'The WordPress filesystem could not be initialized.' );
}
}
return $wp_filesystem;
}
/**
* Wrapper to initialize the WP filesystem with defined credentials if they are available.
*
* @return bool True if the $wp_filesystem global was successfully initialized.
*/
protected static function initialize_wp_filesystem(): bool {
global $wp_filesystem;
if ( $wp_filesystem instanceof WP_Filesystem_Base ) {
return true;
}
require_once ABSPATH . 'wp-admin/includes/file.php';
$method = get_filesystem_method();
$initialized = false;
if ( 'direct' === $method ) {
$initialized = WP_Filesystem();
} elseif ( false !== $method ) {
// See https://core.trac.wordpress.org/changeset/56341.
ob_start();
$credentials = request_filesystem_credentials( '' );
ob_end_clean();
$initialized = $credentials && WP_Filesystem( $credentials );
}
return is_null( $initialized ) ? false : $initialized;
}
}

View File

@@ -32,6 +32,20 @@ class HtmlSanitizer {
),
);
/**
* Sanitizes a chunk of HTML, by following the same rules as `wp_kses_post()` but also allowing
* the style element to be supplied.
*
* @param string $html The HTML to be sanitized.
*
* @return string
*/
public function styled_post_content( string $html ): string {
$rules = wp_kses_allowed_html( 'post' );
$rules['style'] = true;
return wp_kses( $html, $rules );
}
/**
* Sanitizes the HTML according to the provided rules.
*

View File

@@ -0,0 +1,308 @@
<?php
namespace Automattic\WooCommerce\Internal\Utilities;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\StringUtil;
/**
* This class allows installing a plugin programmatically.
*
* Information about plugins installed in that way will be stored in a 'woocommerce_autoinstalled_plugins' option,
* and a notice will be shown under the plugin name in the plugins list indicating that it was automatically
* installed (these notices can be disabled with the 'woocommerce_show_autoinstalled_plugin_notices' hook).
*
* Currently it's only possible to install new plugins, not to upgrade or reinstall already installed plugins.
*
* The 'upgrader_process_complete' hook is used to remove the autoinstall information from any plugin that is later
* upgraded or reinstalled by any means other than the usage of this class.
*/
class PluginInstaller implements RegisterHooksInterface {
use AccessiblePrivateMethods;
/**
* Flag indicating that a plugin install is in progress, so the upgrader_process_complete hook must be ignored.
*
* @var bool
*/
private bool $installing_plugin = false;
/**
* Attach hooks used by the class.
*/
public function register() {
self::add_action( 'after_plugin_row', array( $this, 'handle_plugin_list_rows' ), 10, 2 );
self::add_action( 'upgrader_process_complete', array( $this, 'handle_upgrader_process_complete' ), 10, 2 );
}
/**
* Programmatically installs a plugin. Upgrade/reinstall of already existing plugins is not supported.
* The plugin source must be the WordPress.org plugins directory.
*
* $metadata can contain anything, but the following keys are recognized by the code that renders the notice
* in the plugins list:
*
* - 'installed_by': defaults to 'WooCommerce' if not present.
* - 'info_link': if present, a "More information" link will be included in the notice.
*
* If 'installed_by' is supplied and it's not 'WooCommerce' (case-insensitive), an exception will be thrown
* if the code calling this method is not in a WooCommerce core file (in 'includes' or in 'src').
*
* Information about plugins successfully installed with this method will be kept in an option named
* 'woocommerce_autoinstalled_plugins'. Keys will be the plugin name and values will be associative arrays
* with these keys: 'plugin_name', 'version', 'date' and 'metadata' (same meaning as in the returned array).
*
* A log entry will be created with the result of the process and all the installer messages
* (source: 'plugin_auto_installs'). In multisite this log entry will be created on each site.
*
* The returned array will contain the following (only 'install_ok' and 'messages' if the installation fails):
*
* - 'install_ok', a boolean.
* - 'messages', all the messages generated by the installer.
* - 'plugin_name', in the form of 'directory/file.php' (taken from the instance of PluginInstaller used).
* - 'version', of the plugin that has been installed.
* - 'date', ISO-formatted installation date.
* - 'metadata', as supplied (except the 'plugin_name' key) and only if not empty.
*
* If the plugin is already in the process of being installed (can happen in multisite), the returned array
* will contain only one key: 'already_installing', with a value of true.
*
* @param string $plugin_url URL or file path of the plugin to install.
* @param array $metadata Metadata to store if the installation succeeds.
* @return array Information about the installation result.
* @throws \InvalidArgumentException Source doesn't start with 'https://downloads.wordpress.org/', or installer name is 'WooCommerce' but caller is not WooCommerce core code.
*/
public function install_plugin( string $plugin_url, array $metadata = array() ): array {
$this->installing_plugin = true;
$plugins_being_installed = get_site_option( 'woocommerce_autoinstalling_plugins', array() );
if ( in_array( $plugin_url, $plugins_being_installed, true ) ) {
return array( 'already_installing' => true );
}
$plugins_being_installed[] = $plugin_url;
update_site_option( 'woocommerce_autoinstalling_plugins', $plugins_being_installed );
try {
return $this->install_plugin_core( $plugin_url, $metadata );
} finally {
$plugins_being_installed = array_diff( $plugins_being_installed, array( $plugin_url ) );
if ( empty( $plugins_being_installed ) ) {
delete_site_option( 'woocommerce_autoinstalling_plugins' );
} else {
update_site_option( 'woocommerce_autoinstalling_plugins', $plugins_being_installed );
}
$this->installing_plugin = false;
}
}
/**
* Core version of 'install_plugin' (it doesn't handle the $installing_plugin flag).
*
* @param string $plugin_url URL or file path of the plugin to install.
* @param array $metadata Metadata to store if the installation succeeds.
* @return array Information about the installation result.
* @throws \InvalidArgumentException Source doesn't start with 'https://downloads.wordpress.org/', or installer name is 'WooCommerce' but caller is not WooCommerce core code.
*/
private function install_plugin_core( string $plugin_url, array $metadata ): array {
if ( ! StringUtil::starts_with( $plugin_url, 'https://downloads.wordpress.org/', false ) ) {
throw new \InvalidArgumentException( "Only installs from the WordPress.org plugins directory (plugin URL starting with 'https://downloads.wordpress.org/') are allowed." );
}
$installed_by = $metadata['installed_by'] ?? 'WooCommerce';
if ( 0 === strcasecmp( 'WooCommerce', $installed_by ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$calling_file = debug_backtrace()[1]['file'] ?? null; // [1], not [0], because the immediate caller is the install_plugin method.
if ( ! StringUtil::starts_with( $calling_file, WC_ABSPATH . 'includes/' ) && ! StringUtil::starts_with( $calling_file, WC_ABSPATH . 'src/' ) ) {
throw new \InvalidArgumentException( "If the value of 'installed_by' is 'WooCommerce', the caller of the method must be a WooCommerce core class or function." );
}
}
if ( ! class_exists( \Automatic_Upgrader_Skin::class ) ) {
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
include_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';
}
$skin = new \Automatic_Upgrader_Skin();
if ( ! class_exists( \Plugin_Upgrader::class ) ) {
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
$upgrader = new \Plugin_Upgrader( $skin );
$install_ok = $upgrader->install( $plugin_url );
$result = array( 'messages' => $skin->get_upgrade_messages() );
if ( $install_ok ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_name = $upgrader->plugin_info();
$plugin_version = get_plugins()[ $plugin_name ]['Version'];
$result['plugin_name'] = $plugin_name;
$plugin_data = array(
'version' => $plugin_version,
'date' => current_time( 'mysql' ),
);
if ( ! empty( $metadata ) ) {
$plugin_data['metadata'] = $metadata;
}
$auto_installed_plugins = get_site_option( 'woocommerce_autoinstalled_plugins', array() );
$auto_installed_plugins[ $plugin_name ] = $plugin_data;
update_site_option( 'woocommerce_autoinstalled_plugins', $auto_installed_plugins );
$post_install = function () use ( $plugin_name, $plugin_version, $installed_by, $plugin_url, $plugin_data ) {
$log_context = array(
'source' => 'plugin_auto_installs',
'recorded_data' => $plugin_data,
);
wc_get_logger()->info( "Plugin $plugin_name v{$plugin_version} installed by $installed_by, source: $plugin_url", $log_context );
};
} else {
$messages = $skin->get_upgrade_messages();
$post_install = function () use ( $plugin_url, $installed_by, $messages ) {
$log_context = array(
'source' => 'plugin_auto_installs',
'installer_messages' => $messages,
);
wc_get_logger()->error( "$installed_by failed to install plugin from source: $plugin_url", $log_context );
};
}
if ( is_multisite() ) {
// We log the install in the main site, unless the main site doesn't have WooCommerce installed;
// in that case we fallback to logging in the current site.
switch_to_blog( get_main_site_id() );
if ( self::woocommerce_is_active_in_current_site() ) {
$post_install();
restore_current_blog();
} else {
restore_current_blog();
$post_install();
}
} else {
$post_install();
}
$result['install_ok'] = $install_ok ?? false;
return $result;
}
/**
* Check if WooCommerce is installed and active in the current blog.
* This is useful for multisite installs when a blog other than the one running this code is selected with 'switch_to_blog'.
*
* @return bool True if WooCommerce is installed and active in the current blog, false otherwise.
*/
private static function woocommerce_is_active_in_current_site(): bool {
return ! empty( array_filter( wp_get_active_and_valid_plugins(), fn( $plugin ) => substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0 ) );
}
/**
* Handler for the 'plugin_list_rows' hook, it will display a notice under the name of the plugins
* that have been installed using this class (unless the 'woocommerce_show_autoinstalled_plugin_notices' filter
* returns false) in the plugins list page.
*
* @param string $plugin_file Name of the plugin.
* @param array $plugin_data Plugin data.
*/
private function handle_plugin_list_rows( $plugin_file, $plugin_data ) {
global $wp_list_table;
if ( is_null( $wp_list_table ) ) {
return;
}
/**
* Filter to suppress the notice about autoinstalled plugins in the plugins list page.
*
* @since 8.8.0
*
* @param bool $display_notice Whether notices should be displayed or not.
* @returns bool
*/
if ( ! apply_filters( 'woocommerce_show_autoinstalled_plugin_notices', '__return_true' ) ) {
return;
}
$auto_installed_plugins_info = get_site_option( 'woocommerce_autoinstalled_plugins', array() );
$current_plugin_info = $auto_installed_plugins_info[ $plugin_file ] ?? null;
if ( is_null( $current_plugin_info ) || $current_plugin_info['version'] !== $plugin_data['Version'] ) {
return;
}
$installed_by = $current_plugin_info['metadata']['installed_by'] ?? 'WooCommerce';
$info_link = $current_plugin_info['metadata']['info_link'] ?? null;
if ( $info_link ) {
/* translators: 1 = who installed the plugin, 2 = ISO-formatted date and time, 3 = URL */
$message = sprintf( __( 'Plugin installed by %1$s on %2$s. <a target="_blank" href="%3$s">More information</a>', 'woocommerce' ), $installed_by, $current_plugin_info['date'], $info_link );
} else {
/* translators: 1 = who installed the plugin, 2 = ISO-formatted date and time */
$message = sprintf( __( 'Plugin installed by %1$s on %2$s.', 'woocommerce' ), $installed_by, $current_plugin_info['date'] );
}
$columns_count = $wp_list_table->get_column_count();
$is_active = is_plugin_active( $plugin_file );
$is_active_class = $is_active ? 'active' : 'inactive';
$is_active_td_style = $is_active ? "style='border-left: 4px solid #72aee6;'" : '';
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<tr class='plugin-update-tr update <?php echo $is_active_class; ?>' data-plugin='<?php echo $plugin_file; ?>' data-plugin-row-type='feature-incomp-warn'>
<td colspan='<?php echo $columns_count; ?>' class='plugin-update'<?php echo $is_active_td_style; ?>>
<div class='notice inline notice-success notice-alt'>
<p>
<?php echo $message; ?>
</p>
</div>
</td>
</tr>
<?php
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Handler for the 'upgrader_process_complete' hook. It's used to remove the autoinstalled plugin information
* for plugins that are upgraded or reinstalled manually (or more generally, by using any install method
* other than this class).
*
* @param \WP_Upgrader $upgrader The upgrader class that has performed the plugin upgrade/reinstall.
* @param array $hook_extra Extra information about the upgrade process.
*/
private function handle_upgrader_process_complete( \WP_Upgrader $upgrader, array $hook_extra ) {
if ( $this->installing_plugin || ! ( $upgrader instanceof \Plugin_Upgrader ) || ( 'plugin' !== ( $hook_extra['type'] ?? null ) ) ) {
return;
}
$auto_installed_plugins = get_site_option( 'woocommerce_autoinstalled_plugins' );
if ( ! $auto_installed_plugins ) {
return;
}
if ( $hook_extra['bulk'] ?? false ) {
$updated_plugin_names = $hook_extra['plugins'] ?? array();
} else {
$updated_plugin_names = array( $upgrader->plugin_info() );
}
$auto_installed_plugin_names = array_keys( $auto_installed_plugins );
$updated_auto_installed_plugin_names = array_intersect( $auto_installed_plugin_names, $updated_plugin_names );
if ( empty( $updated_auto_installed_plugin_names ) ) {
return;
}
$new_auto_installed_plugins = array_diff_key( $auto_installed_plugins, array_flip( $updated_auto_installed_plugin_names ) );
if ( empty( $new_auto_installed_plugins ) ) {
delete_site_option( 'woocommerce_autoinstalled_plugins' );
} else {
update_site_option( 'woocommerce_autoinstalled_plugins', $new_auto_installed_plugins );
}
}
}

View File

@@ -138,13 +138,19 @@ class WebhookUtil {
/**
* Gets the count of webhooks that are configured to use the Legacy REST API to compose their payloads.
*
* @param bool $clear_cache If true, the previously cached value of the count will be discarded if it exists.
*
* @return int
*/
public function get_legacy_webhooks_count(): int {
public function get_legacy_webhooks_count( bool $clear_cache = false ): int {
global $wpdb;
$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'legacy_count';
$count = wp_cache_get( $cache_key, 'webhooks' );
if ( $clear_cache ) {
wp_cache_delete( $cache_key, 'webhooks' );
}
$count = wp_cache_get( $cache_key, 'webhooks' );
if ( false === $count ) {
$count = absint( $wpdb->get_var( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `api_version` < 1;" ) );

View File

@@ -1,6 +1,6 @@
<?php
/**
* Helpers for managing connection to Woo.com.
* Helpers for managing connection to WooCommerce.com.
*/
namespace Automattic\WooCommerce\Internal\WCCom;
@@ -10,11 +10,11 @@ defined( 'ABSPATH' ) || exit;
/**
* Class WCConnectionHelper.
*
* Helpers for managing connection to Woo.com.
* Helpers for managing connection to WooCommerce.com.
*/
final class ConnectionHelper {
/**
* Check if Woo.com account is connected.
* Check if WooCommerce.com account is connected.
*
* @since 4.4.0
* @return bool Whether account is connected.