Plugin Updates

This commit is contained in:
Tony Volpe
2024-04-02 20:23:21 +00:00
parent 96800520e8
commit 94170ec2c4
1514 changed files with 133309 additions and 105985 deletions

View File

@@ -664,6 +664,26 @@ class FileController {
return array_slice( $matched_lines, $args['offset'], $args['per_page'] );
}
/**
* Calculate the size, in bytes, of the log directory.
*
* @return int
*/
public function get_log_directory_size(): int {
$bytes = 0;
$path = realpath( $this->log_directory );
if ( wp_is_writable( $path ) ) {
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ) );
foreach ( $iterator as $file ) {
$bytes += $file->getSize();
}
}
return $bytes;
}
/**
* Invalidate the cache group related to log file data.
*

View File

@@ -123,6 +123,10 @@ class LogHandlerFileV2 extends WC_Log_Handler {
$backtrace = static::get_backtrace();
foreach ( $backtrace as $frame ) {
if ( ! isset( $frame['file'] ) ) {
continue;
}
foreach ( $source_roots as $type => $path ) {
if ( 0 === strpos( $frame['file'], $path ) ) {
$relative_path = trim( substr( $frame['file'], strlen( $path ) ), DIRECTORY_SEPARATOR );
@@ -233,7 +237,29 @@ class LogHandlerFileV2 extends WC_Log_Handler {
)
);
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
if ( is_wp_error( $files ) ) {
return 0;
}
$files = array_filter(
$files,
function( $file ) use ( $timestamp ) {
/**
* Allows preventing an expired log file from being deleted.
*
* @param bool $delete True to delete the file.
* @param File $file The log file object.
* @param int $timestamp The expiration threshold.
*
* @since 8.7.0
*/
$delete = apply_filters( 'woocommerce_logger_delete_expired_file', true, $file, $timestamp );
return boolval( $delete );
}
);
if ( count( $files ) < 1 ) {
return 0;
}
@@ -250,31 +276,16 @@ class LogHandlerFileV2 extends WC_Log_Handler {
time(),
'info',
sprintf(
'%s %s',
sprintf(
esc_html(
// translators: %s is a number of log files.
_n(
'%s expired log file was deleted.',
'%s expired log files were deleted.',
$deleted,
'woocommerce'
)
),
number_format_i18n( $deleted )
esc_html(
// translators: %s is a number of log files.
_n(
'%s expired log file was deleted.',
'%s expired log files were deleted.',
$deleted,
'woocommerce'
)
),
sprintf(
esc_html(
// translators: %s is a number of days.
_n(
'The retention period for log files is %s day.',
'The retention period for log files is %s days.',
$retention_days,
'woocommerce'
)
),
number_format_i18n( $retention_days )
)
number_format_i18n( $deleted )
),
array(
'source' => 'wc_logger',

View File

@@ -4,10 +4,12 @@ declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Admin\Logging;
use Automattic\Jetpack\Constants;
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 WC_Admin_Settings;
use WC_Log_Handler, WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
use WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
/**
* Settings class.
@@ -22,11 +24,10 @@ class Settings {
* @const array
*/
private const DEFAULTS = array(
'logging_enabled' => true,
'default_handler' => LogHandlerFileV2::class,
'retention_period_days' => 30,
'level_threshold' => 'none',
'file_entry_collapse_lines' => true,
'logging_enabled' => true,
'default_handler' => LogHandlerFileV2::class,
'retention_period_days' => 30,
'level_threshold' => 'none',
);
/**
@@ -77,13 +78,13 @@ class Settings {
$settings['default_handler'] = $this->get_default_handler_setting_definition();
$settings['retention_period_days'] = $this->get_retention_period_days_setting_definition();
$settings['level_threshold'] = $this->get_level_threshold_setting_definition();
}
$default_handler = $this->get_default_handler();
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
$settings += $this->get_filesystem_settings_definitions();
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
$settings += $this->get_database_settings_definitions();
$default_handler = $this->get_default_handler();
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
$settings += $this->get_filesystem_settings_definitions();
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
$settings += $this->get_database_settings_definitions();
}
}
return $settings;
@@ -153,18 +154,28 @@ class Settings {
'step' => 1,
);
$desc = array();
$hardcoded = has_filter( 'woocommerce_logger_days_to_retain_logs' );
$desc = '';
if ( $hardcoded ) {
$custom_attributes['disabled'] = 'true';
$desc = sprintf(
$desc[] = sprintf(
// translators: %s is the name of a filter hook.
__( 'This setting cannot be changed here because it is being set by a filter on the %s hook.', 'woocommerce' ),
'<code>woocommerce_logger_days_to_retain_logs</code>'
);
}
$file_delete_has_filter = LogHandlerFileV2::class === $this->get_default_handler() && has_filter( 'woocommerce_logger_delete_expired_file' );
if ( $file_delete_has_filter ) {
$desc[] = sprintf(
// translators: %s is the name of a filter hook.
__( 'The %s hook has a filter set, so some log files may have different retention settings.', 'woocommerce' ),
'<code>woocommerce_logger_delete_expired_file</code>'
);
}
return array(
'title' => __( 'Retention period', 'woocommerce' ),
'desc_tip' => __( 'This sets how many days log entries will be kept before being auto-deleted.', 'woocommerce' ),
@@ -180,7 +191,7 @@ class Settings {
' %s',
__( 'days', 'woocommerce' ),
),
'desc' => $desc,
'desc' => implode( '<br><br>', $desc ),
);
}
@@ -231,7 +242,7 @@ class Settings {
*/
private function get_filesystem_settings_definitions(): array {
$location_info = array();
$directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
$directory = trailingslashit( Constants::get_constant( 'WC_LOG_DIR' ) );
$location_info[] = sprintf(
// translators: %s is a location in the filesystem.
@@ -253,6 +264,12 @@ class Settings {
'<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' ),
size_format( wc_get_container()->get( FileController::class )->get_log_directory_size() )
);
return array(
'file_start' => array(
'title' => __( 'File system settings', 'woocommerce' ),
@@ -260,8 +277,9 @@ class Settings {
'type' => 'title',
),
'log_directory' => array(
'type' => 'info',
'text' => implode( "\n\n", $location_info ),
'title' => __( 'Location', 'woocommerce' ),
'type' => 'info',
'text' => implode( "\n\n", $location_info ),
),
'entry_format' => array(),
'file_end' => array(
@@ -293,8 +311,9 @@ class Settings {
'type' => 'title',
),
'database_table' => array(
'type' => 'info',
'text' => $location_info,
'title' => __( 'Location', 'woocommerce' ),
'type' => 'info',
'text' => $location_info,
),
'file_end' => array(
'id' => self::PREFIX . 'settings',

View File

@@ -11,8 +11,10 @@ namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
class Onboarding {
/**
* Initialize onboarding functionality.
*
* @internal This method is for internal purposes only.
*/
public static function init() {
final public static function init() {
OnboardingHelper::instance()->init();
OnboardingIndustries::init();
OnboardingJetpack::instance()->init();
@@ -21,5 +23,6 @@ class Onboarding {
OnboardingSetupWizard::instance()->init();
OnboardingSync::instance()->init();
OnboardingThemes::init();
OnboardingFonts::init();
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
use Automattic\WooCommerce\Internal\Font\FontFace;
use Automattic\WooCommerce\Internal\Font\FontFamily;
/**
* Class to install fonts for the Assembler.
*
* @internal
*/
class OnboardingFonts {
/**
* Initialize the class.
*
* @internal This method is for internal purposes only.
*/
final public static function init() {
add_action( 'woocommerce_install_assembler_fonts', array( __CLASS__, 'install_fonts' ) );
add_filter( 'update_option_woocommerce_allow_tracking', array( self::class, 'start_install_fonts_async_job' ), 10, 2 );
}
const SOURCE_LOGGER = 'font_loader';
/**
* Font families to install.
* PHP version of https://github.com/woocommerce/woocommerce/blob/45923dc5f38150c717210ae9db10045cd9582331/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts/#L13-L74
*
* @var array
*/
const FONT_FAMILIES_TO_INSTALL = array(
'inter' => array(
'fontFamily' => 'Inter',
'fontWeights' => array( '400', '500', '600' ),
'fontStyles' => array( 'normal' ),
),
'bodoni-moda' => array(
'fontFamily' => 'Bodoni Moda',
'fontWeights' => array( '400' ),
'fontStyles' => array( 'normal' ),
),
'overpass' => array(
'fontFamily' => 'Overpass',
'fontWeights' => array( '300', '400' ),
'fontStyles' => array( 'normal' ),
),
'albert-sans' => array(
'fontFamily' => 'Albert Sans',
'fontWeights' => array( '700' ),
'fontStyles' => array( 'normal' ),
),
'lora' => array(
'fontFamily' => 'Lora',
'fontWeights' => array( '400' ),
'fontStyles' => array( 'normal' ),
),
'montserrat' => array(
'fontFamily' => 'Montserrat',
'fontWeights' => array( '500', '700' ),
'fontStyles' => array( 'normal' ),
),
'arvo' => array(
'fontFamily' => 'Arvo',
'fontWeights' => array( '400' ),
'fontStyles' => array( 'normal' ),
),
'rubik' => array(
'fontFamily' => 'Rubik',
'fontWeights' => array( '400', '800' ),
'fontStyles' => array( 'normal' ),
),
'newsreader' => array(
'fontFamily' => 'Newsreader',
'fontWeights' => array( '400' ),
'fontStyles' => array( 'normal' ),
),
'cormorant' => array(
'fontFamily' => 'Cormorant',
'fontWeights' => array( '400', '500' ),
'fontStyles' => array( 'normal' ),
),
'work-sans' => array(
'fontFamily' => 'Work Sans',
'fontWeights' => array( '400' ),
'fontStyles' => array( 'normal' ),
),
'raleway' => array(
'fontFamily' => 'Raleway',
'fontWeights' => array( '700' ),
'fontStyles' => array( 'normal' ),
),
);
/**
* Start install fonts async job.
*
* @param string $old_value Old option value.
* @param string $value Option value.
* @return string
*/
public static function start_install_fonts_async_job( $old_value, $value ) {
if ( 'yes' !== $value || ! class_exists( 'WP_Font_Library' ) ) {
return;
}
WC()->call_function(
'as_schedule_single_action',
WC()->call_function( 'time' ),
'woocommerce_install_assembler_fonts',
);
}
/**
* Create Font Families and Font Faces.
*
* @return void
*/
public static function install_fonts() {
$collections = \WP_Font_Library::get_instance()->get_font_collections();
$google_fonts = $collections['google-fonts']->get_data();
$font_collection = $google_fonts['font_families'];
$slug_font_families_to_install = array_keys( self::FONT_FAMILIES_TO_INSTALL );
$installed_font_families = self::install_font_families( $slug_font_families_to_install, $font_collection );
if ( ! empty( $installed_font_families ) ) {
$font_faces_from_collection = self::get_font_faces_data_from_font_collection( $slug_font_families_to_install, $font_collection );
self::install_font_faces( $slug_font_families_to_install, $installed_font_families, $font_faces_from_collection );
}
}
/**
* Install font families.
*
* @param array $slug_font_families_to_install Font families to install.
* @param array $font_collection Font collection.
* @return array
*/
private static function install_font_families( $slug_font_families_to_install, $font_collection ) {
return array_reduce(
$slug_font_families_to_install,
function( $carry, $slug ) use ( $font_collection ) {
$font_family_from_collection = self::get_font_family_by_slug_from_font_collection( $slug, $font_collection );
$font_family_name = $font_family_from_collection['fontFamily'];
$font_family_installed = FontFamily::get_font_family_by_name( $font_family_name );
if ( $font_family_installed ) {
return array_merge( $carry, array( $slug => $font_family_installed ) );
}
$font_family_settings = array(
'fontFamily' => $font_family_from_collection['fontFamily'],
'preview' => $font_family_from_collection['preview'],
'slug' => $font_family_from_collection['slug'],
'name' => $font_family_from_collection['name'],
);
$font_family_id = FontFamily::insert_font_family( $font_family_settings );
if ( is_wp_error( $font_family_id ) ) {
if ( 'duplicate_font_family' !== $font_family_id->get_error_code() ) {
wc_get_logger()->error(
sprintf(
'Font Family installation error: %s',
$font_family_id->get_error_message(),
),
array( 'source' => self::SOURCE_LOGGER )
);
}
return $carry;
}
return array_merge( $carry, array( $slug => get_post( $font_family_id ) ) );
},
array(),
);
}
/**
* Install font faces.
*
* @param array $slug_font_families_to_install Font families to install.
* @param array $installed_font_families Installed font families.
* @param array $font_faces_from_collection Font faces from collection.
*/
private static function install_font_faces( $slug_font_families_to_install, $installed_font_families, $font_faces_from_collection ) {
foreach ( $slug_font_families_to_install as $slug ) {
$font_family = $installed_font_families[ $slug ];
$font_faces = $font_faces_from_collection[ $slug ];
$font_faces_to_install = self::FONT_FAMILIES_TO_INSTALL[ $slug ]['fontWeights'];
foreach ( $font_faces as $font_face ) {
if ( ! in_array( $font_face['fontWeight'], $font_faces_to_install, true ) ) {
continue;
}
$slug = \WP_Font_Utils::get_font_face_slug( $font_face );
$font_face_installed = FontFace::get_installed_font_faces_by_slug( $slug );
if ( $font_face_installed ) {
continue;
}
$wp_error = FontFace::insert_font_face( $font_face, $font_family->ID );
if ( is_wp_error( $wp_error ) ) {
wc_get_logger()->error(
sprintf(
/* translators: %s: error message */
__( 'Font Face installation error: %s', 'woocommerce' ),
$wp_error->get_error_message()
),
array( 'source' => self::SOURCE_LOGGER )
);
}
}
}
}
/**
* Get font faces data from font collection.
*
* @param array $slug_font_families_to_install Font families to install.
* @param array $font_collection Font collection.
* @return array
*/
private static function get_font_faces_data_from_font_collection( $slug_font_families_to_install, $font_collection ) {
return array_reduce(
$slug_font_families_to_install,
function( $carry, $slug ) use ( $font_collection ) {
$font_family = self::get_font_family_by_slug_from_font_collection( $slug, $font_collection );
if ( ! $font_family ) {
return $carry;
}
return array_merge( $carry, array( $slug => $font_family['fontFace'] ) );
},
array()
);
}
/**
* Get font family by slug from font collection.
*
* @param string $slug Font slug.
* @param array $font_families_collection Font families collection.
* @return array|null
*/
private static function get_font_family_by_slug_from_font_collection( $slug, $font_families_collection ) {
$font_family = null;
foreach ( $font_families_collection as $font_family ) {
if ( $font_family['font_family_settings']['slug'] === $slug ) {
$font_family = $font_family['font_family_settings'];
break;
}
}
return $font_family;
}
}

View File

@@ -20,7 +20,7 @@ class EvaluateExtension {
* @param object $extension The extension to evaluate.
* @return object The evaluated extension.
*/
public static function evaluate( $extension ) {
private static function evaluate( $extension ) {
global $wp_version;
$rule_evaluator = new RuleEvaluator();
@@ -49,4 +49,44 @@ class EvaluateExtension {
return $extension;
}
/**
* Evaluates the specs and returns the bundles with visible extensions.
*
* @param array $specs extensions spec array.
* @param array $allowed_bundles Optional array of allowed bundles to be returned.
* @return array The bundles and errors.
*/
public static function evaluate_bundles( $specs, $allowed_bundles = array() ) {
$bundles = array();
foreach ( $specs as $spec ) {
$spec = (object) $spec;
$bundle = (array) $spec;
$bundle['plugins'] = array();
if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) {
continue;
}
$errors = array();
foreach ( $spec->plugins as $plugin ) {
try {
$extension = self::evaluate( (object) $plugin );
if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
$bundle['plugins'][] = $extension;
}
} catch ( \Throwable $e ) {
$errors[] = $e;
}
}
$bundles[] = $bundle;
}
return array(
'bundles' => $bundles,
'errors' => $errors,
);
}
}

View File

@@ -8,12 +8,13 @@ namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\DefaultFreeExtensions;
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
/**
* Remote Payment Methods engine.
* This goes through the specs and gets eligible payment methods.
*/
class Init {
class Init extends RemoteSpecsEngine {
/**
* Constructor.
@@ -29,34 +30,39 @@ class Init {
* @return array
*/
public static function get_extensions( $allowed_bundles = array() ) {
$bundles = array();
$specs = self::get_specs();
$locale = get_user_locale();
foreach ( $specs as $spec ) {
$spec = (object) $spec;
$bundle = (array) $spec;
$bundle['plugins'] = array();
$specs = self::get_specs();
$results = EvaluateExtension::evaluate_bundles( $specs, $allowed_bundles );
$specs_to_return = $results['bundles'];
$specs_to_save = null;
if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) {
continue;
$plugins = array_filter(
$results['bundles'],
function( $bundle ) {
return count( $bundle['plugins'] ) > 0;
}
);
foreach ( $spec->plugins as $plugin ) {
try {
$extension = EvaluateExtension::evaluate( (object) $plugin );
if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
$bundle['plugins'][] = $extension;
}
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch ( \Throwable $e ) {
// Ignore errors.
}
}
$bundles[] = $bundle;
if ( empty( $plugins ) ) {
// When no plugins are visible, replace it with defaults and save for 3 hours.
$specs_to_save = DefaultFreeExtensions::get_all();
$specs_to_return = EvaluateExtension::evaluate_bundles( $specs_to_save, $allowed_bundles )['bundles'];
} elseif ( count( $results['errors'] ) > 0 ) {
// When suggestions is not empty but has errors, save it for 3 hours.
$specs_to_save = $specs;
}
return $bundles;
// When plugins is not empty but has errors, save it for 3 hours.
if ( count( $results['errors'] ) > 0 ) {
self::log_errors( $results['errors'] );
}
if ( $specs_to_save ) {
RemoteFreeExtensionsDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs_to_save ), 3 * HOUR_IN_SECONDS );
}
return $specs_to_return;
}
/**

View File

@@ -288,6 +288,8 @@ class WCAdminAssets {
'wc-components',
'wc-customer-effort-score',
'wc-experimental',
'wc-navigation',
'wc-product-editor',
WC_ADMIN_APP,
);

View File

@@ -7,15 +7,14 @@ namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\DataSourcePoller;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\EvaluateSuggestion;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaySuggestionsDataSourcePoller as PaymentGatewaySuggestionsDataSourcePoller;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
/**
* WC Pay Promotion engine.
*/
class Init {
class Init extends RemoteSpecsEngine {
const EXPLAT_VARIATION_PREFIX = 'woocommerce_wc_pay_promotion_payment_methods_table_';
/**
@@ -30,8 +29,8 @@ class Init {
}
add_filter( 'woocommerce_payment_gateways', array( __CLASS__, 'possibly_register_pre_install_wc_pay_promotion_gateway' ) );
add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] );
add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] );
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' : '';
@@ -122,28 +121,19 @@ class Init {
* Go through the specs and run them.
*/
public static function get_promotions() {
$suggestions = array();
$specs = self::get_specs();
$locale = get_user_locale();
foreach ( $specs as $spec ) {
try {
$suggestion = EvaluateSuggestion::evaluate( $spec );
$suggestions[] = $suggestion;
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch ( \Throwable $e ) {
// Ignore errors.
}
$specs = self::get_specs();
$results = EvaluateSuggestion::evaluate_specs( $specs );
if ( count( $results['errors'] ) > 0 ) {
// Unlike payment gateway suggestions, we don't have a non-empty default set of promotions to fall back to.
// So just set the specs transient with expired time to 3 hours.
WCPayPromotionDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs ), 3 * HOUR_IN_SECONDS );
self::log_errors( $results['errors'] );
}
return array_values(
array_filter(
$suggestions,
function( $suggestion ) {
return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
}
)
);
return $results['suggestions'];
}
/**

View File

@@ -11,7 +11,6 @@ use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\PluginUtil;
use ActionScheduler;
use WC_Admin_Settings;
defined( 'ABSPATH' ) || exit;
@@ -68,6 +67,13 @@ class CustomOrdersTableController {
*/
private $data_synchronizer;
/**
* The data cleanup instance to use.
*
* @var LegacyDataCleanup
*/
private $data_cleanup;
/**
* The batch processing controller to use.
*
@@ -116,7 +122,7 @@ class CustomOrdersTableController {
private function init_hooks() {
self::add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 );
self::add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 );
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_hpos_tools' ), 999 );
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
@@ -131,6 +137,7 @@ class CustomOrdersTableController {
* @internal
* @param OrdersTableDataStore $data_store The data store to use.
* @param DataSynchronizer $data_synchronizer The data synchronizer to use.
* @param LegacyDataCleanup $data_cleanup The legacy data cleanup instance to use.
* @param OrdersTableRefundDataStore $refund_data_store The refund data store to use.
* @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
* @param FeaturesController $features_controller The features controller instance to use.
@@ -141,6 +148,7 @@ class CustomOrdersTableController {
final public function init(
OrdersTableDataStore $data_store,
DataSynchronizer $data_synchronizer,
LegacyDataCleanup $data_cleanup,
OrdersTableRefundDataStore $refund_data_store,
BatchProcessingController $batch_processing_controller,
FeaturesController $features_controller,
@@ -150,6 +158,7 @@ class CustomOrdersTableController {
) {
$this->data_store = $data_store;
$this->data_synchronizer = $data_synchronizer;
$this->data_cleanup = $data_cleanup;
$this->batch_processing_controller = $batch_processing_controller;
$this->refund_data_store = $refund_data_store;
$this->features_controller = $features_controller;
@@ -218,11 +227,15 @@ class CustomOrdersTableController {
* @param array $tools_array The array of tools to add the tool to.
* @return array The updated array of tools-
*/
private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ): array {
private function add_hpos_tools( array $tools_array ): array {
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
return $tools_array;
}
// Cleanup tool.
$tools_array = array_merge( $tools_array, $this->data_cleanup->get_tools_entries() );
// Delete HPOS tables tool.
if ( $this->custom_orders_table_usage_is_enabled() || $this->data_synchronizer->data_sync_is_enabled() ) {
$disabled = true;
$message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" is not authoritative and sync is disabled (via Settings > Advanced > Features).', 'woocommerce' );
@@ -336,6 +349,7 @@ class CustomOrdersTableController {
return;
}
$this->data_cleanup->toggle_flag( false );
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
}

View File

@@ -281,6 +281,7 @@ class DataSynchronizer implements BatchProcessorInterface {
}
if ( $this->data_sync_is_enabled() ) {
wc_get_container()->get( LegacyDataCleanup::class )->toggle_flag( false );
$this->batch_processing_controller->enqueue_processor( self::class );
} else {
$this->batch_processing_controller->remove_processor( self::class );

View File

@@ -0,0 +1,326 @@
<?php
/**
* LegacyDataCleanup class file.
*/
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
defined( 'ABSPATH' ) || exit;
/**
* This class handles the background process in charge of cleaning up legacy data for orders when HPOS is authoritative.
*/
class LegacyDataCleanup implements BatchProcessorInterface {
use AccessiblePrivateMethods;
/**
* Option name for this feature.
*/
public const OPTION_NAME = 'woocommerce_hpos_legacy_data_cleanup_in_progress';
/**
* The default number of orders to process per batch.
*/
private const BATCH_SIZE = 25;
/**
* The batch processing controller to use.
*
* @var BatchProcessingController
*/
private $batch_processing;
/**
* The legacy handler to use for the actual cleanup.
*
* @var LegacyHandler
*/
private $legacy_handler;
/**
* The data synchronizer object to use.
*
* @var DataSynchronizer
*/
private $data_synchronizer;
/**
* Logger object to be used to log events.
*
* @var \WC_Logger
*/
private $error_logger;
/**
* Class constructor.
*/
public function __construct() {
self::add_filter( 'pre_update_option_' . self::OPTION_NAME, array( $this, 'pre_update_option' ), 999, 2 );
self::add_action( 'add_option_' . self::OPTION_NAME, array( $this, 'process_added_option' ), 999, 2 );
self::add_action( 'update_option_' . self::OPTION_NAME, array( $this, 'process_updated_option' ), 999, 2 );
self::add_action( 'delete_option_' . self::OPTION_NAME, array( $this, 'process_deleted_option' ), 999 );
self::add_action( 'shutdown', array( $this, 'maybe_reset_state' ) );
}
/**
* Class initialization, invoked by the DI container.
*
* @param BatchProcessingController $batch_processing The batch processing controller to use.
* @param LegacyDataHandler $legacy_handler Legacy order data handler instance.
* @param DataSynchronizer $data_synchronizer Data synchronizer instance.
* @internal
*/
final public function init( BatchProcessingController $batch_processing, LegacyDataHandler $legacy_handler, DataSynchronizer $data_synchronizer ) {
$this->legacy_handler = $legacy_handler;
$this->data_synchronizer = $data_synchronizer;
$this->batch_processing = $batch_processing;
$this->error_logger = wc_get_logger();
}
/**
* A user friendly name for this process.
*
* @return string Name of the process.
*/
public function get_name(): string {
return 'Order legacy data cleanup';
}
/**
* A user friendly description for this process.
*
* @return string Description.
*/
public function get_description(): string {
return 'Cleans up order data from legacy tables.';
}
/**
* Get total number of pending records that require update.
*
* @return int Number of pending records.
*/
public function get_total_pending_count(): int {
return $this->should_run() ? $this->legacy_handler->count_orders_for_cleanup() : 0;
}
/**
* Returns the batch with records that needs to be processed for a given size.
*
* @param int $size Size of the batch.
* @return array Batch of records.
*/
public function get_next_batch_to_process( int $size ): array {
return $this->should_run()
? array_map( 'absint', $this->legacy_handler->get_orders_for_cleanup( array(), $size ) )
: array();
}
/**
* Process data for current batch.
*
* @param array $batch Batch details.
*/
public function process_batch( array $batch ): void {
// This is a destructive operation, so check if we need to bail out just in case.
if ( ! $this->should_run() ) {
$this->toggle_flag( false );
return;
}
$batch_failed = true;
foreach ( $batch as $order_id ) {
try {
$this->legacy_handler->cleanup_post_data( absint( $order_id ) );
$batch_failed = false;
} catch ( \Exception $e ) {
$this->error_logger->error(
sprintf(
// translators: %1$d is an order ID, %2$s is an error message.
__( 'Order %1$d legacy data could not be cleaned up during batch process. Error: %2$s', 'woocommerce' ),
$order_id,
$e->getMessage()
)
);
}
}
if ( $batch_failed ) {
$this->error_logger->error( __( 'Order legacy cleanup failed for an entire batch of orders. Aborting cleanup.', 'woocommerce' ) );
}
if ( ! $this->orders_pending() || $batch_failed ) {
$this->toggle_flag( false );
}
}
/**
* Default batch size to use.
*
* @return int Default batch size.
*/
public function get_default_batch_size(): int {
return self::BATCH_SIZE;
}
/**
* Determine whether the cleanup process can be initiated. Legacy data cleanup requires HPOS to be authoritative and
* compatibility mode to be disabled.
*
* @return boolean TRUE if the cleanup process can be enabled, FALSE otherwise.
*/
public function can_run() {
return $this->data_synchronizer->custom_orders_table_is_authoritative() && ! $this->data_synchronizer->data_sync_is_enabled() && ! $this->batch_processing->is_enqueued( get_class( $this->data_synchronizer ) );
}
/**
* Checks whether the cleanup process should run. That is, it must be activated and {@see can_run()} must return TRUE.
*
* @return boolean TRUE if the cleanup process should be run, FALSE otherwise.
*/
public function should_run() {
return $this->can_run() && $this->is_flag_set();
}
/**
* Whether the user has initiated the cleanup process.
*
* @return boolean TRUE if the user has initiated the cleanup process, FALSE otherwise.
*/
public function is_flag_set() {
return 'yes' === get_option( self::OPTION_NAME, 'no' );
}
/**
* Sets the flag that indicates that the cleanup process should be initiated.
*
* @param boolean $enabled TRUE if the process should be initiated, FALSE if it should be canceled.
*/
public function toggle_flag( bool $enabled ) {
if ( $enabled ) {
update_option( self::OPTION_NAME, wc_bool_to_string( $enabled ) );
} else {
delete_option( self::OPTION_NAME );
}
}
/**
* Returns an array in format required by 'woocommerce_debug_tools' to register the cleanup tool in WC.
*
* @return array Tools entries to register with WC.
*/
public function get_tools_entries() {
$orders_for_cleanup_exist = ! empty( $this->legacy_handler->get_orders_for_cleanup( array(), 1 ) );
$entry_id = $this->is_flag_set() ? 'hpos_legacy_cleanup_cancel' : 'hpos_legacy_cleanup';
$entry = array(
'name' => __( 'Clean up order data from legacy tables', 'woocommerce' ),
'desc' => __( 'This tool will clear the data from legacy order tables in WooCommerce.', 'woocommerce' ),
'requires_refresh' => true,
'button' => __( 'Clear data', 'woocommerce' ),
'disabled' => ! ( $this->can_run() && ( $orders_for_cleanup_exist || $this->is_flag_set() ) ),
);
if ( ! $this->can_run() ) {
$entry['desc'] .= '<br />';
$entry['desc'] .= sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'Only available when HPOS is authoritative and compatibility mode is disabled.', 'woocommerce' )
);
} else {
if ( $this->is_flag_set() ) {
$entry['status_text'] = sprintf(
'%1$s %2$s',
'<span class="dashicons dashicons-update spin"></span>',
__( 'Clearing data...', 'woocommerce' )
);
$entry['button'] = __( 'Cancel', 'woocommerce' );
$entry['callback'] = function() {
$this->toggle_flag( false );
return __( 'Order legacy data cleanup has been canceled.', 'woocommerce' );
};
} elseif ( ! $orders_for_cleanup_exist ) {
$entry['button'] = __( 'No orders in need of cleanup', 'woocommerce' );
} else {
$entry['callback'] = function() {
$this->toggle_flag( true );
return __( 'Order legacy data cleanup process has been started.', 'woocommerce' );
};
}
}
return array( $entry_id => $entry );
}
/**
* Hooked onto 'add_option' to enqueue the batch processor (if needed).
*
* @param string $option Name of the option to add.
* @param mixed $value Value of the option.
*/
private function process_added_option( string $option, $value ) {
$this->process_updated_option( false, $value );
}
/**
* Hooked onto 'delete_option' to remove the batch processor.
*/
private function process_deleted_option() {
$this->process_updated_option( false, false );
}
/**
* Hooked onto 'update_option' to enqueue the batch processor as needed.
*
* @param mixed $old_value Previous option value.
* @param mixed $new_value New option value.
*/
private function process_updated_option( $old_value, $new_value ) {
$enable = wc_string_to_bool( $new_value );
if ( $enable ) {
$this->batch_processing->enqueue_processor( self::class );
} else {
$this->batch_processing->remove_processor( self::class );
}
}
/**
* Hooked onto 'pre_update_option' to prevent enabling of the cleanup process when conditions aren't met.
*
* @param mixed $new_value New option value.
* @param mixed $old_value Previous option value.
*/
private function pre_update_option( $new_value, $old_value ) {
return $this->can_run() ? $new_value : 'no';
}
/**
* Checks whether there are any orders in need of cleanup and cleanup can run.
*
* @return bool TRUE if there are orders in need of cleanup, FALSE otherwise.
*/
private function orders_pending() {
return ! empty( $this->get_next_batch_to_process( 1 ) );
}
/**
* Hooked onto 'shutdown' to clean up or set things straight in case of failures (timeouts, etc).
*/
private function maybe_reset_state() {
$is_enqueued = $this->batch_processing->is_enqueued( self::class );
$is_flag_set = $this->is_flag_set();
if ( $is_enqueued xor $is_flag_set ) {
$this->toggle_flag( false );
$this->batch_processing->remove_processor( self::class );
}
}
}

View File

@@ -5,6 +5,7 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Utilities\ArrayUtil;
defined( 'ABSPATH' ) || exit;
@@ -28,17 +29,26 @@ class LegacyDataHandler {
*/
private DataSynchronizer $data_synchronizer;
/**
* Instance of the PostsToOrdersMigrationController.
*
* @var PostsToOrdersMigrationController
*/
private PostsToOrdersMigrationController $posts_to_cot_migrator;
/**
* Class initialization, invoked by the DI container.
*
* @param OrdersTableDataStore $data_store HPOS datastore instance to use.
* @param DataSynchronizer $data_synchronizer DataSynchronizer instance to use.
* @param OrdersTableDataStore $data_store HPOS datastore instance to use.
* @param DataSynchronizer $data_synchronizer DataSynchronizer instance to use.
* @param PostsToOrdersMigrationController $posts_to_cot_migrator Posts to HPOS migration controller instance to use.
*
* @internal
*/
final public function init( OrdersTableDataStore $data_store, DataSynchronizer $data_synchronizer ) {
$this->data_store = $data_store;
$this->data_synchronizer = $data_synchronizer;
final public function init( OrdersTableDataStore $data_store, DataSynchronizer $data_synchronizer, PostsToOrdersMigrationController $posts_to_cot_migrator ) {
$this->data_store = $data_store;
$this->data_synchronizer = $data_synchronizer;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
}
/**
@@ -151,10 +161,13 @@ class LegacyDataHandler {
throw new \Exception( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables.', 'woocommerce' ) ) );
}
$meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id = %d", $order->get_id() ) );
foreach ( $meta_ids as $meta_id ) {
delete_metadata_by_mid( 'post', $meta_id );
}
// Delete all metadata.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->postmeta} WHERE post_id = %d",
$order->get_id()
)
);
// wp_update_post() changes the post modified date, so we do this manually.
// Also, we suspect using wp_update_post() could lead to integrations mistakenly updating the entity.
@@ -208,7 +221,7 @@ class LegacyDataHandler {
$diff = array();
$hpos_order = $this->get_order_from_datastore( $order_id, 'hpos' );
$cpt_order = $this->get_order_from_datastore( $order_id, 'cpt' );
$cpt_order = $this->get_order_from_datastore( $order_id, 'posts' );
if ( $hpos_order->get_type() !== $cpt_order->get_type() ) {
$diff['type'] = array( $hpos_order->get_type(), $cpt_order->get_type() );
@@ -252,8 +265,9 @@ class LegacyDataHandler {
* @since 8.6.0
*
* @param int $order_id Order ID.
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'cpt'. Defaults to 'hpos'.
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'posts'. Defaults to 'hpos'.
* @return \WC_Order Order instance.
* @throws \Exception When an error occurs.
*/
public function get_order_from_datastore( int $order_id, string $data_store_id = 'hpos' ) {
$data_store = ( 'hpos' === $data_store_id ) ? $this->data_store : $this->data_store->get_cpt_data_store_instance();
@@ -265,7 +279,14 @@ class LegacyDataHandler {
$data_store->prime_caches_for_orders( array( $order_id ), array() );
}
$classname = wc_get_order_type( $data_store->get_order_type( $order_id ) )['class_name'];
$order_type = wc_get_order_type( $data_store->get_order_type( $order_id ) );
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 ) );
}
$classname = $order_type['class_name'];
$order = new $classname();
$order->set_id( $order_id );
@@ -292,6 +313,38 @@ class LegacyDataHandler {
return $order;
}
/**
* Backfills an order from/to the CPT or HPOS datastore.
*
* @since 8.7.0
*
* @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'.
* @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 ) {
$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 ) );
}
$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;
}
}
/**
* Returns all metadata in an order object as an array.
*

View File

@@ -2550,6 +2550,9 @@ FROM $order_meta_table
}
$this->persist_order_to_db( $order );
$this->update_order_meta( $order );
$order->save_meta_data();
if ( $backfill ) {
@@ -2882,6 +2885,8 @@ CREATE TABLE $meta_table (
* @return bool
*/
public function delete_meta( &$object, $meta ) {
global $wpdb;
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
// Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables.
$db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id );
@@ -2896,7 +2901,23 @@ CREATE TABLE $meta_table (
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
self::$backfilling_order_ids[] = $object->get_id();
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
if ( is_object( $meta->value ) && '__PHP_Incomplete_Class' === get_class( $meta->value ) ) {
$meta_value = maybe_serialize( $meta->value );
$wpdb->delete(
_get_meta_table( 'post' ),
array(
'post_id' => $object->get_id(),
'meta_key' => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
),
array( '%d', '%s', '%s' )
);
wp_cache_delete( $object->get_id(), 'post_meta' );
$logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
$logger->warning( sprintf( 'encountered an order meta value of type __PHP_Incomplete_Class during `delete_meta` in order with ID %d: "%s"', $object->get_id(), var_export( $meta_value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
} else {
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
}
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}

View File

@@ -48,6 +48,13 @@ class ExtendedContainer extends BaseContainer {
Container::class,
);
/**
* A list of tags that have already been fully resolved, see 'get' for details.
*
* @var array
*/
private array $known_tags = array();
/**
* Register a class in the container.
*
@@ -155,6 +162,17 @@ class ExtendedContainer extends BaseContainer {
throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" );
}
// This is a workaround for an issue that arises when using service providers inheriting from AbstractInterfaceServiceProvider:
// if one of these providers registers classes both by name and by tag, and one of its registered classes is requested
// with 'get' by name before a list of classes is requested by tag, then that service provider gets locked as
// the only one providing that tag, and the others get ignored. This is due to the fact that container definitions
// are created "on the fly" as needed and the base 'get' method won't try to register additional providers
// if the requested tag is already provided by at least one of the already existing definitions.
if ( $this->definitions->hasTag( $id ) && ! in_array( $id, $this->known_tags, true ) && $this->providers->provides( $id ) ) {
$this->providers->register( $id );
$this->known_tags[] = $id;
}
return parent::get( $id, $new );
}

View File

@@ -1,19 +1,20 @@
<?php
/**
* Service provider for ActionUpdateController class.
* Service provider for the batch processing classes.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\OrderCouponDataMigrator;
/**
* Class BatchProcessingServiceProvider
*
* @package Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders
*/
class BatchProcessingServiceProvider extends AbstractServiceProvider {
class BatchProcessingServiceProvider extends AbstractInterfaceServiceProvider {
/**
* Services provided by this provider.
@@ -22,6 +23,7 @@ class BatchProcessingServiceProvider extends AbstractServiceProvider {
*/
protected $provides = array(
BatchProcessingController::class,
OrderCouponDataMigrator::class,
);
/**
@@ -33,5 +35,6 @@ class BatchProcessingServiceProvider extends AbstractServiceProvider {
*/
public function register() {
$this->share( BatchProcessingController::class, new BatchProcessingController() );
$this->share_with_implements_tags( OrderCouponDataMigrator::class );
}
}

View File

@@ -5,7 +5,8 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\ReceiptRendering\ReceiptRenderingEngine;
use Automattic\WooCommerce\Internal\ReceiptRendering\ReceiptRenderingRestController;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
@@ -21,6 +22,8 @@ class EnginesServiceProvider extends AbstractInterfaceServiceProvider {
*/
protected $provides = array(
TransientFilesEngine::class,
ReceiptRenderingEngine::class,
ReceiptRenderingRestController::class,
);
/**
@@ -28,5 +31,7 @@ class EnginesServiceProvider extends AbstractInterfaceServiceProvider {
*/
public function register() {
$this->share_with_implements_tags( TransientFilesEngine::class )->addArgument( LegacyProxy::class );
$this->share( ReceiptRenderingEngine::class )->addArguments( array( TransientFilesEngine::class, LegacyProxy::class ) );
$this->share_with_implements_tags( ReceiptRenderingRestController::class );
}
}

View File

@@ -8,7 +8,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Caching\TransientsEngine;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
@@ -16,6 +15,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableRefundDataStore
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\LegacyDataCleanup;
use Automattic\WooCommerce\Internal\DataStores\Orders\LegacyDataHandler;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStoreMeta;
@@ -44,6 +44,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
OrderCache::class,
OrderCacheController::class,
LegacyDataHandler::class,
LegacyDataCleanup::class,
);
/**
@@ -68,6 +69,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
array(
OrdersTableDataStore::class,
DataSynchronizer::class,
LegacyDataCleanup::class,
OrdersTableRefundDataStore::class,
BatchProcessingController::class,
FeaturesController::class,
@@ -82,6 +84,19 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
$this->share( CLIRunner::class )->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class, PostsToOrdersMigrationController::class ) );
}
$this->share( LegacyDataHandler::class )->addArguments( array( OrdersTableDataStore::class, DataSynchronizer::class ) );
$this->share( LegacyDataCleanup::class )->addArguments(
array(
BatchProcessingController::class,
LegacyDataHandler::class,
DataSynchronizer::class,
)
);
$this->share( LegacyDataHandler::class )->addArguments(
array(
OrdersTableDataStore::class,
DataSynchronizer::class,
PostsToOrdersMigrationController::class,
)
);
}
}

View File

@@ -6,6 +6,7 @@ use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\GroupInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SectionInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SubsectionInterface;
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\AbstractBlockTemplate;
/**
@@ -47,6 +48,20 @@ abstract class AbstractProductFormTemplate extends AbstractBlockTemplate impleme
return $section;
}
/**
* Get a subsection block by ID.
*
* @param string $subsection_id The subsection block ID.
* @throws \UnexpectedValueException If block is not of type SubsectionInterface.
*/
public function get_subsection_by_id( string $subsection_id ): ?SubsectionInterface {
$subsection = $this->get_block( $subsection_id );
if ( $subsection && ! $subsection instanceof SubsectionInterface ) {
throw new \UnexpectedValueException( 'Block with specified ID is not a subsection.' );
}
return $subsection;
}
/**
* Get a block by ID.
*

View File

@@ -0,0 +1,78 @@
<?php
/**
* DownloadableProductTrait
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\GroupInterface;
/**
* Downloadable Product Trait.
*/
trait DownloadableProductTrait {
/**
* Adds downloadable blocks to the given parent block.
*
* @param GroupInterface $parent_block The parent block.
*/
private function add_downloadable_product_blocks( $parent_block ) {
// Downloads section.
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
$product_downloads_section_group = $parent_block->add_section(
array(
'id' => 'product-downloads-section-group',
'order' => 50,
'attributes' => array(
'blockGap' => 'unit-40',
),
'hideConditions' => array(
array(
'expression' => 'postType === "product" && editedProduct.type !== "simple"',
),
),
)
);
$product_downloads_section_group->add_block(
array(
'id' => 'product-downloadable',
'blockName' => 'woocommerce/product-checkbox-field',
'order' => 10,
'attributes' => array(
'property' => 'downloadable',
'label' => __( 'Include downloads', 'woocommerce' ),
),
)
);
$product_downloads_section_group->add_subsection(
array(
'id' => 'product-downloads-section',
'order' => 20,
'attributes' => array(
'title' => __( 'Downloads', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Downloads settings link opening tag. %2$s: Downloads settings link closing tag. */
__( 'Add any files you\'d like to make available for the customer to download after purchasing, such as instructions or warranty info. Store-wide updates can be managed in your %1$sproduct settings%2$s.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=downloadable' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
),
'hideConditions' => array(
array(
'expression' => 'editedProduct.downloadable !== true',
),
),
)
)->add_block(
array(
'id' => 'product-downloads',
'blockName' => 'woocommerce/product-downloads-field',
'order' => 10,
)
);
}
}
}

View File

@@ -7,11 +7,14 @@ namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\Prod
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.
*/
class ProductVariationTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
use DownloadableProductTrait;
/**
* The context name used to identify the editor.
*/
@@ -133,12 +136,12 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
$basic_details->add_block(
array(
'id' => 'product-variation-note',
'blockName' => 'woocommerce/product-summary-field',
'blockName' => 'woocommerce/product-text-area-field',
'order' => 20,
'attributes' => array(
'property' => 'description',
'label' => __( 'Note <optional />', 'woocommerce' ),
'helpText' => 'Enter an optional note displayed on the product page when customers select this variation.',
'label' => __( 'Note', 'woocommerce' ),
'help' => 'Enter an optional note displayed on the product page when customers select this variation.',
),
)
);
@@ -185,29 +188,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
);
// Downloads section.
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
$general_group->add_section(
array(
'id' => 'product-variation-downloads-section',
'order' => 40,
'attributes' => array(
'title' => __( 'Downloads', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Downloads settings link opening tag. %2$s: Downloads settings link closing tag. */
__( 'Add any files you\'d like to make available for the customer to download after purchasing, such as instructions or warranty info. Store-wide updates can be managed in your %1$sproduct settings%2$s.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=downloadable' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
),
)
)->add_block(
array(
'id' => 'product-variation-downloads',
'blockName' => 'woocommerce/product-downloads-field',
'order' => 10,
)
);
}
$this->add_downloadable_product_blocks( $general_group );
}
/**
@@ -374,7 +355,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
),
)
);
$product_inventory_inner_section = $product_inventory_section->add_section(
$product_inventory_inner_section = $product_inventory_section->add_subsection(
array(
'id' => 'product-variation-inventory-inner-section',
'order' => 10,

View File

@@ -8,6 +8,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\Prod
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SectionInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SubsectionInterface;
/**
* Class for Section block.
@@ -34,12 +35,24 @@ class Section extends ProductBlock implements SectionInterface {
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
/**
* Add a section block type to this template.
* Add a sub-section block type to this template.
*
* @param array $block_config The block data.
*/
public function add_section( array $block_config ): SectionInterface {
$block = new Section( $block_config, $this->get_root_template(), $this );
public function add_subsection( array $block_config ): SubsectionInterface {
$block = new Subsection( $block_config, $this->get_root_template(), $this );
return $this->add_inner_block( $block );
}
/**
* Add a sub-section block type to this template.
*
* @deprecated 8.6.0
*
* @param array $block_config The block data.
*/
public function add_section( array $block_config ): SubsectionInterface {
wc_deprecated_function( 'add_section', '8.6.0', 'add_subsection' );
return $this->add_subsection( $block_config );
}
}

View File

@@ -7,11 +7,14 @@ namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\Prod
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\DownloadableProductTrait;
/**
* Simple Product Template.
*/
class SimpleProductTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
use DownloadableProductTrait;
/**
* The context name used to identify the editor.
*/
@@ -215,12 +218,18 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
)
);
$basic_details->add_block(
array(
'id' => 'product-summary',
'blockName' => 'woocommerce/product-summary-field',
'blockName' => 'woocommerce/product-text-area-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Summary', 'woocommerce' ),
'help' => __(
"Summarize this product in 1-2 short sentences. We'll show it at the top of the page.",
'woocommerce'
),
'property' => 'short_description',
),
)
@@ -453,62 +462,9 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
)
);
// Downloads section.
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
$product_downloads_section_group = $general_group->add_section(
array(
'id' => 'product-downloads-section-group',
'order' => 50,
'attributes' => array(
'blockGap' => 'unit-40',
),
'hideConditions' => array(
array(
'expression' => 'editedProduct.type !== "simple"',
),
),
)
);
$product_downloads_section_group->add_block(
array(
'id' => 'product-downloadable',
'blockName' => 'woocommerce/product-checkbox-field',
'order' => 10,
'attributes' => array(
'property' => 'downloadable',
'label' => __( 'Include downloads', 'woocommerce' ),
),
)
);
$product_downloads_section_group->add_section(
array(
'id' => 'product-downloads-section',
'order' => 20,
'attributes' => array(
'title' => __( 'Downloads', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Downloads settings link opening tag. %2$s: Downloads settings link closing tag. */
__( 'Add any files you\'d like to make available for the customer to download after purchasing, such as instructions or warranty info. Store-wide updates can be managed in your %1$sproduct settings%2$s.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=downloadable' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
),
'hideConditions' => array(
array(
'expression' => 'editedProduct.downloadable !== true',
),
),
)
)->add_block(
array(
'id' => 'product-downloads',
'blockName' => 'woocommerce/product-downloads-field',
'order' => 10,
)
);
}
$this->add_downloadable_product_blocks( $general_group );
}
/**
@@ -807,7 +763,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
)
);
$product_inventory_inner_section = $product_inventory_section->add_section(
$product_inventory_inner_section = $product_inventory_section->add_subsection(
array(
'id' => 'product-inventory-inner-section',
'order' => 10,
@@ -1077,51 +1033,34 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
if ( ! $variation_group ) {
return;
}
$variation_fields = $variation_group->add_block(
array(
'id' => 'product_variation-field-group',
'blockName' => 'woocommerce/product-variations-fields',
'order' => 10,
'attributes' => array(
'description' => sprintf(
/* translators: %1$s: Sell your product in multiple variations like size or color. strong opening tag. %2$s: Sell your product in multiple variations like size or color. strong closing tag.*/
__( '%1$sSell your product in multiple variations like size or color.%2$s Get started by adding options for the buyers to choose on the product page.', 'woocommerce' ),
'<strong>',
'</strong>'
),
),
)
);
$variation_options_section = $variation_fields->add_block(
$variation_group->add_section(
array(
'id' => 'product-variation-options-section',
'blockName' => 'woocommerce/product-section',
'order' => 10,
'attributes' => array(
'title' => __( 'Variation options', 'woocommerce' ),
'description' => __( 'Add and manage attributes used for product options, such as size and color.', 'woocommerce' ),
),
)
);
$variation_options_section->add_block(
)->add_block(
array(
'id' => 'product-variation-options',
'blockName' => 'woocommerce/product-variations-options-field',
'order' => 10,
)
);
$variation_section = $variation_fields->add_block(
$variation_group->add_section(
array(
'id' => 'product-variation-section',
'blockName' => 'woocommerce/product-section',
'order' => 20,
'attributes' => array(
'title' => __( 'Variations', 'woocommerce' ),
'description' => __( 'Manage individual product combinations created from options.', 'woocommerce' ),
),
)
);
$variation_section->add_block(
)->add_block(
array(
'id' => 'product-variation-items',
'blockName' => 'woocommerce/product-variation-items-field',

View File

@@ -0,0 +1,35 @@
<?php
/**
* WooCommerce Subsection Block class.
*/
namespace Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SubsectionInterface;
/**
* Class for Subsection block.
*/
class Subsection extends ProductBlock implements SubsectionInterface {
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
/**
* Subsection Block constructor.
*
* @param array $config The block configuration.
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
* @param ContainerInterface|null $parent The parent block container.
*
* @throws \ValueError If the block configuration is invalid.
* @throws \ValueError If the parent block container does not belong to the same template as the block.
* @throws \InvalidArgumentException If blockName key and value are passed into block configuration.
*/
public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
if ( ! empty( $config['blockName'] ) ) {
throw new \InvalidArgumentException( 'Unexpected key "blockName", this defaults to "woocommerce/product-subsection".' );
}
parent::__construct( array_merge( array( 'blockName' => 'woocommerce/product-subsection' ), $config ), $root_template, $parent );
}
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
}

View File

@@ -0,0 +1,234 @@
<?php
/**
* FontFace class file
*/
namespace Automattic\WooCommerce\Internal\Font;
// IMPORTANT: We have to switch to the WordPress API to create the FontFace post type when they will be implemented: https://github.com/WordPress/gutenberg/issues/58670!
/**
* Helper class for font face related functionality.
*
* @internal Just for internal use.
*/
class FontFace {
const POST_TYPE = 'wp_font_face';
/**
* Gets the installed font face by slug.
*
* @param string $slug The font face slug.
* @return \WP_Post|null The font face post or null if not found.
*/
public static function get_installed_font_faces_by_slug( $slug ) {
$query = new \WP_Query(
array(
'post_type' => self::POST_TYPE,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'name' => $slug,
)
);
if ( ! empty( $query->get_posts() ) ) {
return $query->get_posts()[0];
}
return null;
}
/**
* Sanitizes a single src value for a font face.
*
* Copied from Gutenberg: https://github.com/WordPress/gutenberg/blob/8d94c3bd7af977d998466b56bd773f9b19de8d03/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php/#L837-L840
*
* @param string $value Font face src that is a URL or the key for a $_FILES array item.
*
* @return string Sanitized value.
*/
private static function sanitize_src( $value ) {
$value = ltrim( $value );
return false === wp_http_validate_url( $value ) ? (string) $value : esc_url_raw( $value );
}
/**
* Handles file upload error.
*
* 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
*
* @param array $file File upload data.
* @param string $message Error message from wp_handle_upload().
* @return WP_Error WP_Error object.
*/
private static function handle_font_file_upload_error( $file, $message ) {
$status = 500;
$code = 'rest_font_upload_unknown_error';
if ( __( 'Sorry, you are not allowed to upload this file type.', 'woocommerce' ) === $message ) {
$status = 400;
$code = 'rest_font_upload_invalid_file_type';
}
return new \WP_Error( $code, $message, array( 'status' => $status ) );
}
/**
* 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
*
* @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' );
$overrides = array(
'upload_error_handler' => array( self::class, 'handle_font_file_upload_error' ),
// Arbitrary string to avoid the is_uploaded_file() check applied
// when using 'wp_handle_upload'.
'action' => 'wp_handle_font_upload',
// Not testing a form submission.
'test_form' => false,
// Seems mime type for files that are not images cannot be tested.
// See wp_check_filetype_and_ext().
'test_type' => true,
// Only allow uploading font files for this request.
'mimes' => \WP_Font_Utils::get_allowed_font_mime_types(),
);
$uploaded_file = wp_handle_upload( $file, $overrides );
remove_filter( 'upload_dir', 'wp_get_font_dir' );
remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
return $uploaded_file;
}
/**
* Downloads a file from a URL.
*
* @param string $file_url The file URL.
**/
private static function download_file( $file_url ) {
if ( ! function_exists( 'download_url' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$allowed_extensions = array( 'ttf', 'otf', 'woff', 'woff2', 'eot' );
$allowed_extensions = array_map( 'preg_quote', $allowed_extensions );
// Set variables for storage, fix file filename for query strings.
preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $file_url, $matches );
$file_array = array();
$file_array['name'] = wp_basename( $matches[0] );
// Download file to temp location.
$file_array['tmp_name'] = download_url( $file_url );
return $file_array;
}
/**
* Inserts a font face.
*
* @param array $font_face The font face settings.
* @param int $parent_font_family_id The parent font family ID.
* @return \WP_Error|\WP_Post The inserted font face post or an error if the font face already exists.
*/
public static function insert_font_face( array $font_face, int $parent_font_family_id ) {
$slug = \WP_Font_Utils::get_font_face_slug( $font_face );
// Check that the font face slug is unique.
$query = new \WP_Query(
array(
'post_type' => self::POST_TYPE,
'posts_per_page' => 1,
'name' => $slug,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
if ( ! empty( $query->get_posts() ) ) {
return new \WP_Error(
'duplicate_font_face',
/* translators: %s: Font face slug. */
sprintf( __( 'A font face with slug "%s" already exists.', 'woocommerce' ), $slug ),
);
}
// Validate the font face settings.
$validation_error = self::validate_font_face( $font_face );
if ( is_wp_error( $validation_error ) ) {
return $validation_error;
}
$parsed_font_face['fontFamily'] = addslashes( \WP_Font_Utils::sanitize_font_family( $font_face['fontFamily'] ) );
$parsed_font_face['fontStyle'] = sanitize_text_field( $font_face['fontStyle'] );
$parsed_font_face['fontWeight'] = sanitize_text_field( $font_face['fontWeight'] );
$file = self::download_file( $font_face['src'] );
$uploaded_file = self::handle_font_file_upload( $file );
$parsed_font_face['src'] = self::sanitize_src( $uploaded_file['url'] );
$parsed_font_face['preview'] = esc_url_raw( $font_face['preview'] );
// Insert the font face.
wp_insert_post(
array(
'post_type' => self::POST_TYPE,
'post_parent' => $parent_font_family_id,
'post_title' => $slug,
'post_name' => sanitize_title( $slug ),
'post_content' => wp_json_encode( $parsed_font_face ),
'post_status' => 'publish',
)
);
}
/**
* Validates a font face.
*
* @param array $font_face The font face settings.
* @return \WP_Error|null The error if the font family is invalid, null otherwise.
*/
private static function validate_font_face( $font_face ) {
// Validate the font face family name.
if ( empty( $font_face['fontFamily'] ) ) {
return new \WP_Error(
'invalid_font_face_font_family',
__( 'The font face family name is required.', 'woocommerce' ),
);
}
// Validate the font face font style.
if ( empty( $font_face['fontStyle'] ) ) {
return new \WP_Error(
'invalid_font_face_font_style',
__( 'The font face font style is required.', 'woocommerce' ),
);
}
// Validate the font face weight.
if ( empty( $font_face['fontWeight'] ) ) {
return new \WP_Error(
'invalid_font_face_font_weight',
__( 'The font face weight is required.', 'woocommerce' ),
);
}
// Validate the font face src.
if ( empty( $font_face['src'] ) ) {
return new \WP_Error(
'invalid_font_face_src',
__( 'The font face src is required.', 'woocommerce' ),
);
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* FontFamily class file
*/
namespace Automattic\WooCommerce\Internal\Font;
// IMPORTANT: We have to switch to the WordPress API to create the FontFamily post type when they will be implemented: https://github.com/WordPress/gutenberg/issues/58670!
/**
* Helper class for font family related functionality.
*
* @internal Just for internal use.
*/
class FontFamily {
const POST_TYPE = 'wp_font_family';
/**
* Validates a font family.
*
* @param array $font_family The font family settings.
* @return \WP_Error|null The error if the font family is invalid, null otherwise.
*/
private static function validate_font_family( $font_family ) {
// Validate the font family name.
if ( empty( $font_family['fontFamily'] ) ) {
return new \WP_Error(
'invalid_font_family_name',
__( 'The font family name is required.', 'woocommerce' ),
);
}
// Validate the font family slug.
if ( empty( $font_family['preview'] ) ) {
return new \WP_Error(
'invalid_font_family_name_preview',
__( 'The font family preview is required.', 'woocommerce' ),
);
}
}
/**
* Registers the font family post type.
*
* @param array $font_family_settings The font family settings.
*/
public static function insert_font_family( array $font_family_settings ) {
$font_family = $font_family_settings;
// Check that the font family slug is unique.
$query = new \WP_Query(
array(
'post_type' => self::POST_TYPE,
'posts_per_page' => 1,
'name' => $font_family['slug'],
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
if ( ! empty( $query->get_posts() ) ) {
return new \WP_Error(
'duplicate_font_family',
/* translators: %s: Font family slug. */
sprintf( __( 'A font family with slug "%s" already exists.', 'woocommerce' ), $font_family['slug'] )
);
}
// Validate the font family settings.
$validation_error = self::validate_font_family( $font_family );
if ( is_wp_error( $validation_error ) ) {
return $validation_error;
}
$post['fontFamily'] = addslashes( \WP_Font_Utils::sanitize_font_family( $font_family['fontFamily'] ) );
$post['preview'] = $font_family['preview'];
// Insert the font family.
return wp_insert_post(
array(
'post_type' => self::POST_TYPE,
'post_title' => $font_family['name'],
'name' => $font_family['slug'],
'post_content' => wp_json_encode( $post ),
'post_status' => 'publish',
)
);
}
/**
* Gets a font family by name.
*
* @param string $name The font family slug.
* @return \WP_Post|null The font family post or null if not found.
*/
public static function get_font_family_by_name( $name ) {
$query = new \WP_Query(
array(
'post_type' => self::POST_TYPE,
'posts_per_page' => 1,
'title' => $name,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
if ( ! empty( $query->get_posts() ) ) {
return $query->get_posts()[0];
}
return null;
}
}

View File

@@ -0,0 +1,230 @@
<?php
namespace Automattic\WooCommerce\Internal;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\StringUtil;
use \Exception;
/**
* This class is intended to be used with BatchProcessingController and converts verbose
* 'coupon_data' metadata entries in coupon line items (corresponding to coupons applied to orders)
* into simplified 'coupon_info' entries. See WC_Coupon::get_short_info.
*
* Additionally, this class manages the "Convert order coupon data" tool.
*/
class OrderCouponDataMigrator implements BatchProcessorInterface, RegisterHooksInterface {
use AccessiblePrivateMethods;
/**
* Register hooks for the class.
*/
public function register() {
self::add_filter( 'woocommerce_debug_tools', array( $this, 'handle_woocommerce_debug_tools' ), 999, 1 );
self::mark_method_as_accessible( 'enqueue' );
self::mark_method_as_accessible( 'dequeue' );
}
/**
* Get a user-friendly name for this processor.
*
* @return string Name of the processor.
*/
public function get_name(): string {
return "Coupon line item 'coupon_data' to 'coupon_info' metadata migrator";
}
/**
* Get a user-friendly description for this processor.
*
* @return string Description of what this processor does.
*/
public function get_description(): string {
return "Migrates verbose metadata about coupons applied to an order ('coupon_data' metadata key in coupon line items) to simplified metadata ('coupon_info' keys)";
}
/**
* Get the total number of pending items that require processing.
*
* @return int Number of items pending processing.
*/
public function get_total_pending_count(): int {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key=%s",
'coupon_data'
)
);
}
/**
* Returns the next batch of items that need to be processed.
* A batch in this context is a list of 'meta_id' values from the wp_woocommerce_order_itemmeta table.
*
* @param int $size Maximum size of the batch to be returned.
*
* @return array Batch of items to process, containing $size or less items.
*/
public function get_next_batch_to_process( int $size ): array {
global $wpdb;
$meta_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT meta_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key=%s ORDER BY meta_id ASC LIMIT %d",
'coupon_data',
$size
)
);
return array_map( 'absint', $meta_ids );
}
/**
* Process data for the supplied batch. See the convert_item method.
*
* @throw \Exception Something went wrong while processing the batch.
*
* @param array $batch Batch to process, as returned by 'get_next_batch_to_process'.
*/
public function process_batch( array $batch ): void {
global $wpdb;
if ( empty( $batch ) ) {
return;
}
$meta_ids = StringUtil::to_sql_list( $batch );
$meta_ids_and_values = $wpdb->get_results(
//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT meta_id,meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_id IN $meta_ids",
ARRAY_N
);
foreach ( $meta_ids_and_values as $meta_id_and_value ) {
try {
$this->convert_item( (int) $meta_id_and_value[0], $meta_id_and_value[1] );
} catch ( Exception $ex ) {
wc_get_logger()->error( StringUtil::class_name_without_namespace( self::class ) . ": when converting meta row with id {$meta_id_and_value[0]}: {$ex->getMessage()}" );
}
}
}
/**
* Convert one verbose 'coupon_data' entry into a simplified 'coupon_info' entry.
*
* The existing database row is updated in place, both the 'meta_key' and the 'meta_value' columns.
*
* @param int $meta_id Value of 'meta_id' of the row being converted.
* @param string $meta_value Value of 'meta_value' of the row being converted.
* @throws Exception Database error.
*/
private function convert_item( int $meta_id, string $meta_value ) {
global $wpdb;
//phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
$coupon_data = unserialize( $meta_value );
$temp_coupon = new \WC_Coupon();
$temp_coupon->set_props( $coupon_data );
//phpcs:disable WordPress.DB.SlowDBQuery
$wpdb->update(
"{$wpdb->prefix}woocommerce_order_itemmeta",
array(
'meta_key' => 'coupon_info',
'meta_value' => $temp_coupon->get_short_info(),
),
array( 'meta_id' => $meta_id )
);
//phpcs:enable WordPress.DB.SlowDBQuery
if ( $wpdb->last_error ) {
throw new Exception( $wpdb->last_error );
}
}
/**
* Default (preferred) batch size to pass to 'get_next_batch_to_process'.
*
* @return int Default batch size.
*/
public function get_default_batch_size(): int {
return 1000;
}
/**
* Add the tool to start or stop the background process that converts order coupon metadata entries.
*
* @param array $tools Old tools array.
* @return array Updated tools array.
*/
private function handle_woocommerce_debug_tools( array $tools ): array {
$batch_processor = wc_get_container()->get( BatchProcessingController::class );
$pending_count = $this->get_total_pending_count();
if ( 0 === $pending_count ) {
$tools['start_convert_order_coupon_data'] = array(
'name' => __( 'Start converting order coupon data to the simplified format', 'woocommerce' ),
'button' => __( 'Start converting', 'woocommerce' ),
'disabled' => true,
'desc' => __( 'This will convert <code>coupon_data</code> order item meta entries to simplified <code>coupon_info</code> entries. The conversion will happen overtime in the background (via Action Scheduler). There are currently no entries to convert.', 'woocommerce' ),
);
} elseif ( $batch_processor->is_enqueued( self::class ) ) {
$tools['stop_convert_order_coupon_data'] = array(
'name' => __( 'Stop converting order coupon data to the simplified format', 'woocommerce' ),
'button' => __( 'Stop converting', 'woocommerce' ),
'desc' =>
/* translators: %d=count of entries pending conversion */
sprintf( __( 'This will stop the background process that converts <code>coupon_data</code> order item meta entries to simplified <code>coupon_info</code> entries. There are currently %d entries that can be converted.', 'woocommerce' ), $pending_count ),
'callback' => array( $this, 'dequeue' ),
);
} else {
$tools['start_converting_order_coupon_data'] = array(
'name' => __( 'Convert order coupon data to the simplified format', 'woocommerce' ),
'button' => __( 'Start converting', 'woocommerce' ),
'desc' =>
/* translators: %d=count of entries pending conversion */
sprintf( __( 'This will convert <code>coupon_data</code> order item meta entries to simplified <code>coupon_info</code> entries. The conversion will happen overtime in the background (via Action Scheduler). There are currently %d entries that can be converted.', 'woocommerce' ), $pending_count ),
'callback' => array( $this, 'enqueue' ),
);
}
return $tools;
}
/**
* Start the background process for coupon data conversion.
*
* @return string Informative string to show after the tool is triggered in UI.
*/
private function enqueue(): string {
$batch_processor = wc_get_container()->get( BatchProcessingController::class );
if ( $batch_processor->is_enqueued( self::class ) ) {
return __( 'Background process for coupon meta conversion already started, nothing done.', 'woocommerce' );
}
$batch_processor->enqueue_processor( self::class );
return __( 'Background process for coupon meta conversion started', 'woocommerce' );
}
/**
* Stop the background process for coupon data conversion.
*
* @return string Informative string to show after the tool is triggered in UI.
*/
private function dequeue(): string {
$batch_processor = wc_get_container()->get( BatchProcessingController::class );
if ( ! $batch_processor->is_enqueued( self::class ) ) {
return __( 'Background process for coupon meta conversion not started, nothing done.', 'woocommerce' );
}
$batch_processor->remove_processor( self::class );
return __( 'Background process for coupon meta conversion stopped', 'woocommerce' );
}
}

View File

@@ -111,13 +111,8 @@ class OrderAttributionController implements RegisterHooksInterface {
}
);
// Include our hidden `<input>` elements on order notes and registration form.
$source_form_elements = function() {
$this->source_form_elements();
};
add_action( 'woocommerce_after_order_notes', $source_form_elements );
add_action( 'woocommerce_register_form', $source_form_elements );
add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'source_form_elements' ) );
add_action( 'woocommerce_register_form', array( $this, 'source_form_elements' ) );
// Update order based on submitted fields.
add_action(
@@ -181,16 +176,28 @@ class OrderAttributionController implements RegisterHooksInterface {
/**
* If the order is created in the admin, set the source type and origin to admin/Web admin.
* Only execute this if the order is created in the admin interface (or via ajax in the admin interface).
*
* @param WC_Order $order The recently created order object.
*
* @since 8.5.0
*/
private function maybe_set_admin_source( WC_Order $order ) {
if ( function_exists( 'is_admin' ) && is_admin() ) {
$order->add_meta_data( $this->get_meta_prefixed_field_name( 'source_type' ), 'admin' );
$order->save();
// For ajax requests, bail if the referer is not an admin page.
$http_referer = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ?? '' ) );
$referer_is_admin = 0 === strpos( $http_referer, get_admin_url() );
if ( ! $referer_is_admin && wp_doing_ajax() ) {
return;
}
// If not admin interface page, bail.
if ( ! is_admin() ) {
return;
}
$order->add_meta_data( $this->get_meta_prefixed_field_name( 'source_type' ), 'admin' );
$order->save();
}
/**
@@ -335,10 +342,11 @@ class OrderAttributionController implements RegisterHooksInterface {
}
/**
* Add `<input type="hidden">` elements for source fields.
* Used for checkout & customer register froms.
* Print `<input type="hidden">` elements for source fields.
* To be picked up and populated with data by the JS.
* Used for checkout & customer register forms.
*/
private function source_form_elements() {
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 ) ) );
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>diners</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="diners" sketch:type="MSLayerGroup">
<rect id="rectangle" fill="#0079BE" sketch:type="MSShapeGroup" x="0" y="0" width="750" height="471" rx="40"></rect>
<path d="M584.933911,237.947339 C584.933911,138.53154 501.952976,69.8140806 411.038924,69.8471464 L332.79674,69.8471464 C240.793699,69.8140806 165.066089,138.552041 165.066089,237.947339 C165.066089,328.877778 240.793699,403.587432 332.79674,403.150963 L411.038924,403.150963 C501.952976,403.586771 584.933911,328.857939 584.933911,237.947339 L584.933911,237.947339 Z" id="Shape-path" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M333.280302,83.9308394 C249.210378,83.9572921 181.085889,152.238282 181.066089,236.510581 C181.085889,320.768331 249.209719,389.042708 333.280302,389.069161 C417.370025,389.042708 485.508375,320.768331 485.520254,236.510581 C485.507715,152.238282 417.370025,83.9572921 333.280302,83.9308394 L333.280302,83.9308394 Z" id="Shape-path" fill="#0079BE" sketch:type="MSShapeGroup"></path>
<path d="M237.066089,236.09774 C237.145288,194.917524 262.812421,159.801587 299.006443,145.847134 L299.006443,326.327183 C262.812421,312.380667 237.144628,277.283907 237.066089,236.09774 Z M368.066089,326.372814 L368.066089,145.847134 C404.273312,159.767859 429.980043,194.903637 430.046043,236.103692 C429.980043,277.316312 404.273312,312.425636 368.066089,326.372814 Z" id="Path" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 780 501" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>discover</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="discover" sketch:type="MSLayerGroup">
<path d="M54.992188,0 C24.626565,0 -4.7369516e-15,24.629374 0,55.003906 L0,445.99609 C0,476.37636 24.618673,501 54.992188,501 L725.00781,501 C755.37344,501 780,476.37062 780,445.99609 L780,268.55664 L780,55.003906 C780,24.623637 755.38133,-4.7369516e-15 725.00781,0 L54.992188,0 L54.992188,0 Z" id="rectangle" fill="#4D4D4D" sketch:type="MSShapeGroup"></path>
<path d="M415.13086,161.21289 C446.07103,161.21289 471.15234,184.79287 471.15234,213.92188 L471.15234,213.95508 C471.15234,243.08408 446.07103,266.69727 415.13086,266.69727 C384.19069,266.69727 359.10938,243.08408 359.10938,213.95508 L359.10938,213.92188 C359.10938,184.79287 384.19069,161.21289 415.13086,161.21289 L415.13086,161.21289 Z M327.15234,161.89258 C335.9889,161.89258 343.40028,163.67723 352.41992,167.98242 L352.41992,190.73438 C343.87628,182.87089 336.46483,179.58008 326.66406,179.58008 C307.4002,179.58008 292.25,194.59455 292.25,213.63086 C292.25,233.70517 306.93133,247.82617 327.61914,247.82617 C336.93171,247.82617 344.20582,244.70584 352.41992,236.96875 L352.41992,259.73242 C343.07888,263.87291 335.50876,265.50781 326.66406,265.50781 C295.38621,265.50781 271.08203,242.91198 271.08203,213.77148 C271.08203,184.94507 296.03316,161.89258 327.15234,161.89258 L327.15234,161.89258 Z M230.03906,162.51953 C241.58477,162.51953 252.14952,166.24004 260.98242,173.51367 L250.23438,186.76172 C244.88362,181.11594 239.82337,178.73438 233.66992,178.73438 C224.81668,178.73437 218.36914,183.47936 218.36914,189.72266 C218.36914,195.07734 221.98883,197.91138 234.31445,202.20508 C257.67927,210.24859 264.60352,217.3809 264.60352,233.13086 C264.60352,252.32421 249.62806,265.68359 228.2832,265.68359 C212.65323,265.68359 201.29008,259.88895 191.82617,246.8125 L205.09375,234.78125 C209.82489,243.39164 217.71615,248.00391 227.51367,248.00391 C236.67693,248.00391 243.46094,242.05155 243.46094,234.01953 C243.46094,229.85606 241.40612,226.28585 237.30273,223.76172 C235.2368,222.56668 231.1447,220.78491 223.10352,218.11523 C203.81198,211.57701 197.19336,204.58834 197.19336,190.92969 C197.19336,174.70478 211.40702,162.51953 230.03906,162.51953 L230.03906,162.51953 Z M464.76172,164.24805 L487.19922,164.24805 L515.2832,230.83984 L543.72852,164.24805 L565.99609,164.24805 L520.50195,265.93359 L509.44922,265.93359 L464.76172,164.24805 L464.76172,164.24805 Z M67.414062,164.40039 L97.564453,164.40039 C130.87609,164.40039 154.09766,184.78179 154.09766,214.04102 C154.09766,228.63041 146.99364,242.73654 134.98047,252.09766 C124.87172,259.99945 113.35396,263.54297 97.40625,263.54297 L67.414062,263.54297 L67.414062,164.40039 L67.414062,164.40039 Z M163.54883,164.40039 L184.08984,164.40039 L184.08984,263.54297 L163.54883,263.54297 L163.54883,164.40039 L163.54883,164.40039 Z M575.2832,164.40039 L633.53516,164.40039 L633.53516,181.19922 L595.80859,181.19922 L595.80859,203.20508 L632.14453,203.20508 L632.14453,219.99609 L595.80859,219.99609 L595.80859,246.75781 L633.53516,246.75781 L633.53516,263.54297 L575.2832,263.54297 L575.2832,164.40039 L575.2832,164.40039 Z M647.14062,164.40039 L677.5957,164.40039 C701.28599,164.40039 714.86133,175.11052 714.86133,193.67188 C714.86133,208.85113 706.34712,218.81273 690.875,221.77734 L724.02344,263.54297 L698.76367,263.54297 L670.33398,223.71484 L667.65625,223.71484 L667.65625,263.54297 L647.14062,263.54297 L647.14062,164.40039 L647.14062,164.40039 Z M667.65625,180.01562 L667.65625,210.04102 L673.6582,210.04102 C686.77472,210.04102 693.72656,204.67918 693.72656,194.71289 C693.72656,185.06451 686.77347,180.01562 673.98242,180.01562 L667.65625,180.01562 L667.65625,180.01562 Z M87.939453,181.19922 L87.939453,246.75781 L93.451172,246.75781 C106.72432,246.75781 115.10685,244.36382 121.56055,238.87891 C128.66438,232.92288 132.9375,223.41276 132.9375,213.89844 C132.9375,204.39943 128.66438,195.17283 121.56055,189.2168 C114.77608,183.43696 106.72432,181.19922 93.451172,181.19922 L87.939453,181.19922 L87.939453,181.19922 Z" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M779.981917,288.361069 C753.932037,306.691919 558.904907,437.700579 221.228007,500.98412 L724.989727,500.98412 C755.355357,500.98412 779.981917,476.35474 779.981917,445.980209 L779.981917,288.361069 L779.981917,288.361069 Z" id="Shape-9" fill="#F47216" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.1 (12005) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0.031607858%" y1="49.9998574%" x2="99.9743153%" y2="49.9998574%" id="linearGradient-1">
<stop stop-color="#007B40" offset="0%"></stop>
<stop stop-color="#55B330" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0.471693172%" y1="49.999826%" x2="99.9860086%" y2="49.999826%" id="linearGradient-2">
<stop stop-color="#1D2970" offset="0%"></stop>
<stop stop-color="#006DBA" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0.113880772%" y1="50.0008964%" x2="99.9860003%" y2="50.0008964%" id="linearGradient-3">
<stop stop-color="#6E2B2F" offset="0%"></stop>
<stop stop-color="#E30138" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="jcb" sketch:type="MSLayerGroup">
<rect id="Rectangle-1" fill="#0E4C96" sketch:type="MSShapeGroup" x="0" y="0" width="750" height="471" rx="40"></rect>
<path d="M617.243183,346.766281 C617.243183,388.380887 583.514892,422.125974 541.88349,422.125974 L132.756823,422.125974 L132.756823,124.244916 C132.756823,82.6186826 166.489851,48.8744567 208.121683,48.8744567 L617.242752,48.874026 L617.242752,346.766281 L617.243183,346.766281 L617.243183,346.766281 Z" id="path3494" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M483.858874,242.044797 C495.542699,242.298285 507.296188,241.528806 518.936004,242.444883 C530.723244,244.645678 533.563915,262.487874 523.09234,268.332511 C515.950746,272.182115 507.459496,269.764696 499.713328,270.446208 L483.858874,270.446208 L483.858874,242.044797 L483.858874,242.044797 Z M525.691826,209.900487 C528.288491,219.064679 519.453903,227.292118 510.625917,226.030566 L483.858874,226.030566 C484.043758,217.388441 483.491345,208.008973 484.131053,199.821663 C494.854942,200.123386 505.679576,199.205849 516.340394,200.301853 C520.921799,201.451558 524.753935,205.217712 525.691826,209.900487 L525.691826,209.900487 Z M590.120412,73.9972254 C590.617872,91.498454 590.191471,109.92365 590.33359,127.780192 C590.299137,200.376358 590.405942,272.974174 590.278896,345.569303 C589.81042,372.776592 565.696524,396.413678 538.678749,396.956694 C511.63292,397.068451 484.584297,396.972628 457.537396,397.004497 L457.537396,287.253291 C487.007,287.099803 516.49604,287.561 545.953521,287.021594 C559.62072,286.162769 574.586027,277.145695 575.22328,262.107374 C576.833661,247.005483 562.592128,236.557185 549.071096,234.905684 C543.872773,234.770542 544.027132,233.390846 549.071096,232.788972 C561.96307,230.002483 572.090675,216.655787 568.296786,203.290229 C565.06052,189.232374 549.523839,183.79142 536.600366,183.817768 C510.248548,183.638612 483.891299,183.792359 457.537396,183.74111 C457.708585,163.252408 457.182916,142.740653 457.82271,122.267364 C459.910361,95.5513766 484.628603,73.5195319 511.269759,73.997656 C537.553166,73.9973692 563.837737,73.9982301 590.120412,73.9972254 L590.120412,73.9972254 Z" id="path3496" fill="url(#linearGradient-1)" sketch:type="MSShapeGroup"></path>
<path d="M159.740429,125.040498 C160.413689,97.8766592 184.628619,74.4290299 211.614797,74.0325398 C238.559493,73.9499686 265.506204,74.0209119 292.451671,73.9972254 C292.37764,164.882488 292.599905,255.773672 292.340301,346.655222 C291.302298,373.488802 267.350548,396.488661 240.661356,396.962292 C213.665015,397.060957 186.666275,396.976074 159.669012,397.004497 L159.669012,283.550875 C185.891623,289.745491 213.391138,292.382518 240.142406,288.272242 C256.134509,285.697368 273.629935,277.848026 279.044261,261.257567 C283.030122,247.066267 280.785723,232.131602 281.378027,217.566465 L281.378027,183.741541 L235.081246,183.741541 C234.873106,206.112145 235.507258,228.522447 234.746146,250.867107 C233.49785,264.601214 219.900147,273.326996 206.946428,272.861801 C190.879747,273.030535 159.04755,261.221796 159.04755,261.221796 C158.967492,219.3048 159.514314,166.814385 159.740429,125.040498 L159.740429,125.040498 Z" id="path3498" fill="url(#linearGradient-2)" sketch:type="MSShapeGroup"></path>
<path d="M309.719995,197.390136 C307.285788,197.90738 309.229141,189.089459 308.606298,185.743964 C308.772233,164.593637 308.260045,143.420951 308.889718,122.285827 C310.972541,95.4570827 335.881262,73.3701105 362.628748,73.997656 L441.39456,73.997656 C441.320658,164.882346 441.542493,255.77294 441.283406,346.653934 C440.244412,373.488027 416.291344,396.487102 389.602087,396.962292 C362.604605,397.061991 335.604707,396.976504 308.606298,397.004928 L308.606298,272.707624 C327.04641,287.835846 352.105738,290.192248 375.077953,290.233484 C392.39501,290.227455 409.611861,287.557865 426.428143,283.562934 L426.428143,260.790297 C407.474658,270.236609 385.194808,276.235815 364.184745,270.807966 C349.529051,267.157367 338.89089,252.996683 339.128513,237.872204 C337.43001,222.143684 346.652631,205.536885 362.110237,200.860855 C381.300923,194.852545 402.217787,199.448454 420.206344,207.258795 C424.060526,209.27695 427.97066,211.780342 426.428143,205.338044 L426.428143,187.438358 C396.343581,180.280951 364.326644,177.646405 334.099438,185.433619 C325.351193,187.901774 316.82819,191.644647 309.719995,197.390136 L309.719995,197.390136 Z" id="path3500" fill="url(#linearGradient-3)" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="mastercard">
<rect id="Rectangle-1" fill="#F4F4F4" x="0" y="0" width="750" height="471" rx="40"></rect>
<g id="mark" transform="translate(125.719997, 41.850862)">
<g id="text" transform="translate(25.142679, 328.360022)" fill="#000000">
<path d="M467.715561,51.9326899 C466.502604,51.9623585 465.503405,52.3648948 464.717962,53.1403001 C463.932516,53.9157321 463.526027,54.8861098 463.498494,56.0514362 C463.526027,57.2079497 463.932516,58.1758036 464.717963,58.9550005 C465.503406,59.7342125 466.502604,60.1392726 467.715561,60.1701825 C468.900764,60.1392726 469.887352,59.7342123 470.675326,58.9550002 C471.463285,58.175803 471.872297,57.2079493 471.902362,56.0514362 C471.872927,54.8861098 471.465177,53.915732 470.679109,53.1402998 C469.893026,52.3648943 468.905178,51.9623581 467.715561,51.9326899 L467.715561,51.9326899 L467.715561,51.9326899 Z M467.715561,59.2616355 C466.791392,59.2389292 466.029277,58.9259854 465.429214,58.3228033 C464.829145,57.7196374 464.518499,56.9625159 464.497273,56.0514362 C464.518499,55.1363804 464.829146,54.379679 465.429214,53.78133 C466.029277,53.1830062 466.791392,52.8730071 467.715561,52.8513318 C468.620383,52.8730076 469.370728,53.1830066 469.966597,53.7813302 C470.562452,54.379679 470.871417,55.1363804 470.893494,56.0514362 C470.871417,56.9625161 470.562452,57.7196378 469.966597,58.3228033 C469.370728,58.925985 468.620384,59.2389287 467.715561,59.2616355 L467.715561,59.2616355 L467.715561,59.2616355 Z M467.957689,54.1232975 L466.19217,54.1232975 L466.19217,57.9492899 L467.009353,57.9492899 L467.009353,56.5158046 L467.382634,56.5158046 L468.542832,57.9492899 L469.521434,57.9492899 L468.270438,56.5057097 C468.661158,56.4567169 468.961716,56.330109 469.172113,56.1258861 C469.382498,55.9216836 469.488849,55.6613181 469.491168,55.3447885 C469.488429,54.9670796 469.355174,54.6701195 469.091404,54.4539073 C468.827621,54.237719 468.449717,54.1275158 467.957689,54.1232975 L467.957689,54.1232975 L467.957689,54.1232975 Z M467.9476,54.8400402 C468.166813,54.840262 468.338741,54.8827453 468.463383,54.9674885 C468.588015,55.0522552 468.651489,55.1780218 468.653808,55.3447882 C468.651483,55.5164129 468.588015,55.6451235 468.463383,55.73092 C468.338741,55.8167385 468.166813,55.8596412 467.9476,55.859631 L467.009353,55.859631 L467.009353,54.8400393 L467.9476,54.8400402 L467.9476,54.8400402 Z" id="path3078"></path>
<path d="M9.34331724,57.5428029 L0.588175916,57.5428029 L0.588175916,16.6600045 L9.17164757,16.6600045 L9.17164757,21.6415186 C9.17164757,21.6415186 16.7107355,15.5532485 21.1885083,15.6293508 C29.8949298,15.7773317 35.093729,23.1875098 35.093729,23.1875098 C35.093729,23.1875098 39.3109893,15.6293508 48.8272918,15.6293508 C62.8997988,15.6293508 64.9642149,28.5125858 64.9642149,28.5125858 L64.9642149,57.3710273 L56.5524108,57.3710273 L56.5524108,31.9481087 C56.5524108,31.9481087 56.5825922,24.2181741 47.4539323,24.2181741 C38.0139747,24.2181741 37.1537629,31.9481087 37.1537629,31.9481087 L37.1537629,57.3710273 L28.3986215,57.3710273 L28.3986215,31.7763331 C28.3986215,31.7763331 27.5575496,23.7028366 19.6434834,23.7028366 C9.3650113,23.7028366 9.17164757,31.9481087 9.17164757,31.9481087 L9.34331724,57.5428029 L9.34331724,57.5428029 Z" id="path3006"></path>
<path d="M275.596898,15.623773 C271.119122,15.5476814 263.580741,21.6355967 263.580741,21.6355967 L263.580741,16.6649268 L254.988182,16.6649268 L254.988182,57.5386115 L263.748565,57.5386115 L263.580741,31.9463783 C263.580741,31.9463783 263.77445,23.7179044 274.052923,23.7179044 C275.961824,23.7179044 277.444363,24.180569 278.61772,24.8934007 L278.61772,24.8598688 L281.470718,16.9000798 C279.749092,16.1750176 277.791223,15.6610664 275.596898,15.623773 L275.596898,15.623773 L275.596898,15.623773 Z" id="path3008"></path>
<path d="M398.92774,15.623773 C394.449964,15.5476814 386.911582,21.6355967 386.911582,21.6355967 L386.911582,16.6649268 L378.319023,16.6649268 L378.319023,57.5386115 L387.079406,57.5386115 L386.911582,31.9463783 C386.911582,31.9463783 387.105291,23.7179044 397.383764,23.7179044 C399.292666,23.7179044 400.775204,24.180569 401.948561,24.8934007 L401.948561,24.8598688 L404.801559,16.9000798 C403.079933,16.1750713 401.122064,15.6611201 398.92774,15.6238267 L398.92774,15.623773 L398.92774,15.623773 Z" id="path3013"></path>
<path d="M93.2735295,15.4558449 C80.1708646,15.4558449 73.2368626,27.2396859 73.2018479,37.0849763 C73.1658666,47.1762746 81.0955959,58.8148646 93.6427411,58.8148646 C100.962678,58.8148646 106.976041,53.4075817 106.976041,53.4075817 L106.960145,57.5721971 L115.577998,57.5721971 L115.577998,16.6488272 L106.929792,16.6488272 L106.929792,21.8035248 C106.929792,21.8035248 101.282654,15.4558449 93.2735725,15.4558449 L93.2735295,15.4558449 L93.2735295,15.4558449 Z M94.9517638,23.7850756 C101.991433,23.7850756 107.706344,29.9122942 107.706344,37.454418 C107.706344,44.9965418 101.991433,51.0901748 94.9517638,51.0901748 C87.9120947,51.0901748 82.2307482,44.9965418 82.2307482,37.454418 C82.2307482,29.9122942 87.9120947,23.7850756 94.9517638,23.7850756 L94.9517638,23.7850756 L94.9517638,23.7850756 Z" id="path3015"></path>
<path d="M344.597578,15.4558449 C331.494913,15.4558449 324.560911,27.2396859 324.525896,37.0849763 C324.489915,47.1762746 332.419644,58.8148646 344.966789,58.8148646 C352.286726,58.8148646 358.300089,53.4075817 358.300089,53.4075817 L358.284193,57.5721971 L366.902046,57.5721971 L366.902046,16.6488272 L358.25384,16.6488272 L358.25384,21.8035248 C358.25384,21.8035248 352.606702,15.4558449 344.59762,15.4558449 L344.597578,15.4558449 L344.597578,15.4558449 Z M346.275812,23.7850756 C353.315481,23.7850756 359.030392,29.9122942 359.030392,37.454418 C359.030392,44.9965418 353.315481,51.0901748 346.275812,51.0901748 C339.236143,51.0901748 333.554796,44.9965418 333.554796,37.454418 C333.554796,29.9122942 339.236143,23.7850756 346.275812,23.7850756 L346.275812,23.7850756 L346.275812,23.7850756 Z" id="path3020"></path>
<path d="M427.342249,15.4558449 C414.239584,15.4558449 407.305582,27.2396859 407.270567,37.0849763 C407.234586,47.1762746 415.164315,58.8148646 427.71146,58.8148646 C435.031397,58.8148646 441.04476,53.4075817 441.04476,53.4075817 L441.028864,57.5721971 L449.646718,57.5721971 L449.646718,0.49407462 L440.998511,0.49407462 L440.998511,21.8035248 C440.998511,21.8035248 435.351373,15.4558449 427.342292,15.4558449 L427.342249,15.4558449 L427.342249,15.4558449 Z M429.020483,23.7850756 C436.060152,23.7850756 441.775063,29.9122942 441.775063,37.454418 C441.775063,44.9965418 436.060152,51.0901748 429.020483,51.0901748 C421.980814,51.0901748 416.299467,44.9965418 416.299467,37.454418 C416.299467,29.9122942 421.980814,23.7850756 429.020483,23.7850756 L429.020483,23.7850756 L429.020483,23.7850756 Z" id="path3022"></path>
<path d="M141.872122,58.9170078 C132.94558,58.9170078 124.705176,53.4201669 124.705176,53.4201669 L128.481907,47.5797641 C128.481907,47.5797641 136.278978,51.1870733 141.872122,51.1870733 C145.50613,51.1870733 151.583937,50.0128667 151.657274,46.3773348 C151.734822,42.5349478 141.442945,41.39581 141.442945,41.39581 C141.442945,41.39581 126.078536,41.1860853 126.078536,28.5125858 C126.078536,20.5421246 133.751938,15.4575752 143.588818,15.4575752 C149.272667,15.4575752 159.89741,20.4390893 159.89741,20.4390893 L155.605674,27.1383702 C155.605674,27.1383702 147.402218,23.858921 143.073802,23.7028366 C139.418806,23.5710413 135.005346,25.3221465 135.005346,28.5125858 C135.005346,37.1806926 160.584084,27.837198 160.584084,45.3466704 C160.584084,56.8338188 150.166691,58.9170078 141.872122,58.9170078 L141.872122,58.9170078 L141.872122,58.9170078 Z" id="path3024"></path>
<path d="M174.802149,4.80920724 L174.802149,16.6985124 L167.182966,16.6985124 L167.182966,25.296428 L174.802149,25.296428 L174.802149,45.85082 C174.802149,45.85082 174.127827,59.7552616 189.067141,59.7552616 C193.19753,59.7552616 201.284686,56.6989713 201.284686,56.6989713 L197.827523,47.7651996 C197.827523,47.7651996 194.611468,50.5102454 190.980328,50.4184626 C184.076195,50.2440217 184.267391,45.8172343 184.267391,45.8172343 L184.267391,25.296428 L198.498817,25.296428 L198.498817,16.6985124 L184.267391,16.6985124 L184.267391,4.80920724 L174.802149,4.80920724 L174.802149,4.80920724 L174.802149,4.80920724 Z" id="path3026"></path>
<path d="M226.659588,15.959629 C212.610087,15.959629 205.590417,27.5389793 205.648095,37.5887604 C205.707384,47.9238419 212.040304,59.5537479 227.498705,59.5537479 C234.115072,59.5537479 243.408366,53.7434378 243.408366,53.7434378 L239.414168,46.791217 C239.414168,46.791217 233.072548,51.2916884 227.498705,51.2916884 C216.339172,51.2916884 215.616806,40.3763659 215.616806,40.3763659 L245.489376,40.3763659 C245.489376,40.3763659 247.717985,15.959629 226.659588,15.959629 L226.659588,15.959629 L226.659588,15.959629 Z M225.38413,23.9865893 C225.715416,23.9677813 226.070568,23.9865893 226.424635,23.9865893 C236.937954,23.9865893 236.863252,33.9279292 236.863252,33.9279292 L215.616806,33.9279292 C215.616806,33.9279292 215.114206,24.5707962 225.38413,23.9865893 L225.38413,23.9865893 L225.38413,23.9865893 Z" id="path3034"></path>
<path d="M315.5162,46.686 L319.52203,54.7026427 C319.52203,54.7026427 313.17302,58.8324258 306.047898,58.8324258 C291.296557,58.8324258 283.105442,47.7234997 283.105442,37.2117848 C283.105442,20.6912339 296.140698,15.8340672 304.955397,15.8340672 C312.956369,15.8340672 319.886193,20.4497077 319.886193,20.4497077 L315.394819,28.4663505 C315.394819,28.4663505 312.671859,24.2151004 304.712614,24.2151004 C296.766655,24.2151004 292.573755,31.0702185 292.573755,37.5761752 C292.573755,44.8669927 297.454114,51.0587064 304.834005,51.0587064 C310.623393,51.0587064 315.5162,46.686 315.5162,46.686 L315.5162,46.686 L315.5162,46.686 Z" id="path3037"></path>
</g>
<path d="M498.787985,236.781279 L498.787985,231.260623 L497.347484,231.260623 L495.690436,235.057252 L494.033388,231.260623 L492.592886,231.260623 L492.592886,236.781279 L493.609711,236.781279 L493.609711,232.617235 L495.163193,236.206603 L496.217678,236.206603 L497.771161,232.607814 L497.771161,236.781279 L498.787985,236.781279 L498.787985,236.781279 Z M489.664807,236.781279 L489.664807,232.202715 L491.510156,232.202715 L491.510156,231.270044 L486.812049,231.270044 L486.812049,232.202715 L488.657397,232.202715 L488.657397,236.781279 L489.664807,236.781279 L489.664807,236.781279 Z" id="path3057" fill="#F79F1A"></path>
<path d="M499.076678,154.709802 C499.076678,240.135159 429.999707,309.386105 344.788929,309.386105 C259.578151,309.386105 190.501159,240.135159 190.501159,154.709802 C190.501159,69.2844326 259.578151,0.0334920174 344.788929,0.0334920174 C429.999707,0.0334920174 499.076678,69.2844326 499.076678,154.709802 L499.076678,154.709802 L499.076678,154.709802 Z" id="path2997" fill="#F79F1A"></path>
<path d="M308.73932,154.709802 C308.73932,240.135159 239.662349,309.386105 154.451571,309.386105 C69.2407931,309.386105 0.163801275,240.135159 0.163801275,154.709802 C0.163801275,69.2844326 69.2407931,0.0334920174 154.451571,0.0334920174 C239.662349,0.0334920174 308.73932,69.2844326 308.73932,154.709802 L308.73932,154.709802 L308.73932,154.709802 Z" id="path2995" fill="#EA001B"></path>
<path d="M249.620562,32.9474812 C213.621326,61.2636823 190.513152,105.265345 190.513152,154.695309 C190.513152,204.125274 213.621326,248.16052 249.620562,276.476723 C285.619799,248.16052 308.727973,204.125274 308.727973,154.695309 C308.727973,105.265345 285.619799,61.2636823 249.620562,32.9474812 L249.620562,32.9474812 L249.620562,32.9474812 Z" id="path2999" fill="#FF5F01"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 471" preserveAspectRatio="xMidYMid meet"><defs><style>.cls-1{fill:#75787c;}</style></defs><title>credit-card</title><g id="Page-1"><g id="amex"><g id="Rectangle-1"><path class="cls-1" d="M711,40V431H41V40H711m0-40H41A40,40,0,0,0,1,40V431a40,40,0,0,0,40,40H711a40,40,0,0,0,40-40V40A40,40,0,0,0,711,0Z" transform="translate(-1)"/></g></g></g><rect class="cls-1" x="11" y="113" width="728" height="100.73"/><rect class="cls-1" x="45" y="354.08" width="93" height="32.92"/><rect class="cls-1" x="172" y="354.08" width="155.94" height="32.92"/></svg>

After

Width:  |  Height:  |  Size: 635 B

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 750 471" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" preserveAspectRatio="xMidYMid meet">
<!-- Generator: Sketch 3.3.1 (12005) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="visa" sketch:type="MSLayerGroup">
<rect id="Rectangle-1" fill="#0E4595" sketch:type="MSShapeGroup" x="0" y="0" width="750" height="471" rx="40"></rect>
<path d="M278.1975,334.2275 L311.5585,138.4655 L364.9175,138.4655 L331.5335,334.2275 L278.1975,334.2275 L278.1975,334.2275 Z" id="Shape" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M524.3075,142.6875 C513.7355,138.7215 497.1715,134.4655 476.4845,134.4655 C423.7605,134.4655 386.6205,161.0165 386.3045,199.0695 C386.0075,227.1985 412.8185,242.8905 433.0585,252.2545 C453.8275,261.8495 460.8105,267.9695 460.7115,276.5375 C460.5795,289.6595 444.1255,295.6545 428.7885,295.6545 C407.4315,295.6545 396.0855,292.6875 378.5625,285.3785 L371.6865,282.2665 L364.1975,326.0905 C376.6605,331.5545 399.7065,336.2895 423.6355,336.5345 C479.7245,336.5345 516.1365,310.2875 516.5505,269.6525 C516.7515,247.3835 502.5355,230.4355 471.7515,216.4645 C453.1005,207.4085 441.6785,201.3655 441.7995,192.1955 C441.7995,184.0585 451.4675,175.3575 472.3565,175.3575 C489.8055,175.0865 502.4445,178.8915 512.2925,182.8575 L517.0745,185.1165 L524.3075,142.6875" id="path13" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M661.6145,138.4655 L620.3835,138.4655 C607.6105,138.4655 598.0525,141.9515 592.4425,154.6995 L513.1975,334.1025 L569.2285,334.1025 C569.2285,334.1025 578.3905,309.9805 580.4625,304.6845 C586.5855,304.6845 641.0165,304.7685 648.7985,304.7685 C650.3945,311.6215 655.2905,334.1025 655.2905,334.1025 L704.8025,334.1025 L661.6145,138.4655 L661.6145,138.4655 Z M596.1975,264.8725 C600.6105,253.5935 617.4565,210.1495 617.4565,210.1495 C617.1415,210.6705 621.8365,198.8155 624.5315,191.4655 L628.1385,208.3435 C628.1385,208.3435 638.3555,255.0725 640.4905,264.8715 L596.1975,264.8715 L596.1975,264.8725 L596.1975,264.8725 Z" id="Path" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M232.9025,138.4655 L180.6625,271.9605 L175.0965,244.8315 C165.3715,213.5575 135.0715,179.6755 101.1975,162.7125 L148.9645,333.9155 L205.4195,333.8505 L289.4235,138.4655 L232.9025,138.4655" id="path16" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M131.9195,138.4655 L45.8785,138.4655 L45.1975,142.5385 C112.1365,158.7425 156.4295,197.9015 174.8155,244.9525 L156.1065,154.9925 C152.8765,142.5965 143.5085,138.8975 131.9195,138.4655" id="path18" fill="#F2AE14" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,350 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
use \Exception;
use \WC_Order;
/**
* This class generates printable order receipts as transient files (see src/Internal/TransientFiles).
* The template for the receipt is Templates/order-receipt.php, it uses the variables returned as array keys
* 'get_order_data'.
*
* When a receipt is generated for an order with 'generate_receipt' the receipt file name is stored as order meta
* (see RECEIPT_FILE_NAME_META_KEY) for later retrieval with 'get_existing_receipt'. Beware! The files pointed
* by such meta keys could have expired and thus no longer exist. 'get_existing_receipt' will appropriately return null
* if the meta entry exists but the file doesn't.
*/
class ReceiptRenderingEngine {
private const FONT_SIZE = 12;
private const LINE_HEIGHT = self::FONT_SIZE * 1.5;
private const ICON_HEIGHT = self::LINE_HEIGHT;
private const ICON_WIDTH = self::ICON_HEIGHT * ( 4 / 3 );
private const MARGIN = 16;
private const TITLE_FONT_SIZE = 24;
private const FOOTER_FONT_SIZE = 10;
/**
* This array must contain all the names of the files in the CardIcons directory (without extension),
* except 'unknown'.
*/
private const KNOWN_CARD_TYPES = array( 'amex', 'diners', 'discover', 'interac', 'jcb', 'mastercard', 'visa' );
/**
* Order meta key that stores the file name of the last generated receipt.
*/
public const RECEIPT_FILE_NAME_META_KEY = '_receipt_file_name';
/**
* The instance of TransientFilesEngine to use.
*
* @var TransientFilesEngine
*/
private $transient_files_engine;
/**
* The instance of LegacyProxy to use.
*
* @var LegacyProxy
*/
private $legacy_proxy;
/**
* Initializes the class.
*
* @param TransientFilesEngine $transient_files_engine The instance of TransientFilesEngine to use.
* @param LegacyProxy $legacy_proxy The instance of LegacyProxy to use.
* @internal
*/
final public function init( TransientFilesEngine $transient_files_engine, LegacyProxy $legacy_proxy ) {
$this->transient_files_engine = $transient_files_engine;
$this->legacy_proxy = $legacy_proxy;
}
/**
* Get the (transient) file name of the receipt for an order, creating a new file if necessary.
*
* If $force_new is false, and a receipt file for the order already exists (as pointed by order meta key
* RECEIPT_FILE_NAME_META_KEY), then the name of the already existing receipt file is returned.
*
* If $force_new is true, OR if it's false but no receipt file for the order exists (no order meta with key
* RECEIPT_FILE_NAME_META_KEY exists, OR it exists but the file it points to doesn't), then a new receipt
* transient file is created with the supplied expiration date (defaulting to "tomorrow"), and the new file name
* is stored as order meta with the key RECEIPT_FILE_NAME_META_KEY.
*
* @param int|WC_Order $order The order object or order id to get the receipt for.
* @param string|int|null $expiration_date GMT expiration date formatted as yyyy-mm-dd, or as a timestamp, or null for "tomorrow".
* @param bool $force_new If true, creates a new receipt file even if one already exists for the order.
* @return string|null The file name of the new or already existing receipt file, null if an order id is passed and the order doesn't exist.
* @throws \InvalidArgumentException Invalid expiration date (wrongly formatted, or it's a date in the past).
* @throws Exception The directory to store the file doesn't exist and can't be created.
*/
public function generate_receipt( $order, $expiration_date = null, bool $force_new = false ) : ?string {
if ( ! $order instanceof WC_Order ) {
$order = wc_get_order( $order );
if ( false === $order ) {
return null;
}
}
if ( ! $force_new ) {
$existing_receipt_filename = $this->get_existing_receipt( $order );
if ( ! is_null( $existing_receipt_filename ) ) {
return $existing_receipt_filename;
}
}
$expiration_date ??=
$this->legacy_proxy->call_function(
'gmdate',
'Y-m-d',
$this->legacy_proxy->call_function(
'strtotime',
'+1 days'
)
);
// phpcs:ignore WordPress.PHP.DontExtract.extract_extract
extract( $this->get_order_data( $order ) );
ob_start();
include __dir__ . '/Templates/order-receipt.php';
$rendered_template = ob_get_contents();
ob_end_clean();
$file_name = $this->transient_files_engine->create_transient_file( $rendered_template, $expiration_date );
$order->update_meta_data( self::RECEIPT_FILE_NAME_META_KEY, $file_name );
$order->save_meta_data();
return $file_name;
}
/**
* Get the file name of an existing receipt file for an order.
*
* A receipt is considered to be available for the order if there's an order meta entry with key
* RECEIPT_FILE_NAME_META_KEY AND the transient file it points to exists AND it has not expired.
*
* @param WC_Order $order The order object or order id to get the receipt for.
* @return string|null The receipt file name, or null if no receipt is currently available for the order.
* @throws Exception Thrown if a wrong file path is passed.
*/
public function get_existing_receipt( $order ): ?string {
if ( ! $order instanceof WC_Order ) {
$order = wc_get_order( $order );
if ( false === $order ) {
return null;
}
}
$existing_receipt_filename = $order->get_meta( self::RECEIPT_FILE_NAME_META_KEY, true );
if ( '' === $existing_receipt_filename ) {
return null;
}
$file_path = $this->transient_files_engine->get_transient_file_path( $existing_receipt_filename );
if ( is_null( $file_path ) ) {
return null;
}
return $this->transient_files_engine->file_has_expired( $file_path ) ? null : $existing_receipt_filename;
}
/**
* Get the order data that the receipt template will use.
*
* @param WC_Order $order The order to get the data from.
* @return array The order data as an associative array.
*/
private function get_order_data( WC_Order $order ): array {
$store_name = get_bloginfo( 'name' );
if ( $store_name ) {
/* translators: %s = store name */
$receipt_title = sprintf( __( 'Receipt from %s', 'woocommerce' ), $store_name );
} else {
$receipt_title = __( 'Receipt', 'woocommerce' );
}
$order_id = $order->get_id();
if ( $order_id ) {
/* translators: %d = order id */
$summary_title = sprintf( __( 'Summary: Order #%d', 'woocommerce' ), $order->get_id() );
} else {
$summary_title = __( 'Summary', 'woocommerce' );
}
$get_price_args = array( 'currency' => $order->get_currency() );
$line_items_info = array();
$line_items = $order->get_items( 'line_item' );
foreach ( $line_items as $line_item ) {
$line_item_product = $line_item->get_product();
$line_item_title =
( $line_item_product instanceof \WC_Product_Variation ) ?
( wc_get_product( $line_item_product->get_parent_id() )->get_name() ) . '. ' . $line_item_product->get_attribute_summary() :
$line_item_product->get_name();
$line_items_info[] = array(
'title' => wp_kses( $line_item_title, array() ),
'quantity' => $line_item->get_quantity(),
'amount' => wc_price( $line_item->get_subtotal(), $get_price_args ),
);
}
$line_items_info[] = array(
'title' => __( 'Subtotal', 'woocommerce' ),
'amount' => wc_price( $order->get_subtotal(), $get_price_args ),
);
$coupon_names = ArrayUtil::select( $order->get_coupons(), 'get_name', ArrayUtil::SELECT_BY_OBJECT_METHOD );
if ( ! empty( $coupon_names ) ) {
$line_items_info[] = array(
/* translators: %s = comma-separated list of coupon codes */
'title' => sprintf( __( 'Discount (%s)', 'woocommerce' ), join( ', ', $coupon_names ) ),
'amount' => wc_price( -$order->get_total_discount(), $get_price_args ),
);
}
foreach ( $order->get_fees() as $fee ) {
$name = $fee->get_name();
$line_items_info[] = array(
'title' => '' === $name ? __( 'Fee', 'woocommerce' ) : $name,
'amount' => wc_price( $fee->get_total(), $get_price_args ),
);
}
$shipping_total = (float) $order->get_shipping_total();
if ( $shipping_total ) {
$line_items_info[] = array(
'title' => __( 'Shipping', 'woocommerce' ),
'amount' => wc_price( $order->get_shipping_total(), $get_price_args ),
);
}
$total_taxes = 0;
foreach ( $order->get_taxes() as $tax ) {
$total_taxes += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total();
}
if ( $total_taxes ) {
$line_items_info[] = array(
'title' => __( 'Taxes', 'woocommerce' ),
'amount' => wc_price( $total_taxes, $get_price_args ),
);
}
$line_items_info[] = array(
'title' => __( 'Amount Paid', 'woocommerce' ),
'amount' => wc_price( $order->get_total(), $get_price_args ),
);
return array(
'constants' => array(
'font_size' => self::FONT_SIZE,
'margin' => self::MARGIN,
'title_font_size' => self::TITLE_FONT_SIZE,
'footer_font_size' => self::FOOTER_FONT_SIZE,
'line_height' => self::LINE_HEIGHT,
'icon_height' => self::ICON_HEIGHT,
'icon_width' => self::ICON_WIDTH,
),
'texts' => array(
'receipt_title' => $receipt_title,
'amount_paid_section_title' => __( 'Amount Paid', 'woocommerce' ),
'date_paid_section_title' => __( 'Date Paid', 'woocommerce' ),
'payment_method_section_title' => __( 'Payment method', 'woocommerce' ),
'summary_section_title' => $summary_title,
'order_notes_section_title' => __( 'Notes', 'woocommerce' ),
'app_name' => __( 'Application Name', 'woocommerce' ),
'aid' => __( 'AID', 'woocommerce' ),
'account_type' => __( 'Account Type', 'woocommerce' ),
),
'formatted_amount' => wc_price( $order->get_total(), $get_price_args ),
'formatted_date' => wc_format_datetime( $order->get_date_paid() ),
'line_items' => $line_items_info,
'payment_method' => $order->get_payment_method_title(),
'notes' => array_map( 'get_comment_text', $order->get_customer_order_notes() ),
'payment_info' => $this->get_woo_pay_data( $order ),
);
}
/**
* Get the order data related to WooCommerce Payments.
*
* It will return null if any of these is true:
*
* - Payment method is not 'woocommerce_payments".
* - WooCommerce Payments is not installed.
* - No intent id is stored for the order.
* - Retrieving the payment information from Stripe API (providing the intent id) fails.
* - The received data set doesn't contain the expected information.
*
* @param WC_Order $order The order to get the data from.
* @return array|null An array of payment information for the order, or null if not available.
*/
private function get_woo_pay_data( WC_Order $order ): ?array {
// For testing purposes: if WooCommerce Payments development mode is enabled,
// an order meta item with key '_wcpay_payment_details' will be used if it exists as a replacement
// for the call to the Stripe API's 'get intent' endpoint.
// The value must be the JSON encoding of an array simulating the "payment_details" part of the response from the endpoint
// (at the very least it must contain the "card_present" key).
$payment_details = json_decode( defined( 'WCPAY_DEV_MODE' ) && WCPAY_DEV_MODE ? $order->get_meta( '_wcpay_payment_details' ) : false, true );
if ( ! $payment_details ) {
if ( 'woocommerce_payments' !== $order->get_payment_method() ) {
return null;
}
if ( ! class_exists( \WC_Payments::class ) ) {
return null;
}
$intent_id = $order->get_meta( '_intent_id' );
if ( ! $intent_id ) {
return null;
}
try {
$payment_details = \WC_Payments::get_payments_api_client()->get_intent( $intent_id )->get_charge()->get_payment_method_details();
} catch ( Exception $ex ) {
$order_id = $order->get_id();
$message = $ex->getMessage();
wc_get_logger()->error( StringUtil::class_name_without_namespace( static::class ) . " - retrieving info for charge {$intent_id} for order {$order_id}: {$message}" );
return null;
}
}
$card_data = $payment_details['card_present'] ?? null;
if ( is_null( $card_data ) ) {
return null;
}
$card_brand = $card_data['brand'] ?? '';
if ( ! in_array( $card_brand, self::KNOWN_CARD_TYPES, true ) ) {
$card_brand = 'unknown';
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$card_svg = base64_encode( file_get_contents( __DIR__ . "/CardIcons/{$card_brand}.svg" ) );
return array(
'card_icon' => $card_svg,
'card_last4' => wp_kses( $card_data['last4'] ?? '', array() ),
'app_name' => wp_kses( $card_data['receipt']['application_preferred_name'] ?? null, array() ),
'aid' => wp_kses( $card_data['receipt']['dedicated_file_name'] ?? null, array() ),
'account_type' => wp_kses( $card_data['receipt']['account_type'] ?? null, array() ),
);
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use \WP_REST_Server;
use \WP_REST_Request;
use \WP_Error;
use \InvalidArgumentException;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Controller for the REST endpoints associated to the receipt rendering engine.
* The endpoints require the read_shop_order capability for the order at hand.
*/
class ReceiptRenderingRestController extends RestApiControllerBase {
use AccessiblePrivateMethods;
/**
* Get the WooCommerce REST API namespace for the class.
*
* @return string
*/
protected function get_rest_api_namespace(): string {
return 'order-receipts';
}
/**
* Register the REST API endpoints handled by this controller.
*/
public function register_routes() {
register_rest_route(
$this->route_namespace,
'/orders/(?P<id>[\d]+)/receipt',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => fn( $request ) => $this->run( $request, 'create_order_receipt' ),
'permission_callback' => fn( $request ) => $this->check_permission( $request, 'read_shop_order', $request->get_param( 'id' ) ),
'args' => $this->get_args_for_create_order_receipt(),
'schema' => $this->get_schema_for_get_and_post_order_receipt(),
),
)
);
register_rest_route(
$this->route_namespace,
'/orders/(?P<id>[\d]+)/receipt',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => fn( $request ) => $this->run( $request, 'get_order_receipt' ),
'permission_callback' => fn( $request ) => $this->check_permission( $request, 'read_shop_order', $request->get_param( 'id' ) ),
'args' => $this->get_args_for_get_order_receipt(),
'schema' => $this->get_schema_for_get_and_post_order_receipt(),
),
)
);
}
/**
* Handle the GET /orders/id/receipt:
*
* Return the data for a receipt if it exists, or a 404 error if it doesn't.
*
* @param WP_REST_Request $request The received request.
* @return array|WP_Error
*/
public function get_order_receipt( WP_REST_Request $request ) {
$order_id = $request->get_param( 'id' );
$filename = wc_get_container()->get( ReceiptRenderingEngine::class )->get_existing_receipt( $order_id );
return is_null( $filename ) ?
new WP_Error( 'woocommerce_rest_not_found', __( 'Receipt not found', 'woocommerce' ), array( 'status' => 404 ) ) :
$this->get_response_for_file( $filename );
}
/**
* Handle the POST /orders/id/receipt:
*
* Return the data for a receipt if it exists, or create a new receipt and return its data otherwise.
*
* Optional query string arguments:
*
* expiration_date: formatted as yyyy-mm-dd.
* expiration_days: a number, 0 is today, 1 is tomorrow, etc.
* force_new: defaults to false, if true, create a new receipt even if one already exists for the order.
*
* If neither expiration_date nor expiration_days are supplied, the default is expiration_days = 1.
*
* @param WP_REST_Request $request The received request.
* @return array|WP_Error Request response or an error.
*/
public function create_order_receipt( WP_REST_Request $request ) {
$expiration_date =
$request->get_param( 'expiration_date' ) ??
gmdate( 'Y-m-d', strtotime( "+{$request->get_param('expiration_days')} days" ) );
$order_id = $request->get_param( 'id' );
$filename = wc_get_container()->get( ReceiptRenderingEngine::class )->generate_receipt( $order_id, $expiration_date, $request->get_param( 'force_new' ) );
return is_null( $filename ) ?
new WP_Error( 'woocommerce_rest_not_found', __( 'Order not found', 'woocommerce' ), array( 'status' => 404 ) ) :
$this->get_response_for_file( $filename );
}
/**
* Formats the response for both the GET and POST endpoints.
*
* @param string $filename The filename to return the information for.
* @return array The data for the actual response to be returned.
*/
private function get_response_for_file( string $filename ): array {
$expiration_date = TransientFilesEngine::get_expiration_date( $filename );
$public_url = wc_get_container()->get( TransientFilesEngine::class )->get_public_url( $filename );
return array(
'receipt_url' => $public_url,
'expiration_date' => $expiration_date,
);
}
/**
* Get the accepted arguments for the GET request.
*
* @return array[] The accepted arguments for the GET request.
*/
private function get_args_for_get_order_receipt(): array {
return array(
'id' => array(
'description' => __( 'Unique identifier of the order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
}
/**
* Get the schema for both the GET and the POST requests.
*
* @return array[]
*/
private function get_schema_for_get_and_post_order_receipt(): array {
$schema = $this->get_base_schema();
$schema['properties'] = array(
'receipt_url' => array(
'description' => __( 'Public url of the receipt.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'expiration_date' => array(
'description' => __( 'Expiration date of the receipt, formatted as yyyy-mm-dd.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
return $schema;
}
/**
* Get the accepted arguments for the POST request.
*
* @return array[]
*/
private function get_args_for_create_order_receipt(): array {
return array(
'id' => array(
'description' => __( 'Unique identifier of the order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'expiration_date' => array(
'description' => __( 'Expiration date formatted as yyyy-mm-dd.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'default' => null,
),
'expiration_days' => array(
'description' => __( 'Number of days to be added to the current date to get the expiration date.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'default' => 1,
),
'force_new' => array(
'description' => __( 'True to force the creation of a new receipt even if one already exists and has not expired yet.', 'woocommerce' ),
'type' => 'boolean',
'required' => false,
'context' => array( 'view', 'edit' ),
'readonly' => true,
'default' => false,
),
);
}
}

View File

@@ -0,0 +1,106 @@
<?php /* phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped */ ?>
<html>
<head>
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php echo get_option( 'blog_charset' ); ?>" />
<style>
html { font-family: "Helvetica Neue", sans-serif; font-size: <?php echo $constants['font_size']; ?>pt; }
header { margin-top: <?php echo $constants['margin']; ?>; }
h1 { font-size: <?php echo $constants['title_font_size']; ?>pt; font-weight: 500; text-align: center; }
h3 { color: #707070; margin:0; }
table {
background-color:#F5F5F5;
width:100%;
color: #707070;
margin: <?php echo $constants['margin'] / 2; ?>pt 0;
padding: <?php echo $constants['margin'] / 2; ?>pt;
}
table td:last-child { width: 30%; text-align: right; }
table tr:last-child { color: #000000; font-weight: bold; }
footer {
font-size: <?php echo $constants['footer_font_size']; ?>pt;
border-top: 1px solid #707070;
margin-top: <?php echo $constants['margin']; ?>pt;
padding-top: <?php echo $constants['margin']; ?>pt;
}
p { line-height: <?php echo $constants['line_height']; ?>pt; margin: 0 0 <?php echo $constants['margin'] / 2; ?> 0; }
<?php if ( $payment_info ) { ?>
.card-icon {
width: <?php echo $constants['icon_width']; ?>pt;
height: <?php echo $constants['icon_height']; ?>pt;
vertical-align: top;
background-repeat: no-repeat;
background-position-y: center;
display: inline-block;
background-image: url("data:image/svg+xml;base64,<?php echo $payment_info['card_icon']; ?>");
}
<?php } ?>
</style>
</head>
<body>
<header>
<h1><?php echo $texts['receipt_title']; ?></h1>
<h3><?php echo strtoupper( $texts['amount_paid_section_title'] ); ?></h3>
<p>
<?php echo $formatted_amount; ?>
</p>
<h3><?php echo strtoupper( $texts['date_paid_section_title'] ); ?></h3>
<p>
<?php echo $formatted_date; ?>
</p>
<?php if ( $payment_method ) { ?>
<h3><?php echo strtoupper( $texts['payment_method_section_title'] ); ?></h3>
<p>
<?php if ( $payment_info ) { ?>
<span class="card-icon"></span> - <?php echo $payment_info['card_last4']; ?>
<?php } else { ?>
<p><?php echo $payment_method; ?></p>
<?php } ?>
</p>
<?php } ?>
</header>
<h3><?php echo strtoupper( $texts['summary_section_title'] ); ?></h3>
<table>
<?php
foreach ( $line_items as $line_item ) {
if ( isset( $line_item['quantity'] ) ) {
?>
<tr><td><?php echo $line_item['title']; ?> × <?php echo $line_item['quantity']; ?></td><td><?php echo $line_item['amount']; ?></td></tr>
<?php } else { ?>
<tr><td><?php echo $line_item['title']; ?></td><td><?php echo $line_item['amount']; ?></td></tr>
<?php
}
}
?>
</table>
<?php if ( ! empty( $notes ) ) { ?>
<h3><?php echo strtoupper( $texts['order_notes_section_title'] ); ?></h3>
<?php foreach ( $notes as $note ) { ?>
<p><?php echo $note; ?></p>
<?php
}
}
if ( $payment_info ) {
?>
<footer>
<p>
<?php
if ( $payment_info['app_name'] ) {
echo $texts['app_name'] . ': ' . $payment_info['app_name'] . '<br/>';
}
if ( $payment_info['aid'] ) {
echo $texts['aid'] . ': ' . $payment_info['aid'] . '<br/>';
}
if ( $payment_info['account_type'] ) {
echo $texts['account_type'] . ': ' . $payment_info['account_type'];
}
?>
</p>
</footer>
<?php } ?>
</body>
</html>

View File

@@ -0,0 +1,233 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Utilities\StringUtil;
use \WP_HTTP_Response;
use \WP_REST_Request;
use \WP_REST_Response;
use \WP_Error;
use \InvalidArgumentException;
use \Exception;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Base class for REST API controllers defined inside the 'src' directory.
*
* Besides implementing the abstract methods, derived classes must be registered in the dependency injection
* container with the 'share_with_implements_tags' method inside a service provider that inherits from
* 'AbstractInterfaceServiceProvider'. This ensures that 'register_routes' is invoked.
*
* Derived classes must also contain this line:
* use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
*
* Minimal controller example:
*
* class FoobarsController extends RestApiControllerBase {
*
* use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
*
* protected function get_rest_api_namespace(): string {
* return 'foobars';
* }
*
* public function register_routes() {
* register_rest_route(
* $this->route_namespace,
* '/foobars/(?P<id>[\d]+)',
* array(
* array(
* 'methods' => \WP_REST_Server::READABLE,
* 'callback' => fn( $request ) => $this->run( $request, 'get_foobar' ),
* 'permission_callback' => fn( $request ) => $this->check_permission( $request, 'read_foobars', $request->get_param( 'id' ) ),
* 'args' => $this->get_args_for_get_foobar(),
* 'schema' => $this->get_schema_for_get_foobar(),
* ),
* )
* );
* }
*
* protected function get_foobar( \WP_REST_Request $request ) {
* return array( 'message' => 'Get foobar with id ' . $request->get_param(' id' ) );
* }
*
* private function get_args_for_get_foobar(): array {
* return array(
* 'id' => array(
* 'description' => __( 'Unique identifier of the foobar.', 'woocommerce' ),
* 'type' => 'integer',
* 'context' => array( 'view', 'edit' ),
* 'readonly' => true,
* ),
* );
* }
*
* private function get_schema_for_get_foobar(): array {
* $schema = $this->get_base_schema();
* $schema['properties'] = array(
* 'message' => array(
* 'description' => __( 'A message.', 'woocommerce' ),
* 'type' => 'string',
* 'context' => array( 'view', 'edit' ),
* 'readonly' => true,
* ),
* );
* return $schema;
* }
*
* }
*/
abstract class RestApiControllerBase implements RegisterHooksInterface {
use AccessiblePrivateMethods;
/**
* The root namespace for the JSON REST API endpoints.
*
* @var string
*/
protected string $route_namespace = 'wc/v3';
/**
* Holds authentication error messages for each HTTP verb.
*
* @var array
*/
protected array $authentication_errors_by_method;
/**
* Class constructor.
*/
public function __construct() {
$this->authentication_errors_by_method = array(
'GET' => array(
'code' => 'woocommerce_rest_cannot_view',
'message' => __( 'Sorry, you cannot view resources.', 'woocommerce' ),
),
'POST' => array(
'code' => 'woocommerce_rest_cannot_create',
'message' => __( 'Sorry, you cannot create resources.', 'woocommerce' ),
),
'DELETE' => array(
'code' => 'woocommerce_rest_cannot_delete',
'message' => __( 'Sorry, you cannot delete resources.', 'woocommerce' ),
),
);
}
/**
* Register the hooks used by the class.
*/
public function register() {
static::add_filter( 'woocommerce_rest_api_get_rest_namespaces', array( $this, 'handle_woocommerce_rest_api_get_rest_namespaces' ) );
}
/**
* Handle the woocommerce_rest_api_get_rest_namespaces filter
* to add ourselves to the list of REST API controllers registered by WooCommerce.
*
* @param array $namespaces The original list of WooCommerce REST API namespaces/controllers.
* @return array The updated list of WooCommerce REST API namespaces/controllers.
*/
protected function handle_woocommerce_rest_api_get_rest_namespaces( array $namespaces ): array {
$namespaces['wc/v3'][ $this->get_rest_api_namespace() ] = static::class;
return $namespaces;
}
/**
* Get the WooCommerce REST API namespace for the class. It must be unique across all other derived classes
* and the keys returned by the 'get_vX_controllers' methods in includes/rest-api/Server.php.
* Note that this value is NOT related to the route namespace.
*
* @return string
*/
abstract protected function get_rest_api_namespace(): string;
/**
* Register the REST API endpoints handled by this controller.
*
* Use 'register_rest_route' in the usual way, it's recommended to use the 'run' method for 'callback'
* and the 'check_permission' method for 'permission_check', see the example in the class comment.
*/
abstract public function register_routes();
/**
* Handle a request for one of the provided REST API endpoints.
*
* If an exception is thrown, the exception message will be returned as part of the response
* if the user has the 'manage_woocommerce' capability.
*
* Note that the method specified in $method_name must have a 'protected' visibility and accept one argument of type 'WP_REST_Request'.
*
* @param WP_REST_Request $request The incoming HTTP REST request.
* @param string $method_name The name of the class method to execute. It must be protected and accept one argument of type 'WP_REST_Request'.
* @return WP_Error|WP_HTTP_Response|WP_REST_Response The response to send back to the client.
*/
protected function run( WP_REST_Request $request, string $method_name ) {
try {
return rest_ensure_response( $this->$method_name( $request ) );
} catch ( InvalidArgumentException $ex ) {
$message = $ex->getMessage();
return new WP_Error( 'woocommerce_rest_invalid_argument', $message ? $message : __( 'Internal server error', 'woocommerce' ), array( 'status' => 400 ) );
} catch ( Exception $ex ) {
wc_get_logger()->error( StringUtil::class_name_without_namespace( static::class ) . ": when executing method $method_name: {$ex->getMessage()}" );
return $this->internal_wp_error( $ex );
}
}
/**
* Return an WP_Error object for an internal server error, with exception information if the current user is an admin.
*
* @param Exception $exception The exception to maybe include information from.
* @return WP_Error
*/
protected function internal_wp_error( Exception $exception ): WP_Error {
$data = array( 'status' => 500 );
if ( current_user_can( 'manage_woocommerce' ) ) {
$data['exception_class'] = get_class( $exception );
$data['exception_message'] = $exception->getMessage();
$data['exception_trace'] = (array) $exception->getTrace();
}
$data['exception_message'] = $exception->getMessage();
return new WP_Error( 'woocommerce_rest_internal_error', __( 'Internal server error', 'woocommerce' ), $data );
}
/**
* Permission check for REST API endpoints, given the request method.
*
* @param WP_REST_Request $request The request for which the permission is checked.
* @param string $required_capability_name The name of the required capability.
* @param mixed ...$extra_args Extra arguments to be used for the permission check.
* @return bool|WP_Error True if the current user has the capability, otherwise an "Unauthorized" error or False if no error is available for the request method.
*/
protected function check_permission( WP_REST_Request $request, string $required_capability_name, ...$extra_args ) {
if ( current_user_can( $required_capability_name, $extra_args ) ) {
return true;
}
$error_information = $this->authentication_errors_by_method[ $request->get_method() ] ?? null;
if ( is_null( $error_information ) ) {
return false;
}
return new WP_Error(
$error_information['code'],
$error_information['message'],
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Get the base schema for the REST API endpoints.
*
* @return array
*/
protected function get_base_schema(): array {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'order receipts',
'type' => 'object',
);
}
}

View File

@@ -297,6 +297,12 @@ trait OrderAttributionMeta {
__( 'Direct', 'woocommerce' )
: 'Direct';
break;
case 'mobile_app':
$label = '';
$source = $translated ?
__( 'Mobile app', 'woocommerce' )
: 'Mobile app';
break;
case 'admin':
$label = '';
$source = $translated ?

View File

@@ -45,7 +45,7 @@ class TransientFilesEngine implements RegisterHooksInterface {
*
* @var LegacyProxy
*/
private LegacyProxy $legacy_proxy;
private $legacy_proxy;
/**
* Register hooks.
@@ -54,7 +54,7 @@ class TransientFilesEngine implements RegisterHooksInterface {
self::add_action( self::CLEANUP_ACTION_NAME, array( $this, 'handle_expired_files_cleanup_action' ) );
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_debug_tools_entries' ), 999, 1 );
self::add_action( 'init', array( $this, 'handle_init' ), 0 );
self::add_action( 'init', array( $this, 'add_endpoint' ), 0 );
self::add_filter( 'query_vars', array( $this, 'handle_query_vars' ), 0 );
self::add_action( 'parse_request', array( $this, 'handle_parse_request' ), 0 );
}
@@ -109,6 +109,14 @@ class TransientFilesEngine implements RegisterHooksInterface {
if ( ! $this->legacy_proxy->call_function( 'wp_mkdir_p', $transient_files_directory ) ) {
throw new Exception( "Can't create directory: $transient_files_directory" );
}
// Create infrastructure to prevent listing the contents of the transient files directory.
require_once ABSPATH . 'wp-admin/includes/file.php';
\WP_Filesystem();
$wp_filesystem = $this->legacy_proxy->get_global( 'wp_filesystem' );
$wp_filesystem->put_contents( $transient_files_directory . '/.htaccess', 'deny from all' );
$wp_filesystem->put_contents( $transient_files_directory . '/index.html', '' );
$realpathed_transient_files_directory = $this->legacy_proxy->call_function( 'realpath', $transient_files_directory );
} else {
throw new Exception( "The base transient files directory doesn't exist: $transient_files_directory" );
@@ -153,7 +161,8 @@ class TransientFilesEngine implements RegisterHooksInterface {
}
$filepath = $transient_files_directory . '/' . $filename;
WP_Filesystem();
require_once ABSPATH . 'wp-admin/includes/file.php';
\WP_Filesystem();
$wp_filesystem = $this->legacy_proxy->get_global( 'wp_filesystem' );
if ( false === $wp_filesystem->put_contents( $filepath, $file_contents ) ) {
throw new Exception( "Can't create file: $filepath" );
@@ -175,6 +184,23 @@ class TransientFilesEngine implements RegisterHooksInterface {
* @return string|null The full physical path of the file, or null if the files doesn't exist.
*/
public function get_transient_file_path( string $filename ): ?string {
$expiration_date = self::get_expiration_date( $filename );
if ( is_null( $expiration_date ) ) {
return null;
}
$file_path = $this->get_transient_files_directory() . '/' . $expiration_date . '/' . substr( $filename, 6 );
return is_file( $file_path ) ? $file_path : null;
}
/**
* Get the expiration date of a transient file based on its file name. The actual existence of the file is NOT checked.
*
* @param string $filename The name of the transient file to get the expiration date for.
* @return string|null Expiration date formatted as Y-m-d, null if the file name isn't encoding a proper date.
*/
public static function get_expiration_date( string $filename ) : ?string {
if ( strlen( $filename ) < 7 || ! ctype_xdigit( $filename ) ) {
return null;
}
@@ -185,13 +211,18 @@ class TransientFilesEngine implements RegisterHooksInterface {
hexdec( substr( $filename, 3, 1 ) ),
hexdec( substr( $filename, 4, 2 ) )
);
if ( ! TimeUtil::is_valid_date( $expiration_date, 'Y-m-d' ) ) {
return null;
}
$file_path = $this->get_transient_files_directory() . '/' . $expiration_date . '/' . substr( $filename, 6 );
return TimeUtil::is_valid_date( $expiration_date, 'Y-m-d' ) ? $expiration_date : null;
}
return is_file( $file_path ) ? $file_path : null;
/**
* Get the public URL of a transient file. The file name is NOT checked for validity or actual existence.
*
* @param string $filename The name of the transient file to get the public URL for.
* @return string The public URL of the file.
*/
public function get_public_url( string $filename ) {
return $this->legacy_proxy->call_function( 'get_site_url', null, '/wc/file/transient/' . $filename );
}
/**
@@ -394,7 +425,7 @@ class TransientFilesEngine implements RegisterHooksInterface {
/**
* Handle the "init" action, add rewrite rules for the "wc/file" endpoint.
*/
private function handle_init() {
public static function add_endpoint() {
add_rewrite_rule( '^wc/file/transient/?$', 'index.php?wc-transient-file-name=', 'top' );
add_rewrite_rule( '^wc/file/transient/(.+)$', 'index.php?wc-transient-file-name=$matches[1]', 'top' );
add_rewrite_endpoint( 'wc/file/transient', EP_ALL );