plugin updates

This commit is contained in:
Tony Volpe
2024-06-17 14:48:11 -04:00
parent ecc5fbf831
commit 3751a5a1a6
1318 changed files with 91130 additions and 52250 deletions

View File

@@ -51,7 +51,6 @@ class Init {
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
include_once WC_ABSPATH . 'includes/admin/class-wc-admin-upload-downloadable-product.php';
}
/**
@@ -105,6 +104,10 @@ class Init {
$product_form_controllers[] = 'Automattic\WooCommerce\Admin\API\ProductForm';
}
if ( Features::is_enabled( 'launch-your-store' ) ) {
$controllers[] = 'Automattic\WooCommerce\Admin\API\LaunchYourStore';
}
if ( Features::is_enabled( 'analytics' ) ) {
$analytics_controllers = array(
'Automattic\WooCommerce\Admin\API\Customers',
@@ -134,8 +137,7 @@ class Init {
// The performance indicators controller must be registered last, after other /stats endpoints have been registered.
$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
$controllers = array_merge( $controllers, $analytics_controllers, $product_form_controllers );
$controllers = array_merge( $controllers, $analytics_controllers, $product_form_controllers );
}
/**

View File

@@ -0,0 +1,98 @@
<?php
/**
* REST API Launch Your Store Controller
*
* Handles requests to /launch-your-store/*
*/
namespace Automattic\WooCommerce\Admin\API;
use Automattic\WooCommerce\Admin\WCAdminHelper;
defined( 'ABSPATH' ) || exit;
/**
* Launch Your Store controller.
*
* @internal
*/
class LaunchYourStore {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'launch-your-store';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/initialize-coming-soon',
array(
array(
'methods' => 'POST',
'callback' => array( $this, 'initialize_coming_soon' ),
'permission_callback' => array( $this, 'must_be_shop_manager_or_admin' ),
),
)
);
}
/**
* User must be either shop_manager or administrator.
*
* @return bool
*/
public function must_be_shop_manager_or_admin() {
// phpcs:ignore
if ( ! current_user_can( 'manage_woocommerce' ) && ! current_user_can( 'administrator' ) ) {
return false;
}
return true;
}
/**
* Initializes options for coming soon. Does not override if options exist.
*
* @return bool|void
*/
public function initialize_coming_soon() {
$current_user_id = get_current_user_id();
// Abort if we don't have a user id for some reason.
if ( ! $current_user_id ) {
return;
}
$coming_soon = 'yes';
$store_pages_only = WCAdminHelper::is_site_fresh() ? 'no' : 'yes';
$private_link = 'no';
$share_key = wp_generate_password( 32, false );
add_option( 'woocommerce_coming_soon', $coming_soon );
add_option( 'woocommerce_store_pages_only', $store_pages_only );
add_option( 'woocommerce_private_link', $private_link );
add_option( 'woocommerce_share_key', $share_key );
wc_admin_record_tracks_event(
'launch_your_store_initialize_coming_soon',
array(
'coming_soon' => $coming_soon,
'store_pages_only' => $store_pages_only,
'private_link' => $private_link,
)
);
return true;
}
}

View File

@@ -220,11 +220,12 @@ class Options extends \WC_REST_Data_Controller {
'woocommerce_admin_customize_store_completed',
'woocommerce_admin_customize_store_completed_theme_id',
'woocommerce_admin_customize_store_survey_completed',
'woocommerce_admin_launch_your_store_survey_completed',
'woocommerce_coming_soon',
'woocommerce_store_pages_only',
'woocommerce_private_link',
'woocommerce_share_key',
'launch-status',
'woocommerce_show_lys_tour',
// WC Test helper options.
'wc-admin-test-helper-rest-api-filters',
'wc_admin_helper_feature_values',

View File

@@ -211,24 +211,30 @@ class Controller extends ReportsController implements ExportableInterface {
'readonly' => true,
),
'extended_info' => array(
'products' => array(
'products' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'List of order product IDs, names, quantities.', 'woocommerce' ),
),
'coupons' => array(
'coupons' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'List of order coupons.', 'woocommerce' ),
),
'customer' => array(
'customer' => array(
'type' => 'object',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Order customer information.', 'woocommerce' ),
),
'attribution' => array(
'type' => 'object',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Order attribution information.', 'woocommerce' ),
),
),
),
);
@@ -526,6 +532,7 @@ class Controller extends ReportsController implements ExportableInterface {
'num_items_sold' => __( 'Items sold', 'woocommerce' ),
'coupons' => __( 'Coupon(s)', 'woocommerce' ),
'net_total' => __( 'N. Revenue', 'woocommerce' ),
'attribution' => __( 'Attribution', 'woocommerce' ),
);
/**
@@ -558,6 +565,7 @@ class Controller extends ReportsController implements ExportableInterface {
'num_items_sold' => $item['num_items_sold'],
'coupons' => isset( $item['extended_info']['coupons'] ) ? $this->get_coupons( $item['extended_info']['coupons'] ) : null,
'net_total' => $item['net_total'],
'attribution' => $item['extended_info']['attribution']['origin'],
);
/**

View File

@@ -7,6 +7,9 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
@@ -18,6 +21,7 @@ use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
* API\Reports\Orders\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
use OrderAttributionMeta;
/**
* Dynamically sets the date column name based on configuration
@@ -338,13 +342,14 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$orders_data, $query_args ) {
$mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' );
$related_orders = $this->get_orders_with_parent_id( $mapped_orders );
$order_ids = array_merge( array_keys( $mapped_orders ), array_keys( $related_orders ) );
$products = $this->get_products_by_order_ids( $order_ids );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$customers = $this->get_customers_by_orders( $orders_data );
$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );
$mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' );
$related_orders = $this->get_orders_with_parent_id( $mapped_orders );
$order_ids = array_merge( array_keys( $mapped_orders ), array_keys( $related_orders ) );
$products = $this->get_products_by_order_ids( $order_ids );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$order_attributions = $this->get_order_attributions_by_order_ids( array_keys( $mapped_orders ) );
$customers = $this->get_customers_by_orders( $orders_data );
$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );
$mapped_data = array();
foreach ( $products as $product ) {
@@ -384,7 +389,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
foreach ( $coupons as $coupon ) {
if ( ! isset( $mapped_data[ $coupon['order_id'] ] ) ) {
$mapped_data[ $product['order_id'] ]['coupons'] = array();
$mapped_data[ $coupon['order_id'] ]['coupons'] = array();
}
$mapped_data[ $coupon['order_id'] ]['coupons'][] = array(
@@ -394,15 +399,22 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
foreach ( $orders_data as $key => $order_data ) {
$defaults = array(
'products' => array(),
'coupons' => array(),
'customer' => array(),
$defaults = array(
'products' => array(),
'coupons' => array(),
'customer' => array(),
'attribution' => array(),
);
$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_data['order_id'] ] ) ? array_merge( $defaults, $mapped_data[ $order_data['order_id'] ] ) : $defaults;
$order_id = $order_data['order_id'];
$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_id ] ) ? array_merge( $defaults, $mapped_data[ $order_id ] ) : $defaults;
if ( $order_data['customer_id'] && isset( $mapped_customers[ $order_data['customer_id'] ] ) ) {
$orders_data[ $key ]['extended_info']['customer'] = $mapped_customers[ $order_data['customer_id'] ];
}
$source_type = $order_attributions[ $order_id ]['_wc_order_attribution_source_type'] ?? '';
$utm_source = $order_attributions[ $order_id ]['_wc_order_attribution_utm_source'] ?? '';
$orders_data[ $key ]['extended_info']['attribution']['origin'] = $this->get_origin_label( $source_type, $utm_source );
}
}
@@ -534,6 +546,52 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return $coupons;
}
/**
* Get order attributions data from order IDs.
*
* @param array $order_ids Array of order IDs.
* @return array
*/
protected function get_order_attributions_by_order_ids( $order_ids ) {
global $wpdb;
$order_meta_table = OrdersTableDataStore::get_meta_table_name();
$included_order_ids = implode( ',', array_map( 'absint', $order_ids ) );
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$order_attributions_meta = $wpdb->get_results(
"SELECT order_id, meta_key, meta_value
FROM $order_meta_table
WHERE order_id IN ({$included_order_ids})
AND meta_key IN ( '_wc_order_attribution_source_type', '_wc_order_attribution_utm_source' )
",
ARRAY_A
);
/* phpcs:enable */
} else {
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$order_attributions_meta = $wpdb->get_results(
"SELECT post_id as order_id, meta_key, meta_value
FROM $wpdb->postmeta
WHERE post_id IN ({$included_order_ids})
AND meta_key IN ( '_wc_order_attribution_source_type', '_wc_order_attribution_utm_source' )
",
ARRAY_A
);
/* phpcs:enable */
}
$order_attributions = array();
foreach ( $order_attributions_meta as $meta ) {
if ( ! isset( $order_attributions[ $meta['order_id'] ] ) ) {
$order_attributions[ $meta['order_id'] ] = array();
}
$order_attributions[ $meta['order_id'] ][ $meta['meta_key'] ] = $meta['meta_value'];
}
return $order_attributions;
}
/**
* Get all statuses that have been synced.
*

View File

@@ -2,15 +2,24 @@
namespace Automattic\WooCommerce\Admin\Features;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Admin\WCAdminHelper;
/**
* Takes care of Launch Your Store related actions.
*/
class LaunchYourStore {
const BANNER_DISMISS_USER_META_KEY = 'woocommerce_coming_soon_banner_dismissed';
/**
* Constructor.
*/
public function __construct() {
add_action( 'woocommerce_update_options_general', array( $this, 'save_site_visibility_options' ) );
add_action( 'woocommerce_update_options_site-visibility', array( $this, 'save_site_visibility_options' ) );
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'preload_settings' ) );
add_action( 'wp_footer', array( $this, 'maybe_add_coming_soon_banner_on_frontend' ) );
add_action( 'init', array( $this, 'register_launch_your_store_user_meta_fields' ) );
add_action( 'wp_login', array( $this, 'reset_woocommerce_coming_soon_banner_dismissed' ), 10, 2 );
}
/**
@@ -30,12 +39,163 @@ class LaunchYourStore {
'woocommerce_private_link' => array( 'yes', 'no' ),
);
$at_least_one_saved = false;
foreach ( $options as $name => $option ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( isset( $_POST[ $name ] ) && in_array( $_POST[ $name ], $option, true ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
update_option( $name, wp_unslash( $_POST[ $name ] ) );
$at_least_one_saved = true;
}
}
if ( $at_least_one_saved ) {
wc_admin_record_tracks_event( 'site_visibility_saved' );
}
}
/**
* Preload settings for Site Visibility.
*
* @param array $settings settings array.
*
* @return mixed
*/
public function preload_settings( $settings ) {
if ( ! is_admin() ) {
return $settings;
}
$current_screen = get_current_screen();
$is_setting_page = $current_screen && 'woocommerce_page_wc-settings' === $current_screen->id;
if ( $is_setting_page ) {
// Regnerate the share key if it's not set.
add_option( 'woocommerce_share_key', wp_generate_password( 32, false ) );
$settings['siteVisibilitySettings'] = array(
'shop_permalink' => get_permalink( wc_get_page_id( 'shop' ) ),
'woocommerce_coming_soon' => get_option( 'woocommerce_coming_soon' ),
'woocommerce_store_pages_only' => get_option( 'woocommerce_store_pages_only' ),
'woocommerce_private_link' => get_option( 'woocommerce_private_link' ),
'woocommerce_share_key' => get_option( 'woocommerce_share_key' ),
);
}
return $settings;
}
/**
* User must be an admin or editor.
*
* @return bool
*/
private function is_manager_or_admin() {
// phpcs:ignore
if ( ! current_user_can( 'shop_manager' ) && ! current_user_can( 'administrator' ) ) {
return false;
}
return true;
}
/**
* Add 'coming soon' banner on the frontend when the following conditions met.
*
* - User must be either an admin or store editor (must be logged in).
* - 'woocommerce_coming_soon' option value must be 'yes'
* - The page must not be the Coming soon page itself.
*/
public function maybe_add_coming_soon_banner_on_frontend() {
// Do not show the banner if the site is being previewed.
if ( isset( $_GET['site-preview'] ) ) { // @phpcs:ignore
return false;
}
$current_user_id = get_current_user_id();
if ( ! $current_user_id ) {
return false;
}
if ( get_user_meta( $current_user_id, self::BANNER_DISMISS_USER_META_KEY, true ) === 'yes' ) {
return false;
}
if ( ! $this->is_manager_or_admin() ) {
return false;
}
// 'woocommerce_coming_soon' must be 'yes'
if ( get_option( 'woocommerce_coming_soon', 'no' ) !== 'yes' ) {
return false;
}
$store_pages_only = get_option( 'woocommerce_store_pages_only' ) === 'yes';
if ( $store_pages_only && ! WCAdminHelper::is_store_page() ) {
return false;
}
$link = admin_url( 'admin.php?page=wc-settings&tab=site-visibility' );
$rest_url = rest_url( 'wp/v2/users/' . $current_user_id );
$rest_nonce = wp_create_nonce( 'wp_rest' );
$text = sprintf(
// translators: no need to translate it. It's a link.
__(
"
This page is in \"Coming soon\" mode and is only visible to you and those who have permission. To make it public to everyone,&nbsp;<a href='%s'>change visibility settings</a>
",
'woocommerce'
),
$link
);
// phpcs:ignore
echo "<div id='coming-soon-footer-banner'>$text<a class='coming-soon-footer-banner-dismiss' data-rest-url='$rest_url' data-rest-nonce='$rest_nonce'></a></div>";
}
/**
* Register user meta fields for Launch Your Store.
*/
public function register_launch_your_store_user_meta_fields() {
if ( ! $this->is_manager_or_admin() ) {
return;
}
register_meta(
'user',
'woocommerce_launch_your_store_tour_hidden',
array(
'type' => 'string',
'description' => 'Indicate whether the user has dismissed the site visibility tour on the home screen.',
'single' => true,
'show_in_rest' => true,
)
);
register_meta(
'user',
self::BANNER_DISMISS_USER_META_KEY,
array(
'type' => 'string',
'description' => 'Indicate whether the user has dismissed the coming soon notice or not.',
'single' => true,
'show_in_rest' => true,
)
);
}
/**
* Reset 'woocommerce_coming_soon_banner_dismissed' user meta to 'no'.
*
* Runs when a user logs-in successfully.
*
* @param string $user_login user login.
* @param object $user user object.
*/
public function reset_woocommerce_coming_soon_banner_dismissed( $user_login, $user ) {
$existing_meta = get_user_meta( $user->ID, self::BANNER_DISMISS_USER_META_KEY, true );
if ( 'yes' === $existing_meta ) {
update_user_meta( $user->ID, self::BANNER_DISMISS_USER_META_KEY, 'no' );
}
}
}

View File

@@ -165,6 +165,7 @@ class TaskLists {
),
),
'tasks' => array(
'StoreConnect',
'AdditionalPayments',
'GetMobileApp',
),

View File

@@ -9,13 +9,24 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
* Launch Your Store Task
*/
class LaunchYourStore extends Task {
/**
* Constructor
*
* @param TaskList $task_list Parent task list.
*/
public function __construct( $task_list ) {
parent::__construct( $task_list );
add_action( 'show_admin_bar', array( $this, 'possibly_hide_wp_admin_bar' ) );
}
/**
* ID.
*
* @return string
*/
public function get_id() {
return 'launch_your_store';
return 'launch-your-store';
}
/**
@@ -48,21 +59,22 @@ class LaunchYourStore extends Task {
return '';
}
/**
* Action URL.
*
* @return string
*/
public function get_action_url() {
return admin_url( 'wp-admin/admin.php?page=wc-admin&path=%2Flaunch-your-store' );
}
/**
* Task completion.
*
* @return bool
*/
public function is_complete() {
$launch_status = get_option( 'launch-status' );
// The site is launched when the launch status is 'launched' or missing.
$launched_values = array(
'launched',
'',
false,
);
return in_array( $launch_status, $launched_values, true );
return 'yes' !== get_option( 'woocommerce_coming_soon' );
}
/**
@@ -73,4 +85,40 @@ class LaunchYourStore extends Task {
public function can_view() {
return Features::is_enabled( 'launch-your-store' );
}
/**
* Hide the WP admin bar when the user is previewing the site.
*
* @param bool $show Whether to show the admin bar.
*/
public function possibly_hide_wp_admin_bar( $show ) {
if ( isset( $_GET['site-preview'] ) ) { // @phpcs:ignore
return false;
}
global $wp;
$http_referer = wp_get_referer() ?? '';
$parsed_url = wp_parse_url( $http_referer, PHP_URL_QUERY );
$query_string = is_string( $parsed_url ) ? $parsed_url : '';
// Check if the user is coming from the site preview link.
if ( strpos( $query_string, 'site-preview' ) !== false ) {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return $show;
}
// Redirect to the current URL with the site-preview query string.
$current_url =
add_query_arg(
array(
'site-preview' => 1,
),
esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
);
wp_safe_redirect( $current_url );
exit;
}
return $show;
}
}

View File

@@ -45,7 +45,7 @@ class Shipping extends Task {
* @return string
*/
public function get_title() {
return __( 'Get your products shipped', 'woocommerce' );
return __( 'Select your shipping options', 'woocommerce' );
}
/**

View File

@@ -0,0 +1,73 @@
<?php
namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
/**
* Connect store to WooCommerce.com Task
*/
class StoreConnect extends Task {
/**
* ID.
*
* @return string
*/
public function get_id() {
return 'connect-store';
}
/**
* Title.
*
* @return string
*/
public function get_title() {
return __( 'Manage your WooCommerce.com Marketplace subscriptions', 'woocommerce' );
}
/**
* Content.
*
* @return string
*/
public function get_content() {
return '';
}
/**
* Time.
*
* @return string
*/
public function get_time() {
return '';
}
/**
* Task completion.
*
* @return bool
*/
public function is_complete() {
return \WC_Helper::is_site_connected();
}
/**
* Always dismissable.
*
* @return bool
*/
public function is_dismissable() {
return true;
}
/**
* Action URL.
*
* @return string
*/
public function get_action_url() {
return admin_url( 'admin.php?page=wc-admin&tab=my-subscriptions&path=/extensions' );
}
}

View File

@@ -78,6 +78,8 @@ class Init {
add_action( 'rest_api_init', array( $this, 'register_layout_templates' ) );
add_action( 'rest_api_init', array( $this, 'register_user_metas' ) );
add_filter( 'register_block_type_args', array( $this, 'register_metadata_attribute' ) );
// Make sure the block registry is initialized so that core blocks are registered.
BlockRegistry::get_instance();
@@ -113,6 +115,9 @@ class Init {
);
wp_tinymce_inline_scripts();
wp_enqueue_media();
wp_register_style( 'wc-global-presets', false ); // phpcs:ignore
wp_add_inline_style( 'wc-global-presets', wp_get_global_stylesheet( array( 'presets' ) ) );
wp_enqueue_style( 'wc-global-presets' );
}
/**
@@ -417,4 +422,31 @@ class Init {
)
);
}
/**
* Registers the metadata block attribute for all block types.
* This is a fallback/temporary solution until
* the Gutenberg core version registers the metadata attribute.
*
* @see https://github.com/WordPress/gutenberg/blob/6aaa3686ae67adc1a6a6b08096d3312859733e1b/lib/compat/wordpress-6.5/blocks.php#L27-L47
* To do: Remove this method once the Gutenberg core version registers the metadata attribute.
*
* @param array $args Array of arguments for registering a block type.
* @return array $args
*/
public function register_metadata_attribute( $args ) {
// Setup attributes if needed.
if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) {
$args['attributes'] = array();
}
// Add metadata attribute if it doesn't exist.
if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) {
$args['attributes']['metadata'] = array(
'type' => 'object',
);
}
return $args;
}
}

View File

@@ -61,6 +61,13 @@ class ProductTemplate {
*/
private $icon = null;
/**
* If the template is directly selectable through the UI.
*
* @var boolean
*/
private $is_selectable_by_user = true;
/**
* ProductTemplate constructor
*
@@ -86,6 +93,10 @@ class ProductTemplate {
if ( isset( $data['icon'] ) ) {
$this->icon = $data['icon'];
}
if ( isset( $data['is_selectable_by_user'] ) ) {
$this->is_selectable_by_user = $data['is_selectable_by_user'];
}
}
/**
@@ -180,6 +191,15 @@ class ProductTemplate {
return $this->order;
}
/**
* Get the selectable attribute.
*
* @return boolean Selectable.
*/
public function get_is_selectable_by_user() {
return $this->is_selectable_by_user;
}
/**
* Set the template order.
*
@@ -196,13 +216,14 @@ class ProductTemplate {
*/
public function to_json() {
return array(
'id' => $this->get_id(),
'title' => $this->get_title(),
'description' => $this->get_description(),
'icon' => $this->get_icon(),
'order' => $this->get_order(),
'layoutTemplateId' => $this->get_layout_template_id(),
'productData' => $this->get_product_data(),
'id' => $this->get_id(),
'title' => $this->get_title(),
'description' => $this->get_description(),
'icon' => $this->get_icon(),
'order' => $this->get_order(),
'layoutTemplateId' => $this->get_layout_template_id(),
'productData' => $this->get_product_data(),
'isSelectableByUser' => $this->get_is_selectable_by_user(),
);
}
}

View File

@@ -13,7 +13,10 @@ use ActionScheduler_QueueRunner;
use Automatic_Upgrader_Skin;
use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsyncPluginsInstallLogger;
use Automattic\WooCommerce\Admin\PluginsInstallLoggers\PluginsInstallLogger;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use Plugin_Upgrader;
use WC_Helper;
use WC_Helper_Updater;
use WP_Error;
use WP_Upgrader;
@@ -35,6 +38,9 @@ class PluginsHelper {
add_action( 'woocommerce_plugins_install_callback', array( __CLASS__, 'install_plugins' ), 10, 2 );
add_action( 'woocommerce_plugins_install_and_activate_async_callback', array( __CLASS__, 'install_and_activate_plugins_async_callback' ), 10, 2 );
add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 );
add_action( 'admin_notices', array( __CLASS__, 'maybe_show_connect_notice_in_plugin_list' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_connect_notice' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scripts_for_connect_notice_in_plugins' ) );
}
/**
@@ -532,4 +538,80 @@ class PluginsHelper {
return self::get_action_data( $actions );
}
/**
* Show notices to connect to woocommerce.com for unconnected store in the plugin list.
*
* @return void
*/
public static function maybe_show_connect_notice_in_plugin_list() {
if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
return;
}
$notice_type = WC_Helper_Updater::get_woo_connect_notice_type();
if ( 'none' === $notice_type ) {
return;
}
$notice_string = '';
if ( 'long' === $notice_type ) {
$notice_string .= __( 'Your store might be at risk as you are running old versions of WooCommerce plugins.', 'woocommerce' );
$notice_string .= ' ';
}
$connect_page_url = add_query_arg(
array(
'page' => 'wc-admin',
'tab' => 'my-subscriptions',
'path' => rawurlencode( '/extensions' ),
),
admin_url( 'admin.php' )
);
$notice_string .= sprintf(
/* translators: %s: Connect page URL */
__( '<a id="woo-connect-notice-url" href="%s">Connect your store</a> to WooCommerce.com to get updates and streamlined support for your subscriptions.', 'woocommerce' ),
esc_url( $connect_page_url )
);
echo '<div class="woo-connect-notice notice notice-error is-dismissible">
<p class="widefat">' . wp_kses_post( $notice_string ) . '</p>
</div>';
}
/**
* Enqueue scripts for connect notice in WooCommerce settings page.
*
* @return void
*/
public static function maybe_enqueue_scripts_for_connect_notice() {
if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
return;
}
$notice_type = WC_Helper_Updater::get_woo_connect_notice_type();
if ( 'none' === $notice_type ) {
return;
}
WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-connect-notice' );
wp_enqueue_script( 'woo-connect-notice' );
}
/**
* Enqueue scripts for connect notice in plugin list page.
*
* @return void
*/
public static function maybe_enqueue_scripts_for_connect_notice_in_plugins() {
if ( 'plugins' !== get_current_screen()->id ) {
return;
}
WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-plugin-update-connect-notice' );
wp_enqueue_script( 'woo-plugin-update-connect-notice' );
}
}

View File

@@ -128,7 +128,6 @@ class WCAdminHelper {
*
* Store pages are defined as:
*
* - My Account
* - Shop
* - Cart
* - Checkout
@@ -153,12 +152,12 @@ class WCAdminHelper {
// WC store pages.
$store_pages = array(
'myaccount' => wc_get_page_id( 'myaccount' ),
'shop' => wc_get_page_id( 'shop' ),
'cart' => wc_get_page_id( 'cart' ),
'checkout' => wc_get_page_id( 'checkout' ),
'privacy' => wc_privacy_policy_page_id(),
'terms' => wc_terms_and_conditions_page_id(),
'shop' => wc_get_page_id( 'shop' ),
'cart' => wc_get_page_id( 'cart' ),
'checkout' => wc_get_page_id( 'checkout' ),
'privacy' => wc_privacy_policy_page_id(),
'terms' => wc_terms_and_conditions_page_id(),
'coming_soon' => wc_get_page_id( 'coming_soon' ),
);
/**
@@ -237,7 +236,7 @@ class WCAdminHelper {
private static function get_normalized_url_path( $url ) {
$query = wp_parse_url( $url, PHP_URL_QUERY );
$path = wp_parse_url( $url, PHP_URL_PATH ) . ( $query ? '?' . $query : '' );
$home_path = wp_parse_url( site_url(), PHP_URL_PATH );
$home_path = wp_parse_url( site_url(), PHP_URL_PATH ) ?? '';
$normalized_path = trim( substr( $path, strlen( $home_path ) ), '/' );
return $normalized_path;
}

View File

@@ -13,6 +13,7 @@ use WpOrg\Requests\Requests;
*/
class Connection {
const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion';
const MODEL = 'gpt-3.5-turbo-1106';
/**
* The post request.
@@ -33,6 +34,7 @@ class Connection {
'feature' => 'woocommerce_blocks_patterns',
'prompt' => $prompt,
'token' => $token,
'model' => self::MODEL,
);
if ( $response_format ) {
@@ -77,6 +79,7 @@ class Connection {
'feature' => 'woocommerce_blocks_patterns',
'prompt' => $prompt,
'token' => $token,
'model' => self::MODEL,
);
if ( $response_format ) {

View File

@@ -80,24 +80,25 @@ class AssetDataRegistry {
*/
protected function get_core_data() {
return [
'adminUrl' => admin_url(),
'countries' => WC()->countries->get_countries(),
'currency' => $this->get_currency_data(),
'currentUserId' => get_current_user_id(),
'currentUserIsAdmin' => current_user_can( 'manage_woocommerce' ),
'dateFormat' => wc_date_format(),
'homeUrl' => esc_url( home_url( '/' ) ),
'locale' => $this->get_locale_data(),
'dashboardUrl' => wc_get_account_endpoint_url( 'dashboard' ),
'orderStatuses' => $this->get_order_statuses(),
'placeholderImgSrc' => wc_placeholder_img_src(),
'productsSettings' => $this->get_products_settings(),
'siteTitle' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
'storePages' => $this->get_store_pages(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'wcVersion' => defined( 'WC_VERSION' ) ? WC_VERSION : '',
'wpLoginUrl' => wp_login_url(),
'wpVersion' => get_bloginfo( 'version' ),
'adminUrl' => admin_url(),
'countries' => WC()->countries->get_countries(),
'currency' => $this->get_currency_data(),
'currentUserId' => get_current_user_id(),
'currentUserIsAdmin' => current_user_can( 'manage_woocommerce' ),
'currentThemeIsFSETheme' => wc_current_theme_is_fse_theme(),
'dateFormat' => wc_date_format(),
'homeUrl' => esc_url( home_url( '/' ) ),
'locale' => $this->get_locale_data(),
'dashboardUrl' => wc_get_account_endpoint_url( 'dashboard' ),
'orderStatuses' => $this->get_order_statuses(),
'placeholderImgSrc' => wc_placeholder_img_src(),
'productsSettings' => $this->get_products_settings(),
'siteTitle' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
'storePages' => $this->get_store_pages(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'wcVersion' => defined( 'WC_VERSION' ) ? WC_VERSION : '',
'wpLoginUrl' => wp_login_url(),
'wpVersion' => get_bloginfo( 'version' ),
];
}
@@ -332,7 +333,7 @@ class AssetDataRegistry {
public function hydrate_data_from_api_request( $key, $path, $check_key_exists = false ) {
$this->add(
$key,
function() use ( $path ) {
function () use ( $path ) {
if ( isset( $this->preloaded_api_requests[ $path ], $this->preloaded_api_requests[ $path ]['body'] ) ) {
return $this->preloaded_api_requests[ $path ]['body'];
}

View File

@@ -50,6 +50,7 @@ final class AssetsController {
$this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
$this->register_style( 'wc-blocks-editor-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-editor-style', 'css' ), dirname( __DIR__ ) ), array( 'wp-edit-blocks' ), 'all', true );
$this->api->register_script( 'wc-types', $this->api->get_block_asset_build_path( 'wc-types' ), array(), false );
$this->api->register_script( 'wc-blocks-middleware', 'assets/client/blocks/wc-blocks-middleware.js', array(), false );
$this->api->register_script( 'wc-blocks-data-store', 'assets/client/blocks/wc-blocks-data.js', array( 'wc-blocks-middleware' ) );
$this->api->register_script( 'wc-blocks-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-vendors' ), array(), false );
@@ -61,8 +62,9 @@ final class AssetsController {
// The price package is shared externally so has no blocks prefix.
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array() );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array() );
$this->api->register_script( 'wc-blocks-vendors-frontend', $this->api->get_block_asset_build_path( 'wc-blocks-vendors-frontend' ), array(), false );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array( 'wc-blocks-vendors-frontend' ) );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array( 'wc-blocks-vendors-frontend' ) );
// Register the interactivity components here for now.
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
@@ -206,7 +208,7 @@ final class AssetsController {
$this->get_script_dependency_src_array( $script_data['dependencies'] )
);
return array_map(
function( $src ) {
function ( $src ) {
return array(
'href' => $src,
'as' => 'script',
@@ -226,7 +228,7 @@ final class AssetsController {
$wp_scripts = wp_scripts();
return array_reduce(
$dependencies,
function( $src, $handle ) use ( $wp_scripts ) {
function ( $src, $handle ) use ( $wp_scripts ) {
if ( isset( $wp_scripts->registered[ $handle ] ) ) {
$src[] = esc_url( add_query_arg( 'ver', $wp_scripts->registered[ $handle ]->ver, $this->get_absolute_url( $wp_scripts->registered[ $handle ]->src ) ) );
$src = array_merge( $src, $this->get_script_dependency_src_array( $wp_scripts->registered[ $handle ]->deps ) );

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Images\Pexels;
use Automattic\WooCommerce\Blocks\Domain\Package;
@@ -119,6 +120,7 @@ class BlockPatterns {
'keywords' => 'Keywords',
'blockTypes' => 'Block Types',
'inserter' => 'Inserter',
'featureFlag' => 'Feature Flag',
);
if ( ! file_exists( $this->patterns_path ) ) {
@@ -170,6 +172,10 @@ class BlockPatterns {
continue;
}
if ( $pattern_data['featureFlag'] && ! Features::is_enabled( $pattern_data['featureFlag'] ) ) {
continue;
}
// Title is a required property.
if ( ! $pattern_data['title'] ) {
_doing_it_wrong(

View File

@@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate;
/**
* BlockTypesController class.
@@ -26,7 +27,6 @@ class BlockTemplatesController {
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
add_filter( 'post_type_archive_title', array( $this, 'update_product_archive_title' ), 10, 2 );
@@ -36,13 +36,13 @@ class BlockTemplatesController {
// This render_callback wrapper allows us to add support for plugin-housed template parts.
add_filter(
'block_type_metadata_settings',
function( $settings, $metadata ) {
function ( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], [ 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ], true )
in_array( $settings['render_callback'], array( 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ), true )
) {
$settings['render_callback'] = [ $this, 'render_woocommerce_template_part' ];
$settings['render_callback'] = array( $this, 'render_woocommerce_template_part' );
}
return $settings;
},
@@ -54,13 +54,13 @@ class BlockTemplatesController {
// @see https://core.trac.wordpress.org/ticket/58366 for more info.
add_filter(
'block_type_metadata_settings',
function( $settings, $metadata ) {
function ( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$settings['original_render_callback'] = $settings['render_callback'];
$settings['render_callback'] = function( $attributes, $content ) use ( $settings ) {
$settings['render_callback'] = function ( $attributes, $content ) use ( $settings ) {
// The shortcode has already been rendered, so look for the cart/checkout HTML.
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
@@ -92,7 +92,7 @@ class BlockTemplatesController {
$current_screen = get_current_screen();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $current_screen && 'page' === $current_screen->id && ! empty( $_GET['post'] ) && in_array( absint( $_GET['post'] ), [ wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ) ], true ) ) {
if ( $current_screen && 'page' === $current_screen->id && ! empty( $_GET['post'] ) && in_array( absint( $_GET['post'] ), array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ) ), true ) ) {
wp_add_inline_style( 'wc-blocks-editor-style', '.edit-post-post-template { display: none; }' );
}
},
@@ -202,7 +202,7 @@ class BlockTemplatesController {
$templates_eligible_for_fallback = array_filter(
$template_slugs,
function( $template_slug ) {
function ( $template_slug ) {
return BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $template_slug );
}
);
@@ -307,19 +307,7 @@ class BlockTemplatesController {
* @return WP_Block_Template|null
*/
public function add_block_template_details( $block_template, $id, $template_type ) {
if ( ! $block_template ) {
return $block_template;
}
if ( ! BlockTemplateUtils::template_has_title( $block_template ) ) {
$block_template->title = BlockTemplateUtils::get_block_template_title( $block_template->slug );
}
if ( ! $block_template->description ) {
$block_template->description = BlockTemplateUtils::get_block_template_description( $block_template->slug );
}
if ( ! $block_template->area || 'uncategorized' === $block_template->area ) {
$block_template->area = BlockTemplateUtils::get_block_template_area( $block_template->slug, $template_type );
}
return $block_template;
return BlockTemplateUtils::update_template_data( $block_template, $template_type );
}
/**
@@ -331,12 +319,13 @@ class BlockTemplatesController {
* @return array
*/
public function add_block_templates( $query_result, $query, $template_type ) {
if ( ! BlockTemplateUtils::supports_block_templates( $template_type ) ) {
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
if ( ! BlockTemplateUtils::supports_block_templates( $template_type ) && ! in_array( ComingSoonTemplate::SLUG, $slugs, true ) ) {
return $query_result;
}
$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
$template_files = $this->get_block_templates( $slugs, $template_type );
$theme_slug = wp_get_theme()->get_stylesheet();
@@ -398,18 +387,8 @@ class BlockTemplatesController {
* templates that aren't listed in theme.json.
*/
$query_result = array_map(
function( $template ) use ( $template_type ) {
if ( ! BlockTemplateUtils::template_has_title( $template ) ) {
$template->title = BlockTemplateUtils::get_block_template_title( $template->slug );
}
if ( ! $template->description ) {
$template->description = BlockTemplateUtils::get_block_template_description( $template->slug );
}
if ( ! $template->area || 'uncategorized' === $template->area ) {
$template->area = BlockTemplateUtils::get_block_template_area( $template->slug, $template_type );
}
return $template;
function ( $template ) use ( $template_type ) {
return BlockTemplateUtils::update_template_data( $template, $template_type );
},
$query_result
);
@@ -441,16 +420,10 @@ class BlockTemplatesController {
* @return array Templates from the WooCommerce blocks plugin directory.
*/
public function get_block_templates_from_woocommerce( $slugs, $already_found_templates, $template_type = 'wp_template' ) {
$directory = BlockTemplateUtils::get_templates_directory( $template_type );
$template_files = BlockTemplateUtils::get_template_paths( $directory );
$template_files = BlockTemplateUtils::get_template_paths( $template_type );
$templates = array();
foreach ( $template_files as $template_file ) {
// Skip the Product Gallery template part, as it is not supposed to be exposed at this point.
if ( str_contains( $template_file, 'templates/parts/product-gallery.html' ) ) {
continue;
}
// Skip the template if it's blockified, and we should only use classic ones.
if ( ! BlockTemplateUtils::should_use_blockified_product_grid_templates() && strpos( $template_file, 'blockified' ) !== false ) {
continue;
@@ -582,30 +555,4 @@ class BlockTemplatesController {
return $post_type_name;
}
/**
* Remove the template panel from the Sidebar of the Shop page because
* the Site Editor handles it.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/6278
*
* @param bool $is_support Whether the active theme supports block templates.
*
* @return bool
*/
public function remove_block_template_support_for_shop_page( $is_support ) {
global $pagenow, $post;
if (
is_admin() &&
'post.php' === $pagenow &&
function_exists( 'wc_get_page_id' ) &&
is_a( $post, 'WP_Post' ) &&
wc_get_page_id( 'shop' ) === $post->ID
) {
return false;
}
return $is_support;
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\AbstractTemplate;
use Automattic\WooCommerce\Blocks\Templates\AbstractTemplatePart;
@@ -8,6 +9,9 @@ use Automattic\WooCommerce\Blocks\Templates\MiniCartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutHeaderTemplate;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonEntireSiteTemplate;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonStoreOnlyTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
@@ -49,6 +53,9 @@ class BlockTemplatesRegistry {
} else {
$templates = array();
}
if ( Features::is_enabled( 'launch-your-store' ) ) {
$templates[ ComingSoonTemplate::SLUG ] = new ComingSoonTemplate();
}
if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
$template_parts = array(
MiniCartTemplate::SLUG => new MiniCartTemplate(),

View File

@@ -463,6 +463,25 @@ abstract class AbstractBlock {
* @return array
*/
protected function get_routes_from_namespace( $namespace ) {
/**
* Gives opportunity to return routes without invoking the compute intensive REST API.
*
* @since 8.7.0
* @param array $routes Array of routes.
* @param string $namespace Namespace for routes.
* @param string $context Context, can be edit or view.
*/
$routes = apply_filters(
'woocommerce_blocks_pre_get_routes_from_namespace',
array(),
$namespace,
'view'
);
if ( ! empty( $routes ) ) {
return $routes;
}
$rest_server = rest_get_server();
$namespace_index = $rest_server->get_namespace_index(
[

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
/**
* Cart class.
@@ -245,9 +246,8 @@ class Cart extends AbstractBlock {
$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() );
$pickup_location_settings = get_option( 'woocommerce_pickup_location_settings', [] );
$this->asset_data_registry->add( 'localPickupEnabled', wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' ) );
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings();
$this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] );
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
@@ -285,6 +285,7 @@ class Cart extends AbstractBlock {
'Cart',
'CartOrderSummaryTaxesBlock',
'CartOrderSummarySubtotalBlock',
'CartOrderSummaryTotalsBlock',
'FilledCartBlock',
'EmptyCartBlock',
'CartTotalsBlock',

View File

@@ -11,4 +11,68 @@ class CartOrderSummaryBlock extends AbstractInnerBlock {
* @var string
*/
protected $block_name = 'cart-order-summary-block';
/**
* Get the contents of the given inner block.
*
* @param string $block_name Name of the order summary inner block.
* @param string $content The content to search.
* @return array|bool
*/
private function get_inner_block_content( $block_name, $content ) {
if ( preg_match( $this->inner_block_regex( $block_name ), $content, $matches ) ) {
return $matches[0];
}
return false;
}
/**
* Get the regex that will return an inner block.
*
* @param string $block_name Name of the order summary inner block.
* @return string Regex pattern.
*/
private function inner_block_regex( $block_name ) {
return '/<div data-block-name="woocommerce\/cart-order-summary-' . $block_name . '-block"(.+?)>(.*?)<\/div>/si';
}
/**
* Render the Cart Order Summary block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param object $block Block object.
* @return string Rendered block.
*/
protected function render( $attributes, $content, $block ) {
// The order-summary-totals block was introduced as a new parent block for the totals
// (subtotal, discount, fees, shipping and taxes) blocks.
$regex_for_cart_order_summary_totals = '/<div data-block-name="woocommerce\/cart-order-summary-totals-block"(.+?)>/';
$order_summary_totals_content = '<div data-block-name="woocommerce/cart-order-summary-totals-block" class="wp-block-woocommerce-cart-order-summary-totals-block">';
$totals_inner_blocks = array( 'subtotal', 'discount', 'fee', 'shipping', 'taxes' ); // We want to move these blocks inside a parent 'totals' block.
if ( preg_match( $regex_for_cart_order_summary_totals, $content ) ) {
return $content;
}
foreach ( $totals_inner_blocks as $key => $block_name ) {
$inner_block_content = $this->get_inner_block_content( $block_name, $content );
if ( $inner_block_content ) {
$order_summary_totals_content .= "\n" . $inner_block_content;
// The last block is replaced with the totals block.
if ( count( $totals_inner_blocks ) - 1 === $key ) {
$order_summary_totals_content .= '</div>';
$content = preg_replace( $this->inner_block_regex( $block_name ), $order_summary_totals_content, $content );
} else {
// Otherwise, remove the block.
$content = preg_replace( $this->inner_block_regex( $block_name ), '', $content );
}
}
}
return preg_replace( '/\n\n( *?)/i', '', $content );
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryTotalsBlock class.
*/
class CartOrderSummaryTotalsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-totals-block';
}

View File

@@ -41,6 +41,8 @@ class Checkout extends AbstractBlock {
return isset( $_GET['_wp-find-template'] ) ? false : $return;
}
);
add_action( 'save_post', array( $this, 'update_local_pickup_title' ), 10, 2 );
}
/**
@@ -242,6 +244,73 @@ class Checkout extends AbstractBlock {
return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' );
}
/**
* Update the local pickup title in WooCommerce Settings when the checkout page containing a Checkout block is saved.
*
* @param int $post_id The post ID.
* @param \WP_Post $post The post object.
* @return void
*/
public function update_local_pickup_title( $post_id, $post ) {
// This is not a proper save action, maybe an autosave, so don't continue.
if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) {
return;
}
// Check if we are editing the checkout page and that it contains a Checkout block.
// Cast to string for Checkout page ID comparison because get_option can return it as a string, so better to compare both values as strings.
if ( ! empty( $post->post_type ) && 'wp_template' !== $post->post_type && ( false === has_block( 'woocommerce/checkout', $post ) || (string) get_option( 'woocommerce_checkout_page_id' ) !== (string) $post_id ) ) {
return;
}
if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && 'page-checkout' !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( 'woocommerce/checkout', $post ) ) {
return;
}
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings( 'edit' );
if ( ! isset( $pickup_location_settings['title'] ) ) {
return;
}
if ( empty( $post->post_content ) ) {
return;
}
$post_blocks = parse_blocks( $post->post_content );
$title = $this->find_local_pickup_text_in_checkout_block( $post_blocks );
if ( $title ) {
$pickup_location_settings['title'] = $title;
update_option( 'woocommerce_pickup_location_settings', $pickup_location_settings );
}
}
/**
* Recurse through the blocks to find the shipping methods block, then get the value of the localPickupText attribute from it.
*
* @param array $blocks The block(s) to search for the local pickup text.
* @return null|string The local pickup text if found, otherwise void.
*/
private function find_local_pickup_text_in_checkout_block( $blocks ) {
if ( ! is_array( $blocks ) ) {
return null;
}
foreach ( $blocks as $block ) {
if ( ! empty( $block['blockName'] ) && 'woocommerce/checkout-shipping-method-block' === $block['blockName'] ) {
if ( ! empty( $block['attrs']['localPickupText'] ) ) {
return $block['attrs']['localPickupText'];
}
}
if ( ! empty( $block['innerBlocks'] ) ) {
$answer = $this->find_local_pickup_text_in_checkout_block( $block['innerBlocks'] );
if ( $answer ) {
return $answer;
}
}
}
}
/**
* Extra data passed through from server to client for block.
*
@@ -252,7 +321,20 @@ class Checkout extends AbstractBlock {
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'countryData', CartCheckoutUtils::get_country_data() );
$country_data = CartCheckoutUtils::get_country_data();
$address_formats = WC()->countries->get_address_formats();
// Move the address format into the 'countryData' setting.
// We need to skip 'default' because that's not a valid country.
foreach ( $address_formats as $country_code => $format ) {
if ( 'default' === $country_code ) {
continue;
}
$country_data[ $country_code ]['format'] = $format;
}
$this->asset_data_registry->add( 'countryData', $country_data );
$this->asset_data_registry->add( 'defaultAddressFormat', $address_formats['default'] );
$this->asset_data_registry->add( 'baseLocation', wc_get_base_location() );
$this->asset_data_registry->add(
'checkoutAllowsGuest',
@@ -279,8 +361,9 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
$pickup_location_settings = get_option( 'woocommerce_pickup_location_settings', [] );
$this->asset_data_registry->add( 'localPickupEnabled', wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' ) );
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings();
$this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] );
$this->asset_data_registry->add( 'localPickupText', $pickup_location_settings['title'] );
$is_block_editor = $this->is_block_editor();
@@ -487,6 +570,7 @@ class Checkout extends AbstractBlock {
'CheckoutOrderSummaryShippingBlock',
'CheckoutOrderSummarySubtotalBlock',
'CheckoutOrderSummaryTaxesBlock',
'CheckoutOrderSummaryTotalsBlock',
'CheckoutPaymentBlock',
'CheckoutShippingAddressBlock',
'CheckoutShippingMethodsBlock',

View File

@@ -11,4 +11,70 @@ class CheckoutOrderSummaryBlock extends AbstractInnerBlock {
* @var string
*/
protected $block_name = 'checkout-order-summary-block';
/**
* Get the contents of the given inner block.
*
* @param string $block_name Name of the order summary inner block.
* @param string $content The content to search.
* @return array|bool
*/
private function get_inner_block_content( $block_name, $content ) {
if ( preg_match( $this->inner_block_regex( $block_name ), $content, $matches ) ) {
return $matches[0];
}
return false;
}
/**
* Get the regex that will return an inner block.
*
* @param string $block_name Name of the order summary inner block.
* @return string Regex pattern.
*/
private function inner_block_regex( $block_name ) {
return '/<div data-block-name="woocommerce\/checkout-order-summary-' . $block_name . '-block"(.+?)>(.*?)<\/div>/si';
}
/**
* Render the Checkout Order Summary block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param object $block Block object.
* @return string Rendered block.
*/
protected function render( $attributes, $content, $block ) {
// The order-summary-totals block was introduced as a new parent block for the totals
// (subtotal, discount, fees, shipping and taxes) blocks.
$regex_for_checkout_order_summary_totals = '/<div data-block-name="woocommerce\/checkout-order-summary-totals-block"(.+?)>/';
$order_summary_totals_content = '<div data-block-name="woocommerce/checkout-order-summary-totals-block" class="wp-block-woocommerce-checkout-order-summary-totals-block">';
// We want to move these blocks inside a parent 'totals' block.
$totals_inner_blocks = array( 'subtotal', 'discount', 'fee', 'shipping', 'taxes' );
if ( preg_match( $regex_for_checkout_order_summary_totals, $content ) ) {
return $content;
}
foreach ( $totals_inner_blocks as $key => $block_name ) {
$inner_block_content = $this->get_inner_block_content( $block_name, $content );
if ( $inner_block_content ) {
$order_summary_totals_content .= "\n" . $inner_block_content;
// The last block is replaced with the totals block.
if ( count( $totals_inner_blocks ) - 1 === $key ) {
$order_summary_totals_content .= '</div>';
$content = preg_replace( $this->inner_block_regex( $block_name ), $order_summary_totals_content, $content );
} else {
// Otherwise, remove the block.
$content = preg_replace( $this->inner_block_regex( $block_name ), '', $content );
}
}
}
// Remove empty lines.
return preg_replace( '/\n\n( *?)/i', '', $content );
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryTotalsBlock class.
*/
class CheckoutOrderSummaryTotalsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-totals-block';
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ComingSoon class.
*/
class ComingSoon extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'coming-soon';
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
}
}

View File

@@ -3,11 +3,14 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
/**
* CustomerAccount class.
*/
class CustomerAccount extends AbstractBlock {
use BlockHooksTrait;
const TEXT_ONLY = 'text_only';
const ICON_ONLY = 'icon_only';
const DISPLAY_ALT = 'alt';
@@ -19,6 +22,85 @@ class CustomerAccount extends AbstractBlock {
*/
protected $block_name = 'customer-account';
/**
* Block Hook API placements.
*
* @var array
*/
protected $hooked_block_placements = array(
array(
'position' => 'after',
'anchor' => 'core/navigation',
'area' => 'header',
'callback' => 'should_unhook_block',
),
);
/**
* Initialize this block type.
*/
protected function initialize() {
parent::initialize();
/**
* The hooked_block_{$hooked_block_type} filter was added in WordPress 6.5.
* We are the only code adding the filter 'hooked_block_woocommerce/customer-account'.
* Using has_filter() for a compatibility check won't work because add_filter() is used in the same file.
*/
if ( version_compare( get_bloginfo( 'version' ), '6.5', '>=' ) ) {
add_filter( 'hooked_block_woocommerce/customer-account', array( $this, 'modify_hooked_block_attributes' ), 10, 5 );
add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 9, 4 );
}
}
/**
* Callback for the Block Hooks API to modify the attributes of the hooked block.
*
* @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
* @param string $hooked_block_type The hooked block type name.
* @param string $relative_position The relative position of the hooked block.
* @param array $parsed_anchor_block The anchor block, in parsed block array format.
* @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type,
* or pattern that the anchor block belongs to.
* @return array|null
*/
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
/*
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.
* We want to match that margin for the Customer Account block so it looks consistent.
*/
$parsed_hooked_block['attrs']['style']['spacing']['margin']['left'] = '0.5em';
return $parsed_hooked_block;
}
/**
* Callback for the Block Hooks API to determine if the block should be auto-inserted.
*
* @param array $hooked_blocks An array of block slugs hooked into a given context.
* @param string $position Position of the block insertion point.
* @param string $anchor_block The block acting as the anchor for the inserted block.
* @param array|\WP_Post|\WP_Block_Template $context Where the block is embedded.
*
* @return array
*/
protected function should_unhook_block( $hooked_blocks, $position, $anchor_block, $context ) {
$block_name = $this->namespace . '/' . $this->block_name;
$block_is_hooked = in_array( $block_name, $hooked_blocks, true );
if ( $block_is_hooked ) {
$active_theme = wp_get_theme()->get( 'Name' );
$exclude_themes = array( 'Twenty Twenty-Two', 'Twenty Twenty-Three' );
if ( in_array( $active_theme, $exclude_themes, true ) ) {
$key = array_search( $block_name, $hooked_blocks, true );
unset( $hooked_blocks[ $key ] );
}
}
return $hooked_blocks;
}
/**
* Render the block.
*
@@ -47,9 +129,11 @@ class CustomerAccount extends AbstractBlock {
),
);
$label_markup = self::ICON_ONLY === $attributes['displayStyle'] ? '' : '<span class="label">' . wp_kses( $this->render_label(), array() ) . '</span>';
return "<div class='wp-block-woocommerce-customer-account " . esc_attr( $classes_and_styles['classes'] ) . "' style='" . esc_attr( $classes_and_styles['styles'] ) . "'>
<a href='" . esc_attr( $account_link ) . "'>
" . wp_kses( $this->render_icon( $attributes ), $allowed_svg ) . "<span class='label'>" . wp_kses( $this->render_label( $attributes ), array() ) . '</span>
" . wp_kses( $this->render_icon( $attributes ), $allowed_svg ) . $label_markup . '
</a>
</div>';
}
@@ -93,15 +177,9 @@ class CustomerAccount extends AbstractBlock {
/**
* Gets the label to render depending on the displayStyle.
*
* @param array $attributes Block attributes.
*
* @return string Label to render on the block.
*/
private function render_label( $attributes ) {
if ( self::ICON_ONLY === $attributes['displayStyle'] ) {
return '';
}
private function render_label() {
return get_current_user_id()
? __( 'My Account', 'woocommerce' )
: __( 'Login', 'woocommerce' );

View File

@@ -22,7 +22,7 @@ class FeaturedProduct extends FeaturedItem {
$id = absint( $attributes['productId'] ?? 0 );
$product = wc_get_product( $id );
if ( ! $product ) {
if ( ! $product || ( 'publish' !== $product->get_status() && ! current_user_can( 'read_product', $id ) ) ) {
return null;
}

View File

@@ -35,8 +35,8 @@ class AdditionalFields extends AbstractOrderConfirmationBlock {
$content .= $this->render_additional_fields(
$controller->filter_fields_for_order_confirmation(
array_merge(
$controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
$controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
$controller->get_order_additional_fields_with_values( $order, 'contact', 'other', 'view' ),
$controller->get_order_additional_fields_with_values( $order, 'order', 'other', 'view' ),
)
)
);

View File

@@ -33,7 +33,7 @@ class AdditionalFieldsWrapper extends AbstractOrderConfirmationBlock {
// Contact and additional fields are currently grouped in this section.
$additional_fields = array_merge(
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'contact' ),
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'additional' )
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'order' )
);
return empty( $additional_fields ) ? '' : $content;
@@ -48,7 +48,7 @@ class AdditionalFieldsWrapper extends AbstractOrderConfirmationBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'additionalFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'additional' ) );
$this->asset_data_registry->add( 'additionalFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'order' ) );
$this->asset_data_registry->add( 'additionalContactFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'contact' ) );
}
}

View File

@@ -1,32 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ProductAddToCart class.
*/
class ProductAddToCart extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-add-to-cart';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Register script and style assets for the block type before it is registered.
*
* This registers the scripts; it does not enqueue them.
*/
protected function register_block_type_assets() {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
}
}

View File

@@ -54,14 +54,14 @@ final class BlockTypesController {
add_action( 'woocommerce_delete_product_transients', array( $this, 'delete_product_transients' ) );
add_filter(
'woocommerce_is_checkout',
function( $return ) {
return $return || $this->has_block_variation( 'woocommerce/classic-shortcode', 'shortcode', 'checkout' );
function ( $ret ) {
return $ret || $this->has_block_variation( 'woocommerce/classic-shortcode', 'shortcode', 'checkout' );
}
);
add_filter(
'woocommerce_is_cart',
function( $return ) {
return $return || $this->has_block_variation( 'woocommerce/classic-shortcode', 'shortcode', 'cart' );
function ( $ret ) {
return $ret || $this->has_block_variation( 'woocommerce/classic-shortcode', 'shortcode', 'cart' );
}
);
}
@@ -225,6 +225,7 @@ final class BlockTypesController {
'CatalogSorting',
'ClassicTemplate',
'ClassicShortcode',
'ComingSoon',
'CustomerAccount',
'FeaturedCategory',
'FeaturedProduct',
@@ -233,7 +234,6 @@ final class BlockTypesController {
'MiniCart',
'StoreNotices',
'PriceFilter',
'ProductAddToCart',
'ProductBestSellers',
'ProductButton',
'ProductCategories',
@@ -352,5 +352,4 @@ final class BlockTypesController {
return $block_types;
}
}

View File

@@ -32,7 +32,7 @@ class CheckoutFieldsAdmin {
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_address_fields' ), 10, 3 );
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_contact_fields' ), 10, 3 );
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_address_fields' ), 10, 3 );
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_additional_fields' ), 10, 3 );
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_order_fields' ), 10, 3 );
}
/**
@@ -73,7 +73,9 @@ class CheckoutFieldsAdmin {
* @param \WC_Order $order The order to update the field for.
*/
public function update_callback( $key, $value, $order ) {
$this->checkout_fields_controller->persist_field_for_order( $key, $value, $order, false );
list( $group, $key ) = explode( '/', $key, 2 );
$group = CheckoutFields::get_group_name( $group );
$this->checkout_fields_controller->persist_field_for_order( $key, $value, $order, $group, false );
}
/**
@@ -89,11 +91,11 @@ class CheckoutFieldsAdmin {
return $fields;
}
$group = doing_action( 'woocommerce_admin_billing_fields' ) ? 'billing' : 'shipping';
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $group, $context );
$group_name = doing_action( 'woocommerce_admin_billing_fields' ) ? 'billing' : 'shipping';
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $group_name, $context );
foreach ( $additional_fields as $key => $field ) {
$group_key = '/' . $group . '/' . $key;
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $group_key );
$prefixed_key = CheckoutFields::get_group_key( $group_name ) . $key;
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $prefixed_key );
}
array_splice(
@@ -123,16 +125,14 @@ class CheckoutFieldsAdmin {
return $fields;
}
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', $context );
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', 'other', $context );
return array_merge(
$fields,
array_map(
array( $this, 'format_field_for_meta_box' ),
$additional_fields,
array_keys( $additional_fields )
)
);
foreach ( $additional_fields as $key => $field ) {
$prefixed_key = CheckoutFields::get_group_key( 'other' ) . $key;
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $prefixed_key );
}
return array_merge( $fields, $additional_fields );
}
/**
@@ -143,20 +143,18 @@ class CheckoutFieldsAdmin {
* @param string $context The context to show the fields for.
* @return array
*/
public function admin_additional_fields( $fields, $order = null, $context = 'edit' ) {
public function admin_order_fields( $fields, $order = null, $context = 'edit' ) {
if ( ! $order instanceof \WC_Order ) {
return $fields;
}
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', $context );
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'order', 'other', $context );
return array_merge(
$fields,
array_map(
array( $this, 'format_field_for_meta_box' ),
$additional_fields,
array_keys( $additional_fields )
)
);
foreach ( $additional_fields as $key => $field ) {
$prefixed_key = CheckoutFields::get_group_key( 'other' ) . $key;
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $prefixed_key );
}
return array_merge( $fields, $additional_fields );
}
}

View File

@@ -32,7 +32,7 @@ class CheckoutFieldsFrontend {
public function init() {
// Show custom checkout fields on the order details page.
add_action( 'woocommerce_order_details_after_customer_address', array( $this, 'render_order_address_fields' ), 10, 2 );
add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_additional_fields' ), 10 );
add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_other_fields' ), 10 );
// Show custom checkout fields on the My Account page.
add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 );
@@ -87,10 +87,10 @@ class CheckoutFieldsFrontend {
*
* @param WC_Order $order Order object.
*/
public function render_order_additional_fields( $order ) {
public function render_order_other_fields( $order ) {
$fields = array_merge(
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', 'other', 'view' ),
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'order', 'other', 'view' ),
);
if ( ! $fields ) {
@@ -122,7 +122,7 @@ class CheckoutFieldsFrontend {
foreach ( $fields as $key => $field ) {
$value = $this->checkout_fields_controller->format_additional_field_value(
$this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type ),
$this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type ),
$field
);
@@ -160,8 +160,10 @@ class CheckoutFieldsFrontend {
$fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
foreach ( $fields as $key => $field ) {
$field_key = CheckoutFields::get_group_key( 'other' ) . $key;
$form_field = $field;
$form_field['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, 'contact' );
$form_field['id'] = $field_key;
$form_field['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, 'contact' );
if ( 'select' === $field['type'] ) {
$form_field['options'] = array_column( $field['options'], 'label', 'value' );
@@ -189,12 +191,13 @@ class CheckoutFieldsFrontend {
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
$field_values = array();
foreach ( $additional_fields as $key => $field ) {
if ( ! isset( $_POST[ $key ] ) ) {
foreach ( array_keys( $additional_fields ) as $key ) {
$post_key = CheckoutFields::get_group_key( 'other' ) . $key;
if ( ! isset( $_POST[ $post_key ] ) ) {
continue;
}
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $key ] ) ) );
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $post_key ] ) ) );
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
@@ -207,11 +210,11 @@ class CheckoutFieldsFrontend {
// Persist individual additional fields to customer.
foreach ( $field_values as $key => $value ) {
$this->checkout_fields_controller->persist_field_for_customer( $key, $value, $customer );
$this->checkout_fields_controller->persist_field_for_customer( $key, $value, $customer, 'other' );
}
// Validate all fields for this location.
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( $field_values, 'contact' );
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( $field_values, 'contact', 'other' );
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
wc_add_notice( $location_validation->get_error_message(), 'error' );
@@ -233,9 +236,9 @@ class CheckoutFieldsFrontend {
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
foreach ( $fields as $key => $field ) {
$field_key = "/{$address_type}/{$key}";
$field_key = CheckoutFields::get_group_key( $address_type ) . $key;
$address[ $field_key ] = $field;
$address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type );
$address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type );
if ( 'select' === $field['type'] ) {
$address[ $field_key ]['options'] = array_column( $field['options'], 'label', 'value' );
@@ -266,8 +269,8 @@ class CheckoutFieldsFrontend {
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
$field_values = array();
foreach ( $additional_fields as $key => $field ) {
$post_key = "/{$address_type}/{$key}";
foreach ( array_keys( $additional_fields ) as $key ) {
$post_key = CheckoutFields::get_group_key( $address_type ) . $key;
if ( ! isset( $_POST[ $post_key ] ) ) {
continue;
@@ -286,7 +289,7 @@ class CheckoutFieldsFrontend {
// Persist individual additional fields to customer.
foreach ( $field_values as $key => $value ) {
$this->checkout_fields_controller->persist_field_for_customer( "/{$address_type}/{$key}", $value, $customer );
$this->checkout_fields_controller->persist_field_for_customer( $key, $value, $customer, $address_type );
}
// Validate all fields for this location.

View File

@@ -2,6 +2,9 @@
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\StoreApi\RoutesController;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\StoreApi;
/**
* Service class that handles hydration of API data for blocks.
@@ -19,7 +22,7 @@ class Hydration {
*
* @var array
*/
protected $cached_store_notices = [];
protected $cached_store_notices = array();
/**
* Constructor.
@@ -38,24 +41,162 @@ class Hydration {
* @return array Response data.
*/
public function get_rest_api_response_data( $path = '' ) {
$this->cache_store_notices();
if ( ! str_starts_with( $path, '/wc/store' ) ) {
return array();
}
// Allow-list only store API routes. No other request can be hydrated for safety.
$available_routes = StoreApi::container()->get( RoutesController::class )->get_all_routes( 'v1', true );
$controller_class = $this->match_route_to_handler( $path, $available_routes );
/**
* We disable nonce check to support endpoints such as checkout. The caveat here is that we need to be careful to only support GET requests. No other request type should be processed without nonce check. Additionally, no GET request can modify data as part of hydration request, for example adding items to cart.
*
* Long term, we should consider validating nonce here, instead of disabling it temporarily.
*/
$this->disable_nonce_check();
// Preload the request and add it to the array. It will be $preloaded_requests['path'] and contain 'body' and 'headers'.
$preloaded_requests = rest_preload_api_request( [], $path );
$this->cache_store_notices();
$preloaded_data = array();
if ( null !== $controller_class ) {
try {
$response = $this->get_response_from_controller( $controller_class, $path );
if ( $response ) {
$preloaded_data = array(
'body' => $response->get_data(),
'headers' => $response->get_headers(),
);
}
} catch ( \Exception $e ) {
// This is executing in frontend of the site, a failure in hydration should not stop the site from working.
wc_get_logger()->warning(
'Error in hydrating REST API request: ' . $e->getMessage(),
array(
'source' => 'blocks-hydration',
'data' => array(
'path' => $path,
'controller' => $controller_class,
),
'backtrace' => true,
)
);
}
} else {
// Preload the request and add it to the array. It will be $preloaded_requests['path'] and contain 'body' and 'headers'.
$preloaded_requests = rest_preload_api_request( array(), $path );
$preloaded_data = $preloaded_requests[ $path ] ?? array();
}
$this->restore_cached_store_notices();
$this->restore_nonce_check();
// Returns just the single preloaded request, or an empty array if it doesn't exist.
return $preloaded_requests[ $path ] ?? [];
return $preloaded_data;
}
/**
* Helper method to generate GET response from a controller. Also fires the `rest_request_after_callbacks` for backward compatibility.
*
* @param string $controller_class Controller class FQN that will respond to the request.
* @param string $path Request path regex.
*
* @return false|mixed|null Response
*/
private function get_response_from_controller( $controller_class, $path ) {
if ( null === $controller_class ) {
return false;
}
$request = new \WP_REST_Request( 'GET', $path );
$schema_controller = StoreApi::container()->get( SchemaController::class );
$controller = new $controller_class(
$schema_controller,
$schema_controller->get( $controller_class::SCHEMA_TYPE, $controller_class::SCHEMA_VERSION )
);
$controller_args = is_callable( array( $controller, 'get_args' ) ) ? $controller->get_args() : array();
if ( empty( $controller_args ) ) {
return false;
}
// Get the handler that responds to read request.
$handler = current(
array_filter(
$controller_args,
function ( $method_handler ) {
return is_array( $method_handler ) && isset( $method_handler['methods'] ) && \WP_REST_Server::READABLE === $method_handler['methods'];
}
)
);
if ( ! $handler ) {
return false;
}
/**
* Similar to WP core's `rest_dispatch_request` filter, this allows plugin to override hydrating the request.
* Allows backward compatibility with the `rest_dispatch_request` filter by providing the same arguments.
*
* @since 8.9.0
*
* @param mixed $hydration_result Result of the hydration. If not null, this will be used as the response.
* @param WP_REST_Request $request Request used to generate the response.
* @param string $path Request path matched for the request..
* @param array $handler Route handler used for the request.
*/
$hydration_result = apply_filters( 'woocommerce_hydration_dispatch_request', null, $request, $path, $handler );
if ( null !== $hydration_result ) {
$response = $hydration_result;
} else {
$response = call_user_func_array( $handler['callback'], array( $request ) );
}
/**
* Similar to WP core's `rest_request_after_callbacks` filter, this allows to modify the response after it has been generated.
* Allows backward compatibility with the `rest_request_after_callbacks` filter by providing the same arguments.
*
* @since 8.9.0
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
$response = apply_filters( 'woocommerce_hydration_request_after_callbacks', $response, $handler, $request );
return $response;
}
/**
* Inspired from WP core's `match_request_to_handler`, this matches a given path from available route regexes.
* However, unlike WP core, this does not check against query params, request method etc.
*
* @param string $path The path to match.
* @param array $available_routes Available routes in { $regex1 => $contoller_class1, ... } format.
*
* @return string|null
*/
private function match_route_to_handler( $path, $available_routes ) {
$matched_route = null;
foreach ( $available_routes as $route_path => $controller ) {
$match = preg_match( '@^' . $route_path . '$@i', $path );
if ( $match ) {
$matched_route = $controller;
break;
}
}
return $matched_route;
}
/**
* Disable the nonce check temporarily.
*/
protected function disable_nonce_check() {
add_filter( 'woocommerce_store_api_disable_nonce_check', [ $this, 'disable_nonce_check_callback' ] );
add_filter( 'woocommerce_store_api_disable_nonce_check', array( $this, 'disable_nonce_check_callback' ) );
}
/**
@@ -70,7 +211,7 @@ class Hydration {
* Restore the nonce check.
*/
protected function restore_nonce_check() {
remove_filter( 'woocommerce_store_api_disable_nonce_check', [ $this, 'disable_nonce_check_callback' ] );
remove_filter( 'woocommerce_store_api_disable_nonce_check', array( $this, 'disable_nonce_check_callback' ) );
}
/**
@@ -92,6 +233,6 @@ class Hydration {
return;
}
WC()->session->set( 'wc_notices', $this->cached_store_notices );
$this->cached_store_notices = [];
$this->cached_store_notices = array();
}
}

View File

@@ -3,14 +3,14 @@
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_field' ) ) {
if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) {
/**
* Register a checkout field.
*
* @param array $options Field arguments. See CheckoutFields::register_checkout_field() for details.
* @throws \Exception If field registration fails.
*/
function __experimental_woocommerce_blocks_register_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
function woocommerce_register_additional_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
// Check if `woocommerce_blocks_loaded` ran. If not then the CheckoutFields class will not be available yet.
// In that case, re-hook `woocommerce_blocks_loaded` and try running this again.
@@ -19,7 +19,7 @@ if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_fie
add_action(
'woocommerce_blocks_loaded',
function () use ( $options ) {
__experimental_woocommerce_blocks_register_checkout_field( $options );
woocommerce_register_additional_checkout_field( $options );
}
);
return;
@@ -32,6 +32,20 @@ if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_fie
}
}
if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_field' ) ) {
/**
* Register a checkout field.
*
* @param array $options Field arguments. See CheckoutFields::register_checkout_field() for details.
* @throws \Exception If field registration fails.
* @deprecated 5.6.0 Use woocommerce_register_additional_checkout_field() instead.
*/
function __experimental_woocommerce_blocks_register_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
wc_deprecated_function( __FUNCTION__, '8.9.0', 'woocommerce_register_additional_checkout_field' );
woocommerce_register_additional_checkout_field( $options );
}
}
if ( ! function_exists( '__internal_woocommerce_blocks_deregister_checkout_field' ) ) {
/**

View File

@@ -55,7 +55,7 @@ class ShippingController {
if ( is_admin() ) {
$this->asset_data_registry->add(
'countryStates',
function() {
function () {
return WC()->countries->get_states();
}
);
@@ -63,9 +63,9 @@ class ShippingController {
$this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ) );
$this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' );
add_action( 'rest_api_init', [ $this, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'hydrate_client_settings' ] );
add_action( 'rest_api_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'hydrate_client_settings' ) );
add_action( 'woocommerce_load_shipping_methods', array( $this, 'register_local_pickup' ) );
add_filter( 'woocommerce_local_pickup_methods', array( $this, 'register_local_pickup_method' ) );
add_filter( 'woocommerce_order_hide_shipping_address', array( $this, 'hide_shipping_address_for_local_pickup' ), 10 );
@@ -74,7 +74,6 @@ class ShippingController {
add_filter( 'pre_update_option_woocommerce_pickup_location_settings', array( $this, 'flush_cache' ) );
add_filter( 'pre_update_option_pickup_location_pickup_locations', array( $this, 'flush_cache' ) );
add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
add_filter( 'wc_shipping_enabled', array( $this, 'force_shipping_enabled' ), 100, 1 );
add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );
// This is required to short circuit `show_shipping` from class-wc-cart.php - without it, that function
@@ -98,30 +97,17 @@ class ShippingController {
return $value;
}
/**
* Force shipping to be enabled if the Checkout block is in use on the Checkout page.
*
* @param boolean $enabled Whether shipping is currently enabled.
* @return boolean Whether shipping should continue to be enabled/disabled.
*/
public function force_shipping_enabled( $enabled ) {
if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
return true;
}
return $enabled;
}
/**
* Inject collection details onto the order received page.
*
* @param string $return Return value.
* @param string $return_value Return value.
* @param \WC_Order $order Order object.
* @return string
*/
public function show_local_pickup_details( $return, $order ) {
public function show_local_pickup_details( $return_value, $order ) {
// Confirm order is valid before proceeding further.
if ( ! $order instanceof \WC_Order ) {
return $return;
return $return_value;
}
$shipping_method_ids = ArrayUtil::select( $order->get_shipping_methods(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
@@ -129,7 +115,7 @@ class ShippingController {
// Ensure order used pickup location method, otherwise bail.
if ( 'pickup_location' !== $shipping_method_id ) {
return $return;
return $return_value;
}
$shipping_method = current( $order->get_shipping_methods() );
@@ -138,7 +124,7 @@ class ShippingController {
$address = $shipping_method->get_meta( 'pickup_address' );
if ( ! $address ) {
return $return;
return $return_value;
}
return sprintf(
@@ -160,7 +146,7 @@ class ShippingController {
if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
$settings[ $index ]['desc'] = sprintf(
/* translators: %s: URL to the documentation. */
__( 'Not available when using the <a href="%s">Local pickup options powered by the Checkout block</a>.', 'woocommerce' ),
__( 'Hide shipping costs until an address is entered (Not available when using the <a href="%s">Local pickup options powered by the Checkout block</a>)', 'woocommerce' ),
'https://woocommerce.com/document/woocommerce-blocks-local-pickup/'
);
$settings[ $index ]['disabled'] = true;
@@ -180,86 +166,86 @@ class ShippingController {
register_setting(
'options',
'woocommerce_pickup_location_settings',
[
array(
'type' => 'object',
'description' => 'WooCommerce Local Pickup Method Settings',
'default' => [],
'show_in_rest' => [
'default' => array(),
'show_in_rest' => array(
'name' => 'pickup_location_settings',
'schema' => [
'schema' => array(
'type' => 'object',
'properties' => array(
'enabled' => [
'enabled' => array(
'description' => __( 'If enabled, this method will appear on the block based checkout.', 'woocommerce' ),
'type' => 'string',
'enum' => [ 'yes', 'no' ],
],
'title' => [
'enum' => array( 'yes', 'no' ),
),
'title' => array(
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
'type' => 'string',
],
'tax_status' => [
),
'tax_status' => array(
'description' => __( 'If a cost is defined, this controls if taxes are applied to that cost.', 'woocommerce' ),
'type' => 'string',
'enum' => [ 'taxable', 'none' ],
],
'cost' => [
'enum' => array( 'taxable', 'none' ),
),
'cost' => array(
'description' => __( 'Optional cost to charge for local pickup.', 'woocommerce' ),
'type' => 'string',
],
),
),
],
],
]
),
),
)
);
register_setting(
'options',
'pickup_location_pickup_locations',
[
array(
'type' => 'array',
'description' => 'WooCommerce Local Pickup Locations',
'default' => [],
'show_in_rest' => [
'default' => array(),
'show_in_rest' => array(
'name' => 'pickup_locations',
'schema' => [
'schema' => array(
'type' => 'array',
'items' => [
'items' => array(
'type' => 'object',
'properties' => array(
'name' => [
'name' => array(
'type' => 'string',
],
'address' => [
),
'address' => array(
'type' => 'object',
'properties' => array(
'address_1' => [
'address_1' => array(
'type' => 'string',
],
'city' => [
),
'city' => array(
'type' => 'string',
],
'state' => [
),
'state' => array(
'type' => 'string',
],
'postcode' => [
),
'postcode' => array(
'type' => 'string',
],
'country' => [
),
'country' => array(
'type' => 'string',
],
),
),
],
'details' => [
),
'details' => array(
'type' => 'string',
],
'enabled' => [
),
'enabled' => array(
'type' => 'boolean',
],
),
),
],
],
],
]
),
),
),
)
);
}
@@ -267,16 +253,16 @@ class ShippingController {
* Hydrate client settings
*/
public function hydrate_client_settings() {
$locations = get_option( 'pickup_location_pickup_locations', [] );
$locations = get_option( 'pickup_location_pickup_locations', array() );
$formatted_pickup_locations = [];
$formatted_pickup_locations = array();
foreach ( $locations as $location ) {
$formatted_pickup_locations[] = [
$formatted_pickup_locations[] = array(
'name' => $location['name'],
'address' => $location['address'],
'details' => $location['details'],
'enabled' => wc_string_to_bool( $location['enabled'] ),
];
);
}
$has_legacy_pickup = false;
@@ -306,7 +292,7 @@ class ShippingController {
}
$settings = array(
'pickupLocationSettings' => get_option( 'woocommerce_pickup_location_settings', [] ),
'pickupLocationSettings' => LocalPickupUtils::get_local_pickup_settings(),
'pickupLocations' => $formatted_pickup_locations,
'readonlySettings' => array(
'hasLegacyPickup' => $has_legacy_pickup,
@@ -328,7 +314,7 @@ class ShippingController {
* Load admin scripts.
*/
public function admin_scripts() {
$this->asset_api->register_script( 'wc-shipping-method-pickup-location', 'assets/client/blocks/wc-shipping-method-pickup-location.js', [], true );
$this->asset_api->register_script( 'wc-shipping-method-pickup-location', 'assets/client/blocks/wc-shipping-method-pickup-location.js', array(), true );
}
/**
@@ -390,8 +376,8 @@ class ShippingController {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
if ( $chosen_method_id && true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && in_array( $chosen_method_id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
$pickup_locations = get_option( 'pickup_location_pickup_locations', [] );
$pickup_location = $pickup_locations[ $chosen_method_instance ] ?? [];
$pickup_locations = get_option( 'pickup_location_pickup_locations', array() );
$pickup_location = $pickup_locations[ $chosen_method_instance ] ?? array();
if ( isset( $pickup_location['address'], $pickup_location['address']['country'] ) && ! empty( $pickup_location['address']['country'] ) ) {
$address = array(
@@ -420,8 +406,8 @@ class ShippingController {
// Check all packages for an instance of a collectable shipping method.
$valid_packages = array_filter(
$packages,
function( $package ) {
$shipping_method_ids = ArrayUtil::select( $package['rates'] ?? [], 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
function ( $package ) {
$shipping_method_ids = ArrayUtil::select( $package['rates'] ?? array(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
return ! empty( array_intersect( LocalPickupUtils::get_local_pickup_method_ids(), $shipping_method_ids ) );
}
);
@@ -429,14 +415,14 @@ class ShippingController {
// Remove pickup location from rates arrays.
if ( count( $valid_packages ) !== count( $packages ) ) {
$packages = array_map(
function( $package ) {
function ( $package ) {
if ( ! is_array( $package['rates'] ) ) {
$package['rates'] = [];
$package['rates'] = array();
return $package;
}
$package['rates'] = array_filter(
$package['rates'],
function( $rate ) {
function ( $rate ) {
return ! in_array( $rate->get_method_id(), LocalPickupUtils::get_local_pickup_method_ids(), true );
}
);
@@ -481,7 +467,7 @@ class ShippingController {
'pickup_locations_enabled' => count(
array_filter(
$locations,
function( $location ) {
function ( $location ) {
return $location['enabled']; }
)
),

View File

@@ -78,7 +78,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
$block_hooks = array_filter(
$this->hook_data,
function( $hook ) use ( $block_name ) {
function ( $hook ) use ( $block_name ) {
return in_array( $block_name, $hook['block_names'], true );
}
);
@@ -98,9 +98,9 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
$supported_blocks = array_merge(
[],
array(),
...array_map(
function( $hook ) {
function ( $hook ) {
return $hook['block_names'];
},
array_values( $this->hook_data )
@@ -312,7 +312,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
continue;
}
foreach ( $data['hooked'] as $callback => $priority ) {
if ( ! in_array( $callback, $data['permanently_removed_actions'] ?? [], true ) ) {
if ( ! in_array( $callback, $data['permanently_removed_actions'] ?? array(), true ) ) {
add_action( $hook, $callback, $priority );
}
}

View File

@@ -1,8 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* CartTemplate class.
*
@@ -33,7 +31,6 @@ class CartTemplate extends AbstractPageTemplate {
*/
public function get_template_title() {
return _x( 'Page: Cart', 'Template name', 'woocommerce' );
}
/**
@@ -43,7 +40,6 @@ class CartTemplate extends AbstractPageTemplate {
*/
public function get_template_description() {
return __( 'The Cart template displays the items selected by the user for purchase, including quantities, prices, and discounts. It allows users to review their choices before proceeding to checkout.', 'woocommerce' );
}
/**

View File

@@ -1,8 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* CheckoutTemplate class.
*

View File

@@ -0,0 +1,53 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
/**
* ComingSoonTemplate class.
*
* @internal
*/
class ComingSoonTemplate extends AbstractPageTemplate {
/**
* The slug of the template.
*
* @var string
*/
const SLUG = 'coming-soon';
/**
* Returns the title of the template.
*
* @return string
*/
public function get_template_title() {
return _x( 'Page: Coming soon', 'Template name', 'woocommerce' );
}
/**
* Returns the description of the template.
*
* @return string
*/
public function get_template_description() {
return __( 'Page template for Coming soon page.', 'woocommerce' );
}
/**
* Returns the page object assigned to this template/page.
*
* @return \WP_Post|null Post object or null.
*/
protected function get_placeholder_page() {
return null;
}
/**
* True when viewing the coming soon page.
*
* @return boolean
*/
protected function is_active_template() {
return false;
}
}

View File

@@ -54,13 +54,13 @@ class MiniCartTemplate extends AbstractTemplatePart {
* @return array The supported template part areas including the Mini-Cart one.
*/
public function register_mini_cart_template_part_area( $default_area_definitions ) {
$mini_cart_template_part_area = [
$mini_cart_template_part_area = array(
'area' => 'mini-cart',
'label' => __( 'Mini-Cart', 'woocommerce' ),
'description' => __( 'The Mini-Cart template allows shoppers to see their cart items and provides access to the Cart and Checkout pages.', 'woocommerce' ),
'icon' => 'mini-cart',
'area_tag' => 'mini-cart',
];
return array_merge( $default_area_definitions, [ $mini_cart_template_part_area ] );
);
return array_merge( $default_area_definitions, array( $mini_cart_template_part_area ) );
}
}

View File

@@ -23,6 +23,7 @@ class ProductCatalogTemplate extends AbstractTemplate {
*/
public function init() {
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
}
/**
@@ -57,4 +58,30 @@ class ProductCatalogTemplate extends AbstractTemplate {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
}
}
/**
* Remove the template panel from the Sidebar of the Shop page because
* the Site Editor handles it.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/6278
*
* @param bool $is_support Whether the active theme supports block templates.
*
* @return bool
*/
public function remove_block_template_support_for_shop_page( $is_support ) {
global $pagenow, $post;
if (
is_admin() &&
'post.php' === $pagenow &&
function_exists( 'wc_get_page_id' ) &&
is_a( $post, 'WP_Post' ) &&
wc_get_page_id( 'shop' ) === $post->ID
) {
return false;
}
return $is_support;
}
}

View File

@@ -5,7 +5,7 @@ use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* SingleProductTemplae class.
* SingleProductTemplate class.
*
* @internal
*/
@@ -51,7 +51,7 @@ class SingleProductTemplate extends AbstractTemplate {
if ( ! is_embed() && is_singular( 'product' ) ) {
global $post;
$valid_slugs = [ self::SLUG ];
$valid_slugs = array( self::SLUG );
if ( 'product' === $post->post_type && $post->post_name ) {
$valid_slugs[] = 'single-product-' . $post->post_name;
}
@@ -75,7 +75,7 @@ class SingleProductTemplate extends AbstractTemplate {
*/
public function update_single_product_content( $query_result, $query, $template_type ) {
$query_result = array_map(
function( $template ) {
function ( $template ) {
if ( str_contains( $template->slug, self::SLUG ) ) {
// We don't want to add the compatibility layer on the Editor Side.
// The second condition is necessary to not apply the compatibility layer on the REST API. Gutenberg uses the REST API to clone the template.
@@ -84,7 +84,7 @@ class SingleProductTemplate extends AbstractTemplate {
// Add the product class to the body. We should move this to a more appropriate place.
add_filter(
'body_class',
function( $classes ) {
function ( $classes ) {
return array_merge( $classes, wc_get_product_class() );
}
);
@@ -125,7 +125,7 @@ class SingleProductTemplate extends AbstractTemplate {
$single_product_template_blocks = array( 'woocommerce/product-image-gallery', 'woocommerce/product-details', 'woocommerce/add-to-cart-form', 'woocommerce/product-meta', 'woocommerce/product-rating', 'woocommerce/product-price', 'woocommerce/related-products' );
return array_reduce(
$parsed_blocks,
function( $carry, $block ) use ( $single_product_template_blocks ) {
function ( $carry, $block ) use ( $single_product_template_blocks ) {
if ( in_array( $block['blockName'], $single_product_template_blocks, true ) ) {
if ( $carry['is_already_replaced'] ) {
return array(

View File

@@ -31,7 +31,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
$block_hooks = array_filter(
$this->hook_data,
function( $hook ) use ( $block_name ) {
function ( $hook ) use ( $block_name ) {
return in_array( $block_name, $hook['block_names'], true );
}
);
@@ -279,7 +279,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
$grouped_blocks = self::group_blocks( $parsed_blocks );
$wrapped_blocks = array_map(
function( $blocks ) {
function ( $blocks ) {
if ( 'core/template-part' === $blocks[0]['blockName'] ) {
return $blocks;
}
@@ -306,7 +306,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
private static function inject_custom_attributes_to_first_and_last_block_single_product_template( $wrapped_blocks ) {
$template_with_custom_attributes = array_reduce(
$wrapped_blocks,
function( $carry, $item ) {
function ( $carry, $item ) {
$index = $carry['index'];
$carry['index'] = $carry['index'] + 1;
@@ -379,7 +379,6 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
$new_block['innerBlocks'] = $blocks;
return $new_block;
}
/**
@@ -421,7 +420,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
private static function group_blocks( $parsed_blocks ) {
return array_reduce(
$parsed_blocks,
function( array $carry, array $block ) {
function ( array $carry, array $block ) {
if ( 'core/template-part' === $block['blockName'] ) {
$carry[] = array( $block );
return $carry;
@@ -480,7 +479,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
private static function serialize_blocks( $parsed_blocks ) {
return array_reduce(
$parsed_blocks,
function( $carry, $item ) {
function ( $carry, $item ) {
if ( is_array( $item ) ) {
return $carry . serialize_blocks( $item );
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\Utils;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Options;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry;
@@ -292,18 +293,45 @@ class BlockTemplateUtils {
/**
* Finds all nested template part file paths in a theme's directory.
*
* @param string $base_directory The theme's file path.
* @param string $template_type wp_template or wp_template_part.
* @return array $path_list A list of paths to all template part files.
*/
public static function get_template_paths( $base_directory ) {
$path_list = array();
if ( file_exists( $base_directory ) ) {
$nested_files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $base_directory ) );
$nested_html_files = new \RegexIterator( $nested_files, '/^.+\.html$/i', \RecursiveRegexIterator::GET_MATCH );
foreach ( $nested_html_files as $path => $file ) {
$path_list[] = $path;
}
public static function get_template_paths( $template_type ) {
$wp_template_filenames = array(
'archive-product.html',
'order-confirmation.html',
'page-cart.html',
'page-checkout.html',
'product-search-results.html',
'single-product.html',
'taxonomy-product_attribute.html',
'taxonomy-product_cat.html',
'taxonomy-product_tag.html',
);
if ( Features::is_enabled( 'launch-your-store' ) ) {
$wp_template_filenames[] = 'coming-soon.html';
}
$wp_template_part_filenames = array(
'checkout-header.html',
'mini-cart.html',
);
/*
* This may return the blockified directory for wp_templates.
* At the moment every template file has a corresponding blockified file.
* If we decide to add a new template file that doesn't, we will need to update this logic.
*/
$directory = self::get_templates_directory( $template_type );
$path_list = array_map(
function ( $filename ) use ( $directory ) {
return $directory . DIRECTORY_SEPARATOR . $filename;
},
'wp_template' === $template_type ? $wp_template_filenames : $wp_template_part_filenames
);
return $path_list;
}
@@ -678,16 +706,6 @@ class BlockTemplateUtils {
return wc_string_to_bool( $use_blockified_templates );
}
/**
* Returns whether the passed `$template` has a title, and it's different from the slug.
*
* @param object $template The template object.
* @return boolean
*/
public static function template_has_title( $template ) {
return ! empty( $template->title ) && $template->title !== $template->slug;
}
/**
* Returns whether the passed `$template` has the legacy template block.
*
@@ -698,6 +716,37 @@ class BlockTemplateUtils {
return has_block( 'woocommerce/legacy-template', $template->content );
}
/**
* Updates the title, description and area of a template to the correct values and to make them more user-friendly.
* For example, instead of:
* - Title: `Tag (product_tag)`
* - Description: `Displays taxonomy: Tag.`
* we display:
* - Title: `Products by Tag`
* - Description: `Displays products filtered by a tag.`.
*
* @param WP_Block_Template $template The template object.
* @param string $template_type wp_template or wp_template_part.
*
* @return WP_Block_Template
*/
public static function update_template_data( $template, $template_type ) {
if ( ! $template ) {
return $template;
}
if ( empty( $template->title ) || $template->title === $template->slug ) {
$template->title = self::get_block_template_title( $template->slug );
}
if ( empty( $template->description ) ) {
$template->description = self::get_block_template_description( $template->slug );
}
if ( empty( $template->area ) || 'uncategorized' === $template->area ) {
$template->area = self::get_block_template_area( $template->slug, $template_type );
}
return $template;
}
/**
* Gets the templates saved in the database.
*

View File

@@ -30,6 +30,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\Restoc
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\UtilsClassesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchProcessingServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ComingSoonServiceProvider;
/**
* PSR11 compliant dependency injection container for WooCommerce.
@@ -79,6 +80,7 @@ final class Container {
LayoutTemplatesServiceProvider::class,
LoggingServiceProvider::class,
EnginesServiceProvider::class,
ComingSoonServiceProvider::class,
);
/**

View File

@@ -58,16 +58,26 @@ class CLIRunner {
* Registers commands for CLI.
*/
public function register_commands() {
WP_CLI::add_command( 'wc cot count_unmigrated', array( $this, 'count_unmigrated' ) );
WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) );
WP_CLI::add_command( 'wc cot sync', array( $this, 'sync' ) );
WP_CLI::add_command( 'wc cot verify_cot_data', array( $this, 'verify_cot_data' ) );
WP_CLI::add_command( 'wc cot enable', array( $this, 'enable' ) );
WP_CLI::add_command( 'wc cot disable', array( $this, 'disable' ) );
$legacy_commands = array( 'count_unmigrated', 'sync', 'verify_cot_data', 'enable', 'disable' );
foreach ( $legacy_commands as $cmd ) {
$new_cmd_name = 'verify_cot_data' === $cmd ? 'verify_data' : $cmd;
WP_CLI::add_command( "wc hpos {$new_cmd_name}", array( $this, $cmd ) );
WP_CLI::add_command(
"wc cot {$cmd}",
function ( array $args = array(), array $assoc_args = array() ) use ( $cmd, $new_cmd_name ) {
WP_CLI::warning( "Command `wc cot {$cmd}` is deprecated since 8.9.0. Please use `wc hpos {$new_cmd_name}` instead." );
return call_user_func( array( $this, $cmd ), $args, $assoc_args );
}
);
}
WP_CLI::add_command( 'wc hpos cleanup', array( $this, 'cleanup_post_data' ) );
WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
WP_CLI::add_command( 'wc hpos backfill', array( $this, 'backfill' ) );
WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) ); // Fully deprecated. No longer works.
}
/**
@@ -77,7 +87,7 @@ class CLIRunner {
*
* @return bool Whether the COT feature is enabled.
*/
private function is_enabled( $log = true ) : bool {
private function is_enabled( $log = true ): bool {
if ( ! $this->controller->custom_orders_table_usage_is_enabled() ) {
if ( $log ) {
WP_CLI::log(
@@ -98,7 +108,7 @@ class CLIRunner {
*
* ## EXAMPLES
*
* wp wc cot count_unmigrated
* wp wc hpos count_unmigrated
*
* @param array $args Positional arguments passed to the command.
*
@@ -106,7 +116,7 @@ class CLIRunner {
*
* @return int The number of orders to be migrated.*
*/
public function count_unmigrated( $args = array(), $assoc_args = array() ) : int {
public function count_unmigrated( $args = array(), $assoc_args = array() ): int {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$order_count = $this->synchronizer->get_current_orders_pending_sync_count();
@@ -147,7 +157,7 @@ class CLIRunner {
*
* ## EXAMPLES
*
* wp wc cot sync --batch-size=500
* wp wc hpos sync --batch-size=500
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
@@ -212,7 +222,7 @@ class CLIRunner {
)
);
$batch_count ++;
++$batch_count;
$total_time += $batch_total_time;
$progress->tick();
@@ -252,7 +262,7 @@ class CLIRunner {
}
/**
* [Deprecated] Use `wp wc cot sync` instead.
* [Deprecated] Use `wp wc hpos sync` instead.
* Copy order data into the postmeta table.
*
* Note that this could dramatically increase the size of your postmeta table, but is recommended
@@ -274,7 +284,7 @@ class CLIRunner {
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
*/
public function migrate( $args = array(), $assoc_args = array() ) {
public function migrate( array $args = array(), array $assoc_args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- for backwards compat.
WP_CLI::log( __( 'Migrate command is deprecated. Please use `sync` instead.', 'woocommerce' ) );
}
@@ -319,7 +329,7 @@ class CLIRunner {
* ## EXAMPLES
*
* # Verify migrated order data, 500 orders at a time.
* wp wc cot verify_cot_data --batch-size=500 --start-from=0 --end-at=10000
* wp wc hpos verify_cot_data --batch-size=500 --start-from=0 --end-at=10000
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
@@ -414,7 +424,7 @@ class CLIRunner {
$error_processing = $error_processing || ! empty( $failed_ids_in_current_batch );
$processed += count( $order_ids );
$batch_total_time = microtime( true ) - $batch_start_time;
$batch_count ++;
++$batch_count;
$total_time += $batch_total_time;
if ( count( $failed_ids_in_current_batch ) > 0 ) {
@@ -467,7 +477,7 @@ class CLIRunner {
} else {
array_walk(
$errors_in_remigrate_batch,
function( &$errors_for_order ) {
function ( &$errors_for_order ) {
$errors_for_order[] = array( 'remigrate_failed' => true );
}
);
@@ -559,7 +569,7 @@ class CLIRunner {
*
* @return int Order count.
*/
private function get_verify_order_count( int $order_id_start, int $order_id_end, array $order_types, bool $log = true ) : int {
private function get_verify_order_count( int $order_id_start, int $order_id_end, array $order_types, bool $log = true ): int {
global $wpdb;
$order_types_placeholder = implode( ',', array_fill( 0, count( $order_types ), '%s' ) );
@@ -605,7 +615,7 @@ class CLIRunner {
*
* @return array Failed IDs with meta details.
*/
private function verify_meta_data( array $order_ids, array $failed_ids ) : array {
private function verify_meta_data( array $order_ids, array $failed_ids ): array {
$meta_keys_to_ignore = $this->synchronizer->get_ignored_order_props();
global $wpdb;
@@ -685,7 +695,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
*
* @return array Normalized data.
*/
private function normalize_raw_meta_data( array $data ) : array {
private function normalize_raw_meta_data( array $data ): array {
$clubbed_data = array();
foreach ( $data as $row ) {
if ( ! isset( $clubbed_data[ $row['entity_id'] ] ) ) {
@@ -719,7 +729,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
* ### EXAMPLES
*
* # Enable HPOS on new shops.
* wp wc cot enable --for-new-shop
* wp wc hpos enable --for-new-shop
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
@@ -778,7 +788,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
sprintf(
// translators: %s is the command to run (wp wc cot sync).
__( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
'wp wc cot sync',
'wp wc hpos sync',
)
);
$enable_hpos = false;
@@ -826,7 +836,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
* ### EXAMPLES
*
* # Disable HPOS.
* wp wc cot disable
* wp wc hpos disable
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
@@ -849,7 +859,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
sprintf(
// translators: %s is the command to run (wp wc cot sync).
__( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
'wp wc cot sync',
'wp wc hpos sync',
)
);
}
@@ -938,37 +948,63 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
return;
}
$progress = WP_CLI\Utils\make_progress_bar( __( 'HPOS cleanup', 'woocommerce' ), $order_count );
$count = 0;
$progress = WP_CLI\Utils\make_progress_bar( __( 'HPOS cleanup', 'woocommerce' ), $order_count );
$count = 0;
$failed_ids = array();
// translators: %d is the number of orders to clean up.
WP_CLI::log( sprintf( _n( 'Starting cleanup for %d order...', 'Starting cleanup for %d orders...', $order_count, 'woocommerce' ), $order_count ) );
do {
$order_ids = $handler->get_orders_for_cleanup( $q_order_ids, $q_limit );
$failed_ids_in_batch = array();
$order_ids = $handler->get_orders_for_cleanup( $q_order_ids, $q_limit );
if ( $failed_ids && empty( array_diff( $order_ids, $failed_ids ) ) ) {
break;
}
$order_ids = array_diff( $order_ids, $failed_ids ); // Do not reattempt IDs that have already failed.
foreach ( $order_ids as $order_id ) {
try {
$handler->cleanup_post_data( $order_id, $force );
$count++;
++$count;
// translators: %d is an order ID.
WP_CLI::debug( sprintf( __( 'Cleanup completed for order %d.', 'woocommerce' ), $order_id ) );
} catch ( \Exception $e ) {
// translators: %1$d is an order ID, %2$s is an error message.
WP_CLI::warning( sprintf( __( 'An error occurred while cleaning up order %1$d: %2$s', 'woocommerce' ), $order_id, $e->getMessage() ) );
$failed_ids_in_batch[] = $order_id;
}
$progress->tick();
}
$failed_ids = array_merge( $failed_ids, $failed_ids_in_batch );
if ( ! $all_orders ) {
break;
}
if ( $failed_ids_in_batch && ! array_diff( $order_ids, $failed_ids_in_batch ) ) {
WP_CLI::warning( __( 'Failed to clean up all orders in a batch. Aborting.', 'woocommerce' ) );
break;
}
} while ( $order_ids );
$progress->finish();
if ( $failed_ids ) {
return WP_CLI::error(
sprintf(
// translators: %d is the number of orders that were cleaned up.
_n( 'Cleanup completed for %d order. Review errors above.', 'Cleanup completed for %d orders. Review errors above.', $count, 'woocommerce' ),
$count
)
);
}
WP_CLI::success(
sprintf(
// translators: %d is the number of orders that were cleaned up.
@@ -986,7 +1022,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
*/
public function status( array $args = array(), array $assoc_args = array() ) {
public function status( array $args = array(), array $assoc_args = array() ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- for backwards compat.
$legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
// translators: %s is either 'yes' or 'no'.
@@ -1058,7 +1094,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
// Format the diff array.
$diff = array_map(
function( $key, $hpos_value, $cpt_value ) {
function ( $key, $hpos_value, $cpt_value ) {
// Format for dates.
$hpos_value = is_a( $hpos_value, \WC_DateTime::class ) ? $hpos_value->format( DATE_ATOM ) : $hpos_value;
$cpt_value = is_a( $cpt_value, \WC_DateTime::class ) ? $cpt_value->format( DATE_ATOM ) : $cpt_value;
@@ -1177,5 +1213,4 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
)
);
}
}

View File

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

View File

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

View File

@@ -481,10 +481,7 @@ class FileController {
$files = $this->get_files_by_id( $file_ids );
foreach ( $files as $file ) {
$result = false;
if ( $file->is_writable() ) {
$result = $file->delete();
}
$result = $file->delete();
if ( true === $result ) {
$deleted ++;
@@ -662,7 +659,7 @@ class FileController {
$path = realpath( Settings::get_log_directory() );
if ( wp_is_writable( $path ) ) {
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ) );
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ), \RecursiveIteratorIterator::CATCH_GET_CHILD );
foreach ( $iterator as $file ) {
$bytes += $file->getSize();

View File

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

View File

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

View File

@@ -8,10 +8,12 @@ use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\File;
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Exception;
use WC_Admin_Settings;
use WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
use WP_Filesystem_Base;
use WP_Filesystem_Direct;
/**
* Settings class.
@@ -78,14 +80,13 @@ class Settings {
if ( true === $result ) {
// Create infrastructure to prevent listing contents of the logs directory.
require_once ABSPATH . 'wp-admin/includes/file.php';
global $wp_filesystem;
if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
WP_Filesystem();
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
$filesystem->put_contents( $dir . '.htaccess', 'deny from all' );
$filesystem->put_contents( $dir . 'index.html', '' );
} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Creation failed.
}
$wp_filesystem->put_contents( $dir . '.htaccess', 'deny from all' );
$wp_filesystem->put_contents( $dir . 'index.html', '' );
}
}
@@ -292,6 +293,20 @@ class Settings {
$location_info = array();
$directory = self::get_log_directory();
$status_info = array();
try {
$filesystem = FilesystemUtil::get_wp_filesystem();
if ( $filesystem instanceof WP_Filesystem_Direct ) {
$status_info[] = __( '✅ Ready', 'woocommerce' );
} else {
$status_info[] = __( '⚠️ The file system is not configured for direct writes. This could cause problems for the logger.', 'woocommerce' );
$status_info[] = __( 'You may want to switch to the database for log storage.', 'woocommerce' );
}
} catch ( Exception $exception ) {
$status_info[] = __( '⚠️ The file system connection could not be initialized.', 'woocommerce' );
$status_info[] = __( 'You may want to switch to the database for log storage.', 'woocommerce' );
}
$location_info[] = sprintf(
// translators: %s is a location in the filesystem.
__( 'Log files are stored in this directory: %s', 'woocommerce' ),
@@ -317,6 +332,11 @@ class Settings {
'id' => self::PREFIX . 'settings',
'type' => 'title',
),
'file_status' => array(
'title' => __( 'Status', 'woocommerce' ),
'type' => 'info',
'text' => implode( "\n\n", $status_info ),
),
'log_directory' => array(
'title' => __( 'Location', 'woocommerce' ),
'type' => 'info',
@@ -340,7 +360,7 @@ class Settings {
$table = "{$wpdb->prefix}woocommerce_log";
$location_info = sprintf(
// translators: %s is a location in the filesystem.
// translators: %s is the name of a table in the database.
__( 'Log entries are stored in this database table: %s', 'woocommerce' ),
"<code>$table</code>"
);

View File

@@ -221,6 +221,9 @@ class WooSubscriptionsNotes {
* @return int|false
*/
public function get_product_id_from_subscription_note( &$note ) {
if ( ! is_object( $note ) ) {
return false;
}
$content_data = $note->get_content_data();
if ( property_exists( $content_data, 'product_id' ) ) {

View File

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

View File

@@ -66,12 +66,6 @@ class OrderAttribution {
public function output( WC_Order $order ) {
$meta = $this->filter_meta_data( $order->get_meta_data() );
// If we don't have any meta to show, return.
if ( empty( $meta ) ) {
esc_html_e( 'No order source data available.', 'woocommerce' );
return;
}
$this->format_meta_data( $meta );
// No more details if there is only the origin value - this is for unknown source types.

View File

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

View File

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

View File

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

View File

@@ -250,7 +250,9 @@ abstract class CustomMetaDataStore {
$query = "SELECT DISTINCT meta_key FROM {$db_info['table']} ";
if ( ! $include_private ) {
$query .= $wpdb->prepare( 'WHERE meta_key NOT LIKE %s ', $wpdb->esc_like( '_' ) . '%' );
$query .= $wpdb->prepare( 'WHERE meta_key != \'\' AND meta_key NOT LIKE %s ', $wpdb->esc_like( '_' ) . '%' );
} else {
$query .= "WHERE meta_key != '' ";
}
$order = in_array( strtoupper( $order ), array( 'ASC', 'DESC' ), true ) ? $order : 'ASC';

View File

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

View File

@@ -153,21 +153,26 @@ class LegacyDataHandler {
public function cleanup_post_data( int $order_id, bool $skip_checks = false ): void {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
// translators: %d is an order ID.
throw new \Exception( esc_html( sprintf( __( '%d is not a valid order ID.', 'woocommerce' ), $order_id ) ) );
}
$post_is_placeholder = get_post_type( $order_id ) === $this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE;
if ( ! $post_is_placeholder ) {
$order = wc_get_order( $order_id );
if ( ! $skip_checks && ! $this->is_order_newer_than_post( $order ) ) {
throw new \Exception( esc_html( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables.', 'woocommerce' ) ) ) );
if ( ! $order ) {
// translators: %d is an order ID.
throw new \Exception( esc_html( sprintf( __( '%d is not a valid order ID.', 'woocommerce' ), $order_id ) ) );
}
if ( ! $skip_checks && ! $this->is_order_newer_than_post( $order ) ) {
// translators: %1 is an order ID.
throw new \Exception( esc_html( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables. Compare order data with `wp wc hpos diff %1$d` and use `wp wc hpos backfill %1$d --from=posts --to=hpos` to fix.', 'woocommerce' ), $order_id ) ) );
}
}
// Delete all metadata.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->postmeta} WHERE post_id = %d",
$order->get_id()
$order_id
)
);
@@ -178,11 +183,11 @@ class LegacyDataHandler {
"UPDATE {$wpdb->posts} SET post_type = %s, post_status = %s WHERE ID = %d",
$this->data_synchronizer::PLACEHOLDER_ORDER_POST_TYPE,
'draft',
$order->get_id()
$order_id
)
);
clean_post_cache( $order->get_id() );
clean_post_cache( $order_id );
}
/**

View File

@@ -1631,15 +1631,7 @@ WHERE
);
// phpcs:enable
$meta_data_query = $this->get_order_meta_select_statement();
$order_data = array();
$meta_data = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_data_query and $order_meta_table is autogenerated and should already be prepared. $id_placeholder is already prepared.
"$meta_data_query WHERE $order_meta_table.order_id in ( $id_placeholder )",
$ids
)
);
$order_data = array();
foreach ( $table_data as $table_datum ) {
$id = $table_datum->{"{$order_table_alias}_id"};
@@ -1663,14 +1655,27 @@ WHERE
$order_data[ $id ]->meta_data = array();
}
foreach ( $meta_data as $meta_datum ) {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Not a meta query.
$order_data[ $meta_datum->order_id ]->meta_data[] = (object) array(
'meta_id' => $meta_datum->id,
'meta_key' => $meta_datum->meta_key,
'meta_value' => $meta_datum->meta_value,
if ( count( $order_data ) > 0 ) {
$meta_order_ids = array_keys( $order_data );
$meta_order_id_placeholder = implode( ', ', array_fill( 0, count( $meta_order_ids ), '%d' ) );
$meta_data_query = $this->get_order_meta_select_statement();
$meta_data = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_data_query and $order_meta_table is autogenerated and should already be prepared. $id_placeholder is already prepared.
"$meta_data_query WHERE $order_meta_table.order_id in ( $meta_order_id_placeholder )",
$ids
)
);
// phpcs:enable
foreach ( $meta_data as $meta_datum ) {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Not a meta query.
$order_data[ $meta_datum->order_id ]->meta_data[] = (object) array(
'meta_id' => $meta_datum->id,
'meta_key' => $meta_datum->meta_key,
'meta_value' => $meta_datum->meta_value,
);
// phpcs:enable
}
}
return $order_data;
}

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ trait DownloadableProductTrait {
$product_downloads_section_group->add_block(
array(
'id' => 'product-downloadable',
'blockName' => 'woocommerce/product-checkbox-field',
'blockName' => 'woocommerce/product-toggle-field',
'order' => 10,
'attributes' => array(
'property' => 'downloadable',

View File

@@ -64,7 +64,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the group blocks to the template.
*/
private function add_group_blocks() {
protected function add_group_blocks() {
$this->add_group(
array(
'id' => $this::GROUP_IDS['GENERAL'],
@@ -106,7 +106,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the general group blocks to the template.
*/
private function add_general_group_blocks() {
protected function add_general_group_blocks() {
$general_group = $this->get_group_by_id( $this::GROUP_IDS['GENERAL'] );
$general_group->add_block(
array(
@@ -193,7 +193,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the pricing group blocks to the template.
*/
private function add_pricing_group_blocks() {
protected function add_pricing_group_blocks() {
$is_calc_taxes_enabled = wc_tax_enabled();
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
@@ -315,7 +315,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the inventory group blocks to the template.
*/
private function add_inventory_group_blocks() {
protected function add_inventory_group_blocks() {
$inventory_group = $this->get_group_by_id( $this::GROUP_IDS['INVENTORY'] );
$inventory_group->add_block(
array(
@@ -366,7 +366,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'blockName' => 'woocommerce/product-toggle-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
'label' => __( 'Track inventory', 'woocommerce' ),
'property' => 'manage_stock',
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
'disabledCopy' => sprintf(
@@ -425,7 +425,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
/**
* Adds the shipping group blocks to the template.
*/
private function add_shipping_group_blocks() {
protected function add_shipping_group_blocks() {
$shipping_group = $this->get_group_by_id( $this::GROUP_IDS['SHIPPING'] );
$shipping_group->add_block(
array(
@@ -471,7 +471,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'title' => __( 'Fees & dimensions', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
'</a>'
),

View File

@@ -215,6 +215,16 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'attributes' => array(
'name' => 'Product name',
'autoFocus' => true,
'metadata' => array(
'bindings' => array(
'value' => array(
'source' => 'woocommerce/entity-product',
'args' => array(
'prop' => 'name',
),
),
),
),
),
)
);
@@ -529,8 +539,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'title' => __( 'Custom fields', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Custom fields guide link opening tag. %2$s: Custom fields guide link closing tag. */
__( 'Custom fields can be used in a variety of ways, such as sharing more detailed product information, showing more input fields, or internal inventory organization. %1$sRead more about custom fields%2$s', 'woocommerce' ),
'<a href="https://wordpress.org/documentation/article/assign-custom-fields/" target="_blank" rel="noreferrer">',
__( 'Custom fields can be used in a variety of ways, such as sharing more detailed product information, showing more input fields, or for internal inventory organization. %1$sRead more about custom fields%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/document/custom-product-fields/" target="_blank" rel="noreferrer">',
'</a>'
),
),
@@ -600,10 +610,10 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$pricing_column_1->add_block(
array(
'id' => 'product-pricing-regular-price',
'blockName' => 'woocommerce/product-regular-price-field',
'order' => 10,
'attributes' => array(
'id' => 'product-pricing-regular-price',
'blockName' => 'woocommerce/product-regular-price-field',
'order' => 10,
'attributes' => array(
'name' => 'regular_price',
'label' => __( 'List price', 'woocommerce' ),
'help' => $is_calc_taxes_enabled ? null : sprintf(
@@ -613,6 +623,11 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'</a>'
),
),
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$pricing_column_2 = $pricing_columns->add_block(
@@ -627,12 +642,17 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$pricing_column_2->add_block(
array(
'id' => 'product-pricing-sale-price',
'blockName' => 'woocommerce/product-sale-price-field',
'order' => 10,
'attributes' => array(
'id' => 'product-pricing-sale-price',
'blockName' => 'woocommerce/product-sale-price-field',
'order' => 10,
'attributes' => array(
'label' => __( 'Sale price', 'woocommerce' ),
),
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_pricing_section->add_block(
@@ -778,32 +798,44 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$product_inventory_inner_section->add_block(
array(
'id' => 'product-sku-field',
'blockName' => 'woocommerce/product-sku-field',
'order' => 10,
'id' => 'product-sku-field',
'blockName' => 'woocommerce/product-sku-field',
'order' => 10,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$manage_stock = 'yes' === get_option( 'woocommerce_manage_stock' );
$product_inventory_inner_section->add_block(
array(
'id' => 'product-track-stock',
'blockName' => 'woocommerce/product-toggle-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
'id' => 'product-track-stock',
'blockName' => 'woocommerce/product-toggle-field',
'order' => 20,
'attributes' => array(
'label' => __( 'Track inventory', 'woocommerce' ),
'property' => 'manage_stock',
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
'disabledCopy' => sprintf(
'disabled' => ! $manage_stock,
'disabledCopy' => ! $manage_stock ? sprintf(
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
__( 'Per your %1$sstore settings%2$s, inventory management is <strong>disabled</strong>.', 'woocommerce' ),
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=inventory' ) . '" target="_blank" rel="noreferrer">',
'</a>'
),
) : null,
),
'hideConditions' => Features::is_enabled( 'product-external-affiliate' ) || Features::is_enabled( 'product-grouped' ) ? array(
'hideConditions' => Features::is_enabled( 'product-external-affiliate' ) || Features::is_enabled( 'product-grouped' ) ? array(
array(
'expression' => 'editedProduct.type === "external" || editedProduct.type === "grouped"',
),
) : null,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_inventory_quantity_hide_conditions = array(
@@ -836,10 +868,10 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
}
$product_inventory_section->add_block(
array(
'id' => 'product-stock-status',
'blockName' => 'woocommerce/product-radio-field',
'order' => 10,
'attributes' => array(
'id' => 'product-stock-status',
'blockName' => 'woocommerce/product-radio-field',
'order' => 10,
'attributes' => array(
'title' => __( 'Stock status', 'woocommerce' ),
'property' => 'stock_status',
'options' => array(
@@ -857,7 +889,12 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
),
),
'hideConditions' => $product_stock_status_hide_conditions,
'hideConditions' => $product_stock_status_hide_conditions,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
@@ -1023,7 +1060,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'title' => __( 'Fees & dimensions', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
'</a>'
),
@@ -1032,16 +1069,26 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
);
$product_fee_and_dimensions_section->add_block(
array(
'id' => 'product-shipping-class',
'blockName' => 'woocommerce/product-shipping-class-field',
'order' => 10,
'id' => 'product-shipping-class',
'blockName' => 'woocommerce/product-shipping-class-field',
'order' => 10,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$product_fee_and_dimensions_section->add_block(
array(
'id' => 'product-shipping-dimensions',
'blockName' => 'woocommerce/product-shipping-dimensions-fields',
'order' => 20,
'id' => 'product-shipping-dimensions',
'blockName' => 'woocommerce/product-shipping-dimensions-fields',
'order' => 20,
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
}
@@ -1106,8 +1153,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'attributes' => array(
'title' => __( 'Upsells', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Learn more about linked products. %2$s: Learn more about linked products.*/
__( 'Upsells are typically products that are extra profitable or better quality or more expensive. Experiment with combinations to boost sales. %1$sLearn more about linked products.%2$s', 'woocommerce' ),
/* translators: %1$s: "Learn more about linked products" link opening tag. %2$s: "Learn more about linked products" link closing tag. */
__( 'Upsells are typically products that are extra profitable or better quality or more expensive. Experiment with combinations to boost sales. %1$sLearn more about linked products%2$s', 'woocommerce' ),
'<br /><a href="https://woocommerce.com/document/related-products-up-sells-and-cross-sells/" target="_blank" rel="noreferrer">',
'</a>'
),
@@ -1139,8 +1186,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'attributes' => array(
'title' => __( 'Cross-sells', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Learn more about linked products. %2$s: Learn more about linked products.*/
__( 'By suggesting complementary products in the cart using cross-sells, you can significantly increase the average order value. %1$sLearn more about linked products.%2$s', 'woocommerce' ),
/* translators: %1$s: "Learn more about linked products" link opening tag. %2$s: "Learn more about linked products" link closing tag. */
__( 'By suggesting complementary products in the cart using cross-sells, you can significantly increase the average order value. %1$sLearn more about linked products%2$s', 'woocommerce' ),
'<br /><a href="https://woocommerce.com/document/related-products-up-sells-and-cross-sells/" target="_blank" rel="noreferrer">',
'</a>'
),

View File

@@ -16,7 +16,6 @@ use WC_Customer;
use WC_Log_Levels;
use WC_Logger_Interface;
use WC_Order;
use WC_Tracks;
/**
* Class OrderAttributionController
@@ -335,9 +334,6 @@ class OrderAttributionController implements RegisterHooksInterface {
$source_type = $order->get_meta( $this->get_meta_prefixed_field_name( 'source_type' ) );
$source = $order->get_meta( $this->get_meta_prefixed_field_name( 'utm_source' ) );
$origin = $this->get_origin_label( $source_type, $source );
if ( empty( $origin ) ) {
$origin = __( 'Unknown', 'woocommerce' );
}
echo esc_html( $origin );
}
@@ -440,12 +436,9 @@ class OrderAttributionController implements RegisterHooksInterface {
'customer_registered' => $order->get_customer_id() ? 'yes' : 'no',
);
$this->proxy->call_static(
WC_Tracks::class,
'record_event',
'order_attribution',
$tracks_data
);
if ( function_exists( 'wc_admin_record_tracks_event' ) ) {
wc_admin_record_tracks_event( 'order_attribution', $tracks_data );
}
}
/**

View File

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

View File

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

View File

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

View File

@@ -155,6 +155,12 @@ class PluginInstaller implements RegisterHooksInterface {
$auto_installed_plugins[ $plugin_name ] = $plugin_data;
update_site_option( 'woocommerce_autoinstalled_plugins', $auto_installed_plugins );
$auto_installed_plugins_history = get_site_option( 'woocommerce_history_of_autoinstalled_plugins', array() );
if ( ! isset( $auto_installed_plugins_history[ $plugin_name ] ) ) {
$auto_installed_plugins_history[ $plugin_name ] = $plugin_data;
update_site_option( 'woocommerce_history_of_autoinstalled_plugins', $auto_installed_plugins_history );
}
$post_install = function () use ( $plugin_name, $plugin_version, $installed_by, $plugin_url, $plugin_data ) {
$log_context = array(
'source' => 'plugin_auto_installs',

View File

@@ -30,6 +30,15 @@ class BusinessDescription extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/business-description';
}

View File

@@ -30,6 +30,15 @@ class Images extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/images';
}

View File

@@ -34,6 +34,15 @@ class Patterns extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/patterns';
}

View File

@@ -31,6 +31,15 @@ class Product extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/product';
}

View File

@@ -32,6 +32,15 @@ class Products extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/products';
}

View File

@@ -32,6 +32,15 @@ class StoreInfo extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/store-info';
}

View File

@@ -45,6 +45,15 @@ class StoreTitle extends AbstractRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/ai/store-title';
}
@@ -54,21 +63,21 @@ class StoreTitle extends AbstractRoute {
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
return array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ Middleware::class, 'is_authorized' ],
'args' => [
'business_description' => [
'callback' => array( $this, 'get_response' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'business_description' => array(
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
],
],
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
'allow_batch' => [ 'v1' => true ],
];
),
),
),
'schema' => array( $this->schema, 'get_public_item_schema' ),
'allow_batch' => array( 'v1' => true ),
);
}
/**
@@ -91,8 +100,8 @@ class StoreTitle extends AbstractRoute {
);
}
$store_title = get_option( 'blogname' );
$previous_ai_generated_title = get_option( 'ai_generated_site_title' );
$store_title = html_entity_decode( get_option( 'blogname' ) );
$previous_ai_generated_title = html_entity_decode( get_option( 'ai_generated_site_title' ) );
if ( self::DEFAULT_TITLE === $store_title || ( ! empty( $store_title ) && $previous_ai_generated_title !== $store_title ) ) {
return rest_ensure_response( array( 'ai_content_generated' => false ) );

View File

@@ -30,6 +30,15 @@ class Batch extends AbstractRoute implements RouteInterface {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/batch';
}

View File

@@ -18,6 +18,15 @@ class Cart extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart';
}

View File

@@ -20,6 +20,15 @@ class CartAddItem extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/add-item';
}

View File

@@ -20,6 +20,15 @@ class CartApplyCoupon extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/apply-coupon';
}

View File

@@ -27,6 +27,15 @@ class CartCoupons extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/coupons';
}

View File

@@ -27,6 +27,15 @@ class CartCouponsByCode extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/coupons/(?P<code>[\w-]+)';
}

View File

@@ -27,6 +27,15 @@ class CartExtensions extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/extensions';
}

View File

@@ -27,6 +27,15 @@ class CartItems extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/items';
}

View File

@@ -27,6 +27,15 @@ class CartItemsByKey extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/items/(?P<key>[\w-]{32})';
}

View File

@@ -20,6 +20,15 @@ class CartRemoveCoupon extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/remove-coupon';
}

View File

@@ -23,6 +23,15 @@ class CartRemoveItem extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/remove-item';
}

View File

@@ -20,6 +20,15 @@ class CartSelectShippingRate extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/select-shipping-rate';
}

View File

@@ -25,6 +25,15 @@ class CartUpdateCustomer extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/update-customer';
}
@@ -190,10 +199,10 @@ class CartUpdateCustomer extends AbstractCartRoute {
// We save them one by one, and we add the group prefix.
foreach ( $additional_shipping_values as $key => $value ) {
$this->additional_fields_controller->persist_field_for_customer( "/shipping/{$key}", $value, $customer );
$this->additional_fields_controller->persist_field_for_customer( $key, $value, $customer, 'shipping' );
}
foreach ( $additional_billing_values as $key => $value ) {
$this->additional_fields_controller->persist_field_for_customer( "/billing/{$key}", $value, $customer );
$this->additional_fields_controller->persist_field_for_customer( $key, $value, $customer, 'billing' );
}
wc_do_deprecated_action(
@@ -235,20 +244,7 @@ class CartUpdateCustomer extends AbstractCartRoute {
$billing_country = $customer->get_billing_country();
$billing_state = $customer->get_billing_state();
$additional_fields = $this->additional_fields_controller->get_all_fields_from_customer( $customer );
$additional_fields = array_reduce(
array_keys( $additional_fields ),
function( $carry, $key ) use ( $additional_fields ) {
if ( 0 === strpos( $key, '/billing/' ) ) {
$value = $additional_fields[ $key ];
$key = str_replace( '/billing/', '', $key );
$carry[ $key ] = $value;
}
return $carry;
},
array()
);
$additional_fields = $this->additional_fields_controller->get_all_fields_from_object( $customer, 'billing' );
/**
* There's a bug in WooCommerce core in which not having a state ("") would result in us validating against the store's state.
@@ -284,20 +280,8 @@ class CartUpdateCustomer extends AbstractCartRoute {
* @return array
*/
protected function get_customer_shipping_address( \WC_Customer $customer ) {
$additional_fields = $this->additional_fields_controller->get_all_fields_from_customer( $customer );
$additional_fields = $this->additional_fields_controller->get_all_fields_from_object( $customer, 'shipping' );
$additional_fields = array_reduce(
array_keys( $additional_fields ),
function( $carry, $key ) use ( $additional_fields ) {
if ( 0 === strpos( $key, '/shipping/' ) ) {
$value = $additional_fields[ $key ];
$key = str_replace( '/shipping/', '', $key );
$carry[ $key ] = $value;
}
return $carry;
},
array()
);
return array_merge(
[
'first_name' => $customer->get_shipping_first_name(),

View File

@@ -18,6 +18,15 @@ class CartUpdateItem extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/cart/update-item';
}

View File

@@ -44,6 +44,15 @@ class Checkout extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/checkout';
}
@@ -419,7 +428,7 @@ class Checkout extends AbstractCartRoute {
if ( is_callable( [ $customer, $callback ] ) ) {
$customer->$callback( $value );
} elseif ( $this->additional_fields_controller->is_field( $key ) ) {
$this->additional_fields_controller->persist_field_for_customer( "/billing/$key", $value, $customer );
$this->additional_fields_controller->persist_field_for_customer( $key, $value, $customer, 'billing' );
}
}
@@ -431,7 +440,7 @@ class Checkout extends AbstractCartRoute {
if ( is_callable( [ $customer, $callback ] ) ) {
$customer->$callback( $value );
} elseif ( $this->additional_fields_controller->is_field( $key ) ) {
$this->additional_fields_controller->persist_field_for_customer( "/shipping/$key", $value, $customer );
$this->additional_fields_controller->persist_field_for_customer( $key, $value, $customer, 'shipping' );
}
}

View File

@@ -41,6 +41,15 @@ class CheckoutOrder extends AbstractCartRoute {
* @return string
*/
public function get_path() {
return self::get_path_regex();
}
/**
* Get the path of this rest route.
*
* @return string
*/
public static function get_path_regex() {
return '/checkout/(?P<id>[\d]+)';
}

Some files were not shown because too many files have changed in this diff Show More