-

+

diff --git a/plugins/woocommerce/includes/class-wc-auth.php b/plugins/woocommerce/includes/class-wc-auth.php index 099282c7a62..9614234b53f 100644 --- a/plugins/woocommerce/includes/class-wc-auth.php +++ b/plugins/woocommerce/includes/class-wc-auth.php @@ -333,7 +333,7 @@ class WC_Auth { */ // Check if Jetpack is installed and activated. - if ( class_exists( 'Jetpack' ) && Jetpack::connection()->is_active() ) { + if ( class_exists( 'Jetpack' ) && Jetpack::connection()->has_connected_owner() ) { // Check if the user is using the WordPress.com SSO. if ( Jetpack::is_module_active( 'sso' ) ) { @@ -341,7 +341,7 @@ class WC_Auth { $redirect_url = $this->build_url( $data, 'authorize' ); // Build the SSO URL. - $login_url = Jetpack_SSO::get_instance()->build_sso_button_url( + $login_url = \Automattic\Jetpack\Connection\SSO::get_instance()->build_sso_button_url( array( 'redirect_to' => rawurlencode( esc_url_raw( $redirect_url ) ), 'action' => 'login', diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php index b3d2816f442..6b036136f8a 100644 --- a/plugins/woocommerce/includes/class-wc-cart.php +++ b/plugins/woocommerce/includes/class-wc-cart.php @@ -106,6 +106,7 @@ class WC_Cart extends WC_Legacy_Cart { add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 ); + add_action( 'woocommerce_removed_coupon', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 ); add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 ); @@ -715,7 +716,6 @@ class WC_Cart extends WC_Legacy_Cart { } return $return; - } /** diff --git a/plugins/woocommerce/includes/class-wc-cli.php b/plugins/woocommerce/includes/class-wc-cli.php index 274704e683c..c30255ac558 100644 --- a/plugins/woocommerce/includes/class-wc-cli.php +++ b/plugins/woocommerce/includes/class-wc-cli.php @@ -6,7 +6,7 @@ * @version 3.0.0 */ -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner; +use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner; use Automattic\WooCommerce\Internal\ProductAttributesLookup\CLIRunner as ProductAttributesLookupCLIRunner; defined( 'ABSPATH' ) || exit; diff --git a/plugins/woocommerce/includes/class-wc-customer-download.php b/plugins/woocommerce/includes/class-wc-customer-download.php index 43d5a2101af..7d879b7801b 100644 --- a/plugins/woocommerce/includes/class-wc-customer-download.php +++ b/plugins/woocommerce/includes/class-wc-customer-download.php @@ -171,13 +171,8 @@ class WC_Customer_Download extends WC_Data implements ArrayAccess { */ public function get_download_count( $context = 'view' ) { // Check for count of download logs. - $data_store = WC_Data_Store::load( 'customer-download-log' ); - $download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() ); - - $download_log_count = 0; - if ( ! empty( $download_log_ids ) ) { - $download_log_count = count( $download_log_ids ); - } + $data_store = WC_Data_Store::load( 'customer-download-log' ); + $download_log_count = $data_store->get_download_logs_count_for_permission( $this->get_id() ); // Check download count in prop. $download_count_prop = $this->get_prop( 'download_count', $context ); diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php index 24a9000b0a4..6b7a66da18e 100644 --- a/plugins/woocommerce/includes/class-wc-install.php +++ b/plugins/woocommerce/includes/class-wc-install.php @@ -17,7 +17,7 @@ use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchro use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; -use Automattic\WooCommerce\Utilities\OrderUtil; +use Automattic\WooCommerce\Utilities\{ OrderUtil, PluginUtil }; use Automattic\WooCommerce\Internal\Utilities\PluginInstaller; defined( 'ABSPATH' ) || exit; @@ -257,6 +257,10 @@ class WC_Install { ), '9.1.0' => array( 'wc_update_910_add_launch_your_store_tour_option', + 'wc_update_910_remove_obsolete_user_meta', + ), + '9.2.0' => array( + 'wc_update_920_add_wc_hooked_blocks_version_option', ), ); @@ -1263,7 +1267,8 @@ class WC_Install { return; } - if ( in_array( $legacy_api_plugin, wp_get_active_and_valid_plugins(), true ) ) { + $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins(); + if ( in_array( $legacy_api_plugin, $active_valid_plugins, true ) ) { return; } diff --git a/plugins/woocommerce/includes/class-wc-product-simple.php b/plugins/woocommerce/includes/class-wc-product-simple.php index e49f4959d0f..98bb00f3ffe 100644 --- a/plugins/woocommerce/includes/class-wc-product-simple.php +++ b/plugins/woocommerce/includes/class-wc-product-simple.php @@ -74,4 +74,28 @@ class WC_Product_Simple extends WC_Product { return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this ); } + + /** + * Get the add to cart button success message - used to update the mini cart live region. + * + * @return string + */ + public function add_to_cart_success_message() { + $text = ''; + + if ( $this->is_purchasable() && $this->is_in_stock() ) { + /* translators: %s: Product title */ + $text = __( '“%s” has been added to your cart', 'woocommerce' ); + $text = sprintf( $text, $this->get_name() ); + } + + /** + * Filter product add to cart success message. + * + * @since 9.2.0 + * @param string $text The success message when a product is added to the cart. + * @param WC_Product_Simple $this Reference to the current WC_Product_Simple instance. + */ + return apply_filters( 'woocommerce_product_add_to_cart_success_message', $text, $this ); + } } diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php index f41f30fbab1..5ee937b6e1a 100644 --- a/plugins/woocommerce/includes/class-wc-tracker.php +++ b/plugins/woocommerce/includes/class-wc-tracker.php @@ -965,6 +965,7 @@ class WC_Tracker { 'hpos_transactions_enabled' => get_option( 'woocommerce_use_db_transactions_for_custom_orders_table_data_sync' ), 'hpos_transactions_level' => get_option( 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync' ), 'show_marketplace_suggestions' => get_option( 'woocommerce_show_marketplace_suggestions' ), + 'admin_install_timestamp' => get_option( 'woocommerce_admin_install_timestamp' ), ); } diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 9eb3f3c003d..3dba08bbeb3 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -26,10 +26,9 @@ use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub; use Automattic\WooCommerce\Internal\Utilities\WebhookUtil; use Automattic\WooCommerce\Internal\Admin\Marketplace; +use Automattic\WooCommerce\Internal\McStats; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Utilities\{LoggingUtil, RestApiUtil, TimeUtil}; -use Automattic\WooCommerce\Admin\WCAdminHelper; -use Automattic\WooCommerce\Admin\Features\Features; /** * Main WooCommerce Class. @@ -45,7 +44,7 @@ final class WooCommerce { * * @var string */ - public $version = '9.2.0'; + public $version = '9.3.0'; /** * WooCommerce Schema version. @@ -54,7 +53,7 @@ final class WooCommerce { * * @var string */ - public $db_version = '430'; + public $db_version = '920'; /** * The single instance of the class. @@ -307,7 +306,9 @@ final class WooCommerce { self::add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) ); add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_remote_variant' ) ); add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_remote_variant' ) ); + add_action( 'woocommerce_newly_installed', 'wc_set_hooked_blocks_version', 10 ); + self::add_filter( 'robots_txt', array( $this, 'robots_txt' ) ); add_filter( 'wp_plugin_dependencies_slug', array( $this, 'convert_woocommerce_slug' ) ); // These classes set up hooks on instantiation. @@ -402,6 +403,12 @@ final class WooCommerce { $context ); + // Record fatal error stats. + $container = wc_get_container(); + $mc_stats = $container->get( McStats::class ); + $mc_stats->add( 'error', 'fatal-errors-during-shutdown' ); + $mc_stats->do_server_side_stats(); + /** * Action triggered when there are errors during shutdown. * @@ -1038,6 +1045,43 @@ final class WooCommerce { } } + /** + * Tell bots not to index some WooCommerce-created directories. + * + * We try to detect the default "User-agent: *" added by WordPress and add our rules to that group, because + * it's possible that some bots will only interpret the first group of rules if there are multiple groups with + * the same user agent. + * + * @param string $output The contents that WordPress will output in a robots.txt file. + * + * @return string + */ + private function robots_txt( $output ) { + $path = ( ! empty( $site_url['path'] ) ) ? $site_url['path'] : ''; + + $lines = preg_split( '/\r\n|\r|\n/', $output ); + $agent_index = array_search( 'User-agent: *', $lines, true ); + + if ( false !== $agent_index ) { + $above = array_slice( $lines, 0, $agent_index + 1 ); + $below = array_slice( $lines, $agent_index + 1 ); + } else { + $above = $lines; + $below = array(); + + $above[] = ''; + $above[] = 'User-agent: *'; + } + + $above[] = "Disallow: $path/wp-content/uploads/wc-logs/"; + $above[] = "Disallow: $path/wp-content/uploads/woocommerce_transient_files/"; + $above[] = "Disallow: $path/wp-content/uploads/woocommerce_uploads/"; + + $lines = array_merge( $above, $below ); + + return implode( PHP_EOL, $lines ); + } + /** * Set tablenames inside WPDB object. */ diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php index 5219aff2867..82e129cadcd 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php @@ -7,6 +7,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; +use Automattic\WooCommerce\Internal\Utilities\Users; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -340,17 +341,36 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat * @return WC_Order|false */ public function get_last_order( &$customer ) { - //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment + // Try to fetch the last order placed by this customer. + $last_order_id = Users::get_site_user_meta( $customer->get_id(), 'wc_last_order', true ); + $last_customer_order = false; + + if ( ! empty( $last_order_id ) ) { + $last_customer_order = wc_get_order( $last_order_id ); + } + + // "Unset" the last order ID if the order is associated with another customer. Unsetting is done by making it an + // empty string, for compatibility with the declared types of the following filter hook. + if ( + ! $last_customer_order instanceof WC_Order + || intval( $last_customer_order->get_customer_id() ) !== intval( $customer->get_id() ) + ) { + $last_order_id = ''; + } + /** * Filters the id of the last order from a given customer. * - * @param string @last_order_id The last order id as retrieved from the database. - * @param WC_Customer The customer whose last order id is being retrieved. + * @since 4.9.1 + * + * @param string $last_order_id The last order id as retrieved from the database. + * @param WC_Customer $customer The customer whose last order id is being retrieved. + * * @return string The actual last order id to use. */ $last_order_id = apply_filters( 'woocommerce_customer_get_last_order', - get_user_meta( $customer->get_id(), '_last_order', true ), + $last_order_id, $customer ); //phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment @@ -385,7 +405,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat ); } //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - update_user_meta( $customer->get_id(), '_last_order', $last_order_id ); + Users::update_site_user_meta( $customer->get_id(), 'wc_last_order', $last_order_id ); } if ( ! $last_order_id ) { @@ -405,7 +425,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat public function get_order_count( &$customer ) { $count = apply_filters( 'woocommerce_customer_get_order_count', - get_user_meta( $customer->get_id(), '_order_count', true ), + Users::get_site_user_meta( $customer->get_id(), 'wc_order_count', true ), $customer ); @@ -436,7 +456,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat } //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared - update_user_meta( $customer->get_id(), '_order_count', $count ); + Users::update_site_user_meta( $customer->get_id(), 'wc_order_count', $count ); } return absint( $count ); @@ -452,7 +472,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat public function get_total_spent( &$customer ) { $spent = apply_filters( 'woocommerce_customer_get_total_spent', - get_user_meta( $customer->get_id(), '_money_spent', true ), + Users::get_site_user_meta( $customer->get_id(), 'wc_money_spent', true ), $customer ); @@ -499,7 +519,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat if ( ! $spent ) { $spent = 0; } - update_user_meta( $customer->get_id(), '_money_spent', $spent ); + Users::update_site_user_meta( $customer->get_id(), 'wc_money_spent', $spent ); } return wc_format_decimal( $spent, 2 ); diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php index 1001e7001ab..2f56df7666e 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-download-log-data-store.php @@ -149,10 +149,10 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da } /** - * Get array of download log ids by specified args. + * Get array of download logs, or the count of existing logs, by specified args. * - * @param array $args Arguments to define download logs to retrieve. - * @return array + * @param array $args Arguments to define download logs to retrieve. If $args['return'] is 'count' then the count of existing logs will be returned. + * @return array|int */ public function get_download_logs( $args = array() ) { global $wpdb; @@ -171,9 +171,11 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da ) ); + $is_count = 'count' === $args['return']; + $query = array(); $table = $wpdb->prefix . self::get_table_name(); - $query[] = "SELECT * FROM {$table} WHERE 1=1"; + $query[] = 'SELECT ' . ( $is_count ? 'COUNT(1)' : '*' ) . " FROM {$table} WHERE 1=1"; if ( $args['permission_id'] ) { $query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] ); @@ -197,7 +199,13 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); } - $raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok. + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + if ( $is_count ) { + return absint( $wpdb->get_var( implode( ' ', $query ) ) ); + } + + $raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared switch ( $args['return'] ) { case 'ids': @@ -226,6 +234,26 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da ); } + /** + * Get the count of download logs for a given download permission. + * + * @param int $permission_id Permission to get logs count for. + * @return int + */ + public function get_download_logs_count_for_permission( $permission_id ) { + // If no permission_id is passed, return an empty array. + if ( empty( $permission_id ) ) { + return 0; + } + + return $this->get_download_logs( + array( + 'permission_id' => $permission_id, + 'return' => 'count', + ) + ); + } + /** * Method to delete download logs for a given permission ID. * diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php index 7882f1673aa..9a96390266e 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php @@ -5,6 +5,8 @@ * @package WooCommerce\Classes */ +use Automattic\WooCommerce\Utilities\OrderUtil; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -189,22 +191,37 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement // Also grab the current status so we can compare. $previous_status = get_post_status( $order->get_id() ); + // If the order doesn't exist in the DB, we will consider it as new. + if ( ! $previous_status && $order->get_id() === 0 ) { + $previous_status = 'new'; + } // Update the order. parent::update( $order ); - // Fire a hook depending on the status - this should be considered a creation if it was previously draft status. - $new_status = $order->get_status( 'edit' ); + $current_status = $order->get_status( 'edit' ); - if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft', 'checkout-draft' ), true ) ) { - do_action( 'woocommerce_new_order', $order->get_id(), $order ); - } else { - do_action( 'woocommerce_update_order', $order->get_id(), $order ); + // We need to remove the wc- prefix from the status for comparison and proper evaluation of new vs updated orders. + $previous_status = OrderUtil::remove_status_prefix( $previous_status ); + $current_status = OrderUtil::remove_status_prefix( $current_status ); + + $draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' ); + + // This hook should be fired only if the new status is not one of draft statuses and the previous status was one of the draft statuses. + if ( + $current_status !== $previous_status + && ! in_array( $current_status, $draft_statuses, true ) + && in_array( $previous_status, $draft_statuses, true ) + ) { + do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + return; } + + do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } /** - * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. + * Helper method that updates all the post meta for an order based on its settings in the WC_Order class. * * @param WC_Order $order Order object. * @since 3.0.0 @@ -1003,6 +1020,40 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement * @return array|object */ public function query( $query_vars ) { + /** + * Allows 3rd parties to filter query args that will trigger an unsupported notice. + * + * @since 9.2.0 + * + * @param array $unsupported_args Array of query arg names. + */ + $unsupported_args = (array) apply_filters( + 'woocommerce_order_data_store_cpt_query_unsupported_args', + array( 'meta_query', 'field_query' ) + ); + + // Trigger doing_it_wrong() for query vars only supported in HPOS. + $unsupported_args_in_query = array_keys( array_filter( array_intersect_key( $query_vars, array_flip( $unsupported_args ) ) ) ); + + if ( $unsupported_args_in_query && __CLASS__ === get_class( $this ) ) { + wc_doing_it_wrong( + __METHOD__, + esc_html( + sprintf( + // translators: %s is a comma separated list of query arguments. + _n( + 'Order query argument (%s) is not supported on the current order datastore.', + 'Order query arguments (%s) are not supported on the current order datastore.', + count( $unsupported_args_in_query ), + 'woocommerce' + ), + implode( ', ', $unsupported_args_in_query ) + ) + ), + '9.2.0' + ); + } + $args = $this->get_wp_query_args( $query_vars ); if ( ! empty( $args['errors'] ) ) { diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php index 7bdc8bd3368..5087a4beb30 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php @@ -334,7 +334,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da $product->apply_changes(); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment - do_action( 'woocommerce_update_product', $product->get_id(), $product, $changes ); + do_action( 'woocommerce_update_product', $product->get_id(), $product ); } /** @@ -2237,23 +2237,26 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da $stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null; $price = wc_format_decimal( get_post_meta( $id, '_price', true ) ); $sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) ); - return array( - 'product_id' => absint( $id ), - 'sku' => get_post_meta( $id, '_sku', true ), - 'global_unique_id' => get_post_meta( $id, '_global_unique_id', true ), - 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0, - 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0, - 'min_price' => reset( $price_meta ), - 'max_price' => end( $price_meta ), - 'onsale' => $sale_price && $price === $sale_price ? 1 : 0, - 'stock_quantity' => $stock, - 'stock_status' => get_post_meta( $id, '_stock_status', true ), - 'rating_count' => array_sum( array_map( 'intval', (array) get_post_meta( $id, '_wc_rating_count', true ) ) ), - 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ), - 'total_sales' => get_post_meta( $id, 'total_sales', true ), - 'tax_status' => get_post_meta( $id, '_tax_status', true ), - 'tax_class' => get_post_meta( $id, '_tax_class', true ), + $product_data = array( + 'product_id' => absint( $id ), + 'sku' => get_post_meta( $id, '_sku', true ), + 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0, + 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0, + 'min_price' => reset( $price_meta ), + 'max_price' => end( $price_meta ), + 'onsale' => $sale_price && $price === $sale_price ? 1 : 0, + 'stock_quantity' => $stock, + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'rating_count' => array_sum( array_map( 'intval', (array) get_post_meta( $id, '_wc_rating_count', true ) ) ), + 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ), + 'total_sales' => get_post_meta( $id, 'total_sales', true ), + 'tax_status' => get_post_meta( $id, '_tax_status', true ), + 'tax_class' => get_post_meta( $id, '_tax_class', true ), ); + if ( get_option( 'woocommerce_schema_version', 0 ) >= 920 ) { + $product_data['global_unique_id'] = get_post_meta( $id, '_global_unique_id', true ); + } + return $product_data; } return array(); } diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php index 210efaa79b7..72bacd6bf30 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php @@ -144,6 +144,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { + $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -172,6 +177,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { + $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -186,6 +196,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { + $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -218,6 +233,28 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { return rest_ensure_response( $data ); } + /** + * Fetch a single product review from the database. + * + * @param int $id Review ID. + * @param int $product_id Product ID. + * + * @since 9.2.0 + * @return \WP_Comment + */ + protected function get_review( int $id, int $product_id ) { + if ( 0 >= $product_id || 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = 0 <= $id ? get_comment( $id ) : null; + if ( empty( $review ) || empty( $review->comment_ID ) || 'review' !== get_comment_type( $id ) || empty( $review->comment_post_ID ) || (int) $review->comment_post_ID !== $product_id ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $review; + } + /** * Get a single product review. * @@ -225,17 +262,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { - $id = (int) $request['id']; - $product_id = (int) $request['product_id']; - - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $review = get_comment( $id ); - - if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { - return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + $review = $this->get_review( (int) $request['id'], (int) $request['product_id'] ); + if ( is_wp_error( $review ) ) { + return $review; } $delivery = $this->prepare_item_for_response( $review, $request ); @@ -309,14 +338,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { $product_review_id = (int) $request['id']; $product_id = (int) $request['product_id']; - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $review = get_comment( $product_review_id ); - - if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { - return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + $review = $this->get_review( $product_review_id, $product_id ); + if ( is_wp_error( $review ) ) { + return $review; } $prepared_review = $this->prepare_item_for_database( $request ); @@ -358,15 +382,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { public function delete_item( $request ) { $product_id = (int) $request['product_id']; $product_review_id = (int) $request['id']; - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + $product_review = $this->get_review( $product_review_id, $product_id ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - if ( 'product' !== get_post_type( $product_id ) ) { - return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); - } - - $product_review = get_comment( $product_review_id ); - if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { - return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + if ( is_wp_error( $product_review ) ) { + return $product_review; } /** diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index a8aa9c46ede..aeebf511d81 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -468,7 +468,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { } // Format the order status. - $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + $data['status'] = OrderUtil::remove_status_prefix( $data['status'] ); // Format line items. foreach ( $format_line_items as $key ) { diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php index e9da6f31cdd..873d7863c7f 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php @@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper; use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories; use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer as Order_DataSynchronizer; -use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil }; +use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil, PluginUtil }; /** * System status controller class. @@ -1044,16 +1044,11 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller { return array(); } - $active_plugins = (array) get_option( 'active_plugins', array() ); - if ( is_multisite() ) { - $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); - $active_plugins = array_merge( $active_plugins, $network_activated_plugins ); - } + $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins(); + $active_plugins_data = array(); - $active_plugins_data = array(); - - foreach ( $active_plugins as $plugin ) { - $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + foreach ( $active_valid_plugins as $plugin ) { + $data = get_plugin_data( $plugin ); $active_plugins_data[] = $this->format_plugin_data( $plugin, $data ); } diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php index b2081f9cce3..46fad08b36a 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php @@ -149,6 +149,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { + $review = $this->get_review( (int) $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -177,6 +182,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller { * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { + $review = $this->get_review( (int) $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -191,6 +201,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller { * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { + $review = $this->get_review( (int) $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -1057,13 +1072,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller { } $review = get_comment( $id ); - if ( empty( $review ) ) { + if ( empty( $review ) || 'review' !== get_comment_type( $id ) ) { return $error; } if ( ! empty( $review->comment_post_ID ) ) { - $post = get_post( (int) $review->comment_post_ID ); - if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) { return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); } diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php index 49dafb20c7e..5873d403e45 100644 --- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php +++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php @@ -39,7 +39,12 @@ class WC_Shortcode_My_Account { return; } - if ( ! is_user_logged_in() || isset( $wp->query_vars['lost-password'] ) ) { + if ( ! is_user_logged_in() ) { + /** + * Filters the message shown on the 'my account' page when the user is not logged in. + * + * @since 2.6.0 + */ $message = apply_filters( 'woocommerce_my_account_message', '' ); if ( ! empty( $message ) ) { @@ -56,50 +61,16 @@ class WC_Shortcode_My_Account { } else { wc_get_template( 'myaccount/form-login.php' ); } - } else { - // Start output buffer since the html may need discarding for BW compatibility. - ob_start(); - - if ( isset( $wp->query_vars['customer-logout'] ) ) { - /* translators: %s: logout url */ - wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out', 'woocommerce' ), wc_logout_url() ) ); - } - - // Collect notices before output. - $notices = wc_get_notices(); - - // Output the new account page. - self::my_account( $atts ); - - /** - * Deprecated my-account.php template handling. This code should be - * removed in a future release. - * - * If woocommerce_account_content did not run, this is an old template - * so we need to render the endpoint content again. - */ - if ( ! did_action( 'woocommerce_account_content' ) ) { - if ( ! empty( $wp->query_vars ) ) { - foreach ( $wp->query_vars as $key => $value ) { - if ( 'pagename' === $key ) { - continue; - } - if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) { - ob_clean(); // Clear previous buffer. - wc_set_notices( $notices ); - wc_print_notices(); - do_action( 'woocommerce_account_' . $key . '_endpoint', $value ); - break; - } - } - - wc_deprecated_function( 'Your theme version of my-account.php template', '2.6', 'the latest version, which supports multiple account pages and navigation, from WC 2.6.0' ); - } - } - - // Send output buffer. - ob_end_flush(); + return; } + + if ( isset( $wp->query_vars['customer-logout'] ) ) { + /* translators: %s: logout url */ + wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out', 'woocommerce' ), wc_logout_url() ) ); + } + + // Output the my account page. + self::my_account( $atts ); } /** diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php index 0021c9afc50..092be4f81ee 100644 --- a/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php +++ b/plugins/woocommerce/includes/tracks/events/class-wc-product-collection-block-tracking.php @@ -121,7 +121,7 @@ class WC_Product_Collection_Block_Tracking { 'in_single_product' => $is_in_single_product ? 'yes' : 'no', 'in_template_part' => $is_in_template_part ? 'yes' : 'no', 'in_synced_pattern' => $is_in_synced_pattern ? 'yes' : 'no', - 'filters' => $this->get_query_filters_usage_data( $block ), + 'filters' => wp_json_encode( $this->get_query_filters_usage_data( $block ) ), ); } diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php index 3056f5b07d4..91532cf2a9f 100644 --- a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php +++ b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php @@ -29,8 +29,7 @@ class WC_Products_Tracking { add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 ); add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 ); add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 ); - add_action( 'woocommerce_new_product', array( $this, 'track_product_published' ), 10, 3 ); - add_action( 'woocommerce_update_product', array( $this, 'track_product_published' ), 10, 3 ); + add_action( 'wp_after_insert_post', array( $this, 'track_product_published' ), 10, 4 ); add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) ); add_action( 'edited_product_cat', array( $this, 'track_product_category_updated' ) ); add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 ); @@ -304,21 +303,24 @@ class WC_Products_Tracking { /** * Send a Tracks event when a product is published. * - * @param int $product_id Product ID. - * @param WC_Product $product Product object. - * @param array $changes Product changes. + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @param bool $update Whether this is an existing post being updated. + * @param null|WP_Post $post_before Null for new posts, the WP_Post object prior + * to the update for updated posts. */ - public function track_product_published( $product_id, $product, $changes = null ) { + public function track_product_published( $post_id, $post, $update, $post_before ) { if ( - ! isset( $product ) || - 'product' !== $product->post_type || - 'publish' !== $product->get_status( 'edit' ) || - ( $changes && ! isset( $changes['status'] ) ) + 'product' !== $post->post_type || + 'publish' !== $post->post_status || + ( $post_before && 'publish' === $post_before->post_status ) ) { return; } - $product_type_options = self::get_product_type_options( $product_id ); + $product = wc_get_product( $post_id ); + + $product_type_options = self::get_product_type_options( $post_id ); $product_type_options_string = self::get_product_type_options_string( $product_type_options ); $properties = array( @@ -332,7 +334,7 @@ class WC_Products_Tracking { 'is_virtual' => $product->is_virtual() ? 'yes' : 'no', 'manage_stock' => $product->get_manage_stock() ? 'yes' : 'no', 'menu_order' => $product->get_menu_order() ? 'yes' : 'no', - 'product_id' => $product_id, + 'product_id' => $post_id, 'product_gallery' => count( $product->get_gallery_image_ids() ), 'product_image' => $product->get_image_id() ? 'yes' : 'no', 'product_type' => $product->get_type(), diff --git a/plugins/woocommerce/includes/wc-account-functions.php b/plugins/woocommerce/includes/wc-account-functions.php index 7b76c515dae..3f34a3a5801 100644 --- a/plugins/woocommerce/includes/wc-account-functions.php +++ b/plugins/woocommerce/includes/wc-account-functions.php @@ -176,11 +176,13 @@ function wc_get_account_endpoint_url( $endpoint ) { return wc_get_page_permalink( 'myaccount' ); } + $url = wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) ); + if ( 'customer-logout' === $endpoint ) { - return wc_logout_url(); + return wp_nonce_url( $url, 'customer-logout' ); } - return wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) ); + return $url; } /** diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php index c4657188505..b2fb32db9a7 100644 --- a/plugins/woocommerce/includes/wc-cart-functions.php +++ b/plugins/woocommerce/includes/wc-cart-functions.php @@ -184,7 +184,7 @@ function wc_clear_cart_after_payment() { } } - if ( WC()->session->order_awaiting_payment > 0 ) { + if ( is_object( WC()->session ) && WC()->session->order_awaiting_payment > 0 ) { $order = wc_get_order( WC()->session->order_awaiting_payment ); if ( $order instanceof WC_Order && $order->get_id() > 0 ) { diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php index c1e4b6d3944..420f2da080d 100644 --- a/plugins/woocommerce/includes/wc-order-functions.php +++ b/plugins/woocommerce/includes/wc-order-functions.php @@ -9,6 +9,8 @@ */ use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer; +use Automattic\WooCommerce\Internal\Utilities\Users; +use Automattic\WooCommerce\Utilities\OrderUtil; use Automattic\WooCommerce\Utilities\StringUtil; defined( 'ABSPATH' ) || exit; @@ -146,9 +148,9 @@ function wc_get_is_pending_statuses() { */ function wc_get_order_status_name( $status ) { $statuses = wc_get_order_statuses(); - $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status; - $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status; - return $status; + $status = OrderUtil::remove_status_prefix( $status ); + + return $statuses[ 'wc-' . $status ] ?? $status; } /** @@ -484,9 +486,9 @@ function wc_delete_shop_order_transients( $order = 0 ) { // Clear customer's order related caches. if ( is_a( $order, 'WC_Order' ) ) { $order_id = $order->get_id(); - delete_user_meta( $order->get_customer_id(), '_money_spent' ); - delete_user_meta( $order->get_customer_id(), '_order_count' ); - delete_user_meta( $order->get_customer_id(), '_last_order' ); + Users::delete_site_user_meta( $order->get_customer_id(), 'wc_money_spent' ); + Users::delete_site_user_meta( $order->get_customer_id(), 'wc_order_count' ); + Users::delete_site_user_meta( $order->get_customer_id(), 'wc_last_order' ); } else { $order_id = 0; } diff --git a/plugins/woocommerce/includes/wc-page-functions.php b/plugins/woocommerce/includes/wc-page-functions.php index 2b0a03f05e8..f80ac3e54a0 100644 --- a/plugins/woocommerce/includes/wc-page-functions.php +++ b/plugins/woocommerce/includes/wc-page-functions.php @@ -119,26 +119,32 @@ function wc_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { } /** - * Hide menu items conditionally. + * Hide or adjust menu items conditionally. * * @param array $items Navigation items. * @return array */ function wc_nav_menu_items( $items ) { - if ( ! is_user_logged_in() ) { - $customer_logout = get_option( 'woocommerce_logout_endpoint', 'customer-logout' ); + $logout_endpoint = get_option( 'woocommerce_logout_endpoint', 'customer-logout' ); - if ( ! empty( $customer_logout ) && ! empty( $items ) && is_array( $items ) ) { - foreach ( $items as $key => $item ) { - if ( empty( $item->url ) ) { - continue; - } - $path = wp_parse_url( $item->url, PHP_URL_PATH ) ?? ''; - $query = wp_parse_url( $item->url, PHP_URL_QUERY ) ?? ''; + if ( ! empty( $logout_endpoint ) && ! empty( $items ) && is_array( $items ) ) { + foreach ( $items as $key => $item ) { + if ( empty( $item->url ) ) { + continue; + } - if ( strstr( $path, $customer_logout ) || strstr( $query, $customer_logout ) ) { - unset( $items[ $key ] ); - } + $path = wp_parse_url( $item->url, PHP_URL_PATH ) ?? ''; + $query = wp_parse_url( $item->url, PHP_URL_QUERY ) ?? ''; + $is_logout_link = strstr( $path, $logout_endpoint ) || strstr( $query, $logout_endpoint ); + + if ( ! $is_logout_link ) { + continue; + } + + if ( is_user_logged_in() ) { + $items[ $key ]->url = wp_nonce_url( $item->url, 'customer-logout' ); + } else { + unset( $items[ $key ] ); } } } @@ -147,6 +153,40 @@ function wc_nav_menu_items( $items ) { } add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_items', 10 ); +/** + * Hide menu items in navigation blocks conditionally. + * + * Does the same thing as wc_nav_menu_items but for block themes. + * + * @since 9.3.0 + * @param \WP_Block_list $inner_blocks Inner blocks. + * @return \WP_Block_list + */ +function wc_nav_menu_inner_blocks( $inner_blocks ) { + $logout_endpoint = get_option( 'woocommerce_logout_endpoint', 'customer-logout' ); + + if ( ! empty( $logout_endpoint ) && $inner_blocks ) { + foreach ( $inner_blocks as $inner_block_key => $inner_block ) { + $url = $inner_block->parsed_block['attrs']['url'] ?? ''; + $path = wp_parse_url( $url, PHP_URL_PATH ) ?? ''; + $query = wp_parse_url( $url, PHP_URL_QUERY ) ?? ''; + $is_logout_link = strstr( $path, $logout_endpoint ) || strstr( $query, $logout_endpoint ); + + if ( ! $is_logout_link ) { + continue; + } + + if ( is_user_logged_in() ) { + $inner_block->parsed_block['attrs']['url'] = wp_nonce_url( $inner_block->parsed_block['attrs']['url'], 'customer-logout' ); + } else { + unset( $inner_blocks[ $inner_block_key ] ); + } + } + } + + return $inner_blocks; +} +add_filter( 'block_core_navigation_render_inner_blocks', 'wc_nav_menu_inner_blocks' ); /** * Fix active class in nav for shop page. @@ -168,7 +208,7 @@ function wc_nav_menu_item_classes( $menu_items ) { $menu_id = (int) $menu_item->object_id; // Unset active class for blog page. - if ( $page_for_posts === $menu_id ) { + if ( $page_for_posts === $menu_id && isset( $menu_item->object ) && 'page' === $menu_item->object ) { $menu_items[ $key ]->current = false; if ( in_array( 'current_page_parent', $classes, true ) ) { diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php index 92341e606ee..9e49568115e 100644 --- a/plugins/woocommerce/includes/wc-product-functions.php +++ b/plugins/woocommerce/includes/wc-product-functions.php @@ -1571,6 +1571,7 @@ function wc_update_product_lookup_tables_column( $column ) { ); break; case 'sku': + case 'global_unique_id': case 'stock_status': case 'average_rating': case 'total_sales': diff --git a/plugins/woocommerce/includes/wc-stock-functions.php b/plugins/woocommerce/includes/wc-stock-functions.php index 4aee1011828..9ad42486d7b 100644 --- a/plugins/woocommerce/includes/wc-stock-functions.php +++ b/plugins/woocommerce/includes/wc-stock-functions.php @@ -10,6 +10,8 @@ defined( 'ABSPATH' ) || exit; +use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; + /** * Update a product's stock amount. * @@ -348,7 +350,8 @@ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 return 0; } - return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); + $reserve_stock = new ReserveStock(); + return $reserve_stock->get_reserved_stock( $product, $exclude_order_id ); } /** @@ -374,7 +377,8 @@ function wc_reserve_stock_for_order( $order ) { $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { - ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order ); + $reserve_stock = new ReserveStock(); + $reserve_stock->reserve_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); @@ -400,7 +404,8 @@ function wc_release_stock_for_order( $order ) { $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { - ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order ); + $reserve_stock = new ReserveStock(); + $reserve_stock->release_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php index 6bd3be8ce29..61842621efb 100644 --- a/plugins/woocommerce/includes/wc-template-functions.php +++ b/plugins/woocommerce/includes/wc-template-functions.php @@ -31,18 +31,24 @@ function wc_template_redirect() { if ( is_page( wc_get_page_id( 'checkout' ) ) && wc_get_page_id( 'checkout' ) !== wc_get_page_id( 'cart' ) && WC()->cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { wp_safe_redirect( wc_get_cart_url() ); exit; - } - // Logout. - if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { - wp_safe_redirect( str_replace( '&', '&', wp_logout_url( apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ) ) ) ); + // Logout endpoint under My Account page. Logging out requires a valid nonce. + if ( isset( $wp->query_vars['customer-logout'] ) ) { + if ( ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { + wp_logout(); + wp_safe_redirect( wc_get_logout_redirect_url() ); + exit; + } + /* translators: %s: logout url */ + wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out', 'woocommerce' ), wc_logout_url() ) ); + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } - // Redirect to the correct logout endpoint. - if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) { - wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) ); + // Redirect to edit account if trying to recover password whilst logged in. + if ( isset( $wp->query_vars['lost-password'] ) && is_user_logged_in() ) { + wp_safe_redirect( esc_url_raw( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) ) ); exit; } @@ -1387,6 +1393,10 @@ if ( ! function_exists( 'woocommerce_template_loop_add_to_cart' ) ) { ), ); + if ( is_a( $product, 'WC_Product_Simple' ) ) { + $defaults['attributes']['data-success_message'] = $product->add_to_cart_success_message(); + } + $args = apply_filters( 'woocommerce_loop_add_to_cart_args', wp_parse_args( $args, $defaults ), $product ); if ( ! empty( $args['attributes']['aria-describedby'] ) ) { @@ -2854,6 +2864,12 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) { } if ( $args['required'] ) { + // hidden inputs are the only kind of inputs that don't need an `aria-required` attribute. + // checkboxes apply the `custom_attributes` to the label - we need to apply the attribute on the input itself, instead. + if ( ! in_array( $args['type'], array( 'hidden', 'checkbox' ), true ) ) { + $args['custom_attributes']['aria-required'] = 'true'; + } + $args['class'][] = 'validate-required'; $required = ' *'; } else { @@ -2978,12 +2994,13 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) { } $field .= sprintf( - ' %6$s', + ' %7$s', esc_attr( $key ), esc_attr( $args['id'] ), esc_attr( $args['checked_value'] ), esc_attr( 'input-checkbox ' . implode( ' ', $args['input_class'] ) ), checked( $value, $args['checked_value'], false ), + $args['required'] ? ' aria-required="true"' : '', wp_kses_post( $args['label'] ) ); @@ -3728,22 +3745,31 @@ function wc_get_price_html_from_text() { } /** - * Get logout endpoint. + * Get the redirect URL after logging out. Defaults to the my account page. + * + * @since 9.3.0 + * @return string + */ +function wc_get_logout_redirect_url() { + /** + * Filters the logout redirect URL. + * + * @since 2.6.9 + * @param string $logout_url Logout URL. + * @return string + */ + return apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ); +} + +/** + * Get logout link. * * @since 2.6.9 - * * @param string $redirect Redirect URL. - * * @return string */ function wc_logout_url( $redirect = '' ) { - $redirect = $redirect ? $redirect : apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ); - - if ( get_option( 'woocommerce_logout_endpoint' ) ) { - return wp_nonce_url( wc_get_endpoint_url( 'customer-logout', '', $redirect ), 'customer-logout' ); - } - - return wp_logout_url( $redirect ); + return wp_logout_url( $redirect ? $redirect : wc_get_logout_redirect_url() ); } /** @@ -4019,3 +4045,45 @@ function wc_update_product_archive_title( $post_type_name, $post_type ) { add_filter( 'post_type_archive_title', 'wc_update_product_archive_title', 10, 2 ); // phpcs:enable Generic.Commenting.Todo.TaskFound + +/** + * Set the version of the hooked blocks in the database. Used when WC is installed for the first time. + * + * @since 9.2.0 + * + * @return void + */ +function wc_set_hooked_blocks_version() { + // Only set the version if the current theme is a block theme. + if ( ! wc_current_theme_is_fse_theme() && ! current_theme_supports( 'block-template-parts' ) ) { + return; + } + + $option_name = 'woocommerce_hooked_blocks_version'; + + if ( get_option( $option_name ) ) { + return; + } + + add_option( $option_name, WC()->version ); +} + +/** + * If the user switches from a classic to a block theme and they haven't already got a woocommerce_hooked_blocks_version, + * set the version of the hooked blocks in the database, or as "no" to disable all block hooks then set as the latest WC version. + * + * @since 9.2.0 + * + * @param string $old_name Old theme name. + * @param \WP_Theme $old_theme Instance of the old theme. + * @return void + */ +function wc_set_hooked_blocks_version_on_theme_switch( $old_name, $old_theme ) { + $option_name = 'woocommerce_hooked_blocks_version'; + $option_value = get_option( $option_name, false ); + + // Sites with the option value set to "no" have already been migrated, and block hooks have been disabled. Checking explicitly for false to avoid setting the option again. + if ( ! $old_theme->is_block_theme() && ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) && false === $option_value ) { + add_option( $option_name, WC()->version ); + } +} diff --git a/plugins/woocommerce/includes/wc-template-hooks.php b/plugins/woocommerce/includes/wc-template-hooks.php index 22588ca1d2f..e9fa7da964f 100644 --- a/plugins/woocommerce/includes/wc-template-hooks.php +++ b/plugins/woocommerce/includes/wc-template-hooks.php @@ -319,3 +319,8 @@ add_action( 'woocommerce_before_customer_login_form', 'woocommerce_output_all_no add_action( 'woocommerce_before_lost_password_form', 'woocommerce_output_all_notices', 10 ); add_action( 'before_woocommerce_pay', 'woocommerce_output_all_notices', 10 ); add_action( 'woocommerce_before_reset_password_form', 'woocommerce_output_all_notices', 10 ); + +/** + * Hooked blocks. + */ +add_action( 'after_switch_theme', 'wc_set_hooked_blocks_version_on_theme_switch', 10, 2 ); diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php index db0d2fc0c99..b3604083bb8 100644 --- a/plugins/woocommerce/includes/wc-update-functions.php +++ b/plugins/woocommerce/includes/wc-update-functions.php @@ -2729,3 +2729,89 @@ function wc_update_891_create_plugin_autoinstall_history_option() { function wc_update_910_add_launch_your_store_tour_option() { add_option( 'woocommerce_show_lys_tour', 'yes' ); } + +/** + * Add woocommerce_hooked_blocks_version option for existing stores that are using a theme that supports the Block Hooks API + */ +function wc_update_920_add_wc_hooked_blocks_version_option() { + if ( ! wc_current_theme_is_fse_theme() && ! current_theme_supports( 'block-template-parts' ) ) { + return; + } + + $option_name = 'woocommerce_hooked_blocks_version'; + $option_value = get_option( $option_name ); + + // If the option already exists, we don't need to do anything. + if ( false !== $option_value ) { + return; + } + + /** + * A list of theme slugs to execute this with. + * We are applying this filter to allow for the list to be extended by third-parties who were already using it. + * + * @since 8.4.0 + */ + $theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) ); + $active_theme_name = wp_get_theme()->get( 'Name' ); + $should_set_hooked_blocks_version = in_array( $active_theme_name, $theme_include_list, true ); + + if ( $should_set_hooked_blocks_version ) { + // Set 8.4.0 as the version for existing stores that are using a theme that supports the Block Hooks API. + // This will ensure that the Block Hooks API is enabled for these stores and works as expected. + // Existing stores that aren't running approved block themes will not have the Block Hooks API enabled. + add_option( $option_name, '8.4.0' ); + } else { + // For block themes that aren't approved themes set this option to "no" to completely disable hooked blocks. + // This means we can assume the absence of the option is when a site is switching from a classic theme to a block theme for the first time. + // Note: We have to use "no" instead of false since the latter is the default value for the option if it doesn't exist. + add_option( $option_name, 'no' ); + } +} + +/** + * Remove user meta associated with the keys '_last_order', '_order_count' and '_money_spent'. + * + * New keys are now used for these, to improve compatibility with multisite networks. + * + * @return void + */ +function wc_update_910_remove_obsolete_user_meta() { + global $wpdb; + + $deletions = $wpdb->query( " + DELETE FROM $wpdb->usermeta + WHERE meta_key IN ( + '_last_order', + '_order_count', + '_money_spent' + ) + " ); + + $logger = wc_get_logger(); + + if ( null === $logger ) { + return; + } + + if ( false === $deletions ) { + $logger->notice( + 'During the update to 9.1.0, WooCommerce attempted to remove user meta with the keys "_last_order", "_order_count" and "_money_spent" but was unable to do so.', + array( + 'source' => 'wc-updater', + ) + ); + } else { + $logger->info( + sprintf( + 1 === $deletions + ? 'During the update to 9.1.0, WooCommerce removed %d user meta row associated with the meta keys "_last_order", "_order_count" or "_money_spent".' + : 'During the update to 9.1.0, WooCommerce removed %d user meta rows associated with the meta keys "_last_order", "_order_count" or "_money_spent".', + number_format_i18n( $deletions ) + ), + array( + 'source' => 'wc-updater', + ) + ); + } +} diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php index 06a5d732515..aa6e60f318a 100644 --- a/plugins/woocommerce/includes/wc-user-functions.php +++ b/plugins/woocommerce/includes/wc-user-functions.php @@ -9,6 +9,7 @@ */ use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; +use Automattic\WooCommerce\Internal\Utilities\Users; use Automattic\WooCommerce\Utilities\OrderUtil; defined( 'ABSPATH' ) || exit; @@ -280,9 +281,9 @@ function wc_update_new_customer_past_orders( $customer_id ) { if ( $complete ) { update_user_meta( $customer_id, 'paying_customer', 1 ); - update_user_meta( $customer_id, '_order_count', '' ); - update_user_meta( $customer_id, '_money_spent', '' ); - delete_user_meta( $customer_id, '_last_order' ); + Users::update_site_user_meta( $customer_id, 'wc_order_count', '' ); + Users::update_site_user_meta( $customer_id, 'wc_money_spent', '' ); + Users::delete_site_user_meta( $customer_id, 'wc_last_order' ); } return $linked; diff --git a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php index 0dbee4d8feb..886e8e286b2 100644 --- a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php +++ b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-ssr-controller.php @@ -55,7 +55,7 @@ class WC_REST_WCCOM_Site_SSR_Controller extends WC_REST_WCCOM_Site_Controller { } /** - * Generate SSR data and submit it to WooCommmerce.com. + * Generate SSR data and submit it to WooCommerce.com. * * @since 7.8.0 * @param WP_REST_Request $request Full details about the request. diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 942b39e0e6c..0506a8a3dbe 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -2,7 +2,7 @@ "name": "@woocommerce/plugin-woocommerce", "private": true, "title": "WooCommerce", - "version": "9.2.0", + "version": "9.3.0", "homepage": "https://woocommerce.com/", "repository": { "type": "git", @@ -51,10 +51,9 @@ "makepot": "composer run-script makepot", "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", "test": "pnpm test:unit", - "test:api": "API_TEST_REPORT_DIR=\"$PWD/tests/api\" pnpm exec wc-api-tests test api", - "test:api-pw": "USE_WP_ENV=1 pnpm playwright test --config=tests/api-core-tests/playwright.config.js", - "test:e2e-pw": "pnpm test:e2e:install && pnpm playwright test --config=tests/e2e-pw/envs/default/playwright.config.js", - "test:e2e": "pnpm test:e2e:install && pnpm test:e2e:with-env default", + "test:api": "pnpm test:e2e:default --project=api --workers 4", + "test:e2e": "pnpm test:e2e:default --project=ui", + "test:e2e:default": "pnpm test:e2e:install && pnpm test:e2e:with-env default", "test:e2e:install": "pnpm playwright install chromium", "test:e2e:blocks": "pnpm --filter='@woocommerce/block-library' test:e2e", "test:e2e:with-env": "pnpm test:e2e:install && bash ./tests/e2e-pw/run-tests-with-env.sh", @@ -73,7 +72,8 @@ "update-wp-env": "php ./tests/e2e-pw/bin/update-wp-env.php", "watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'", "watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'", - "watch:build:project:copy-assets": "wireit" + "watch:build:project:copy-assets": "wireit", + "wp-env": "wp-env" }, "lint-staged": { "*.php": [ @@ -104,33 +104,13 @@ }, "tests": [ { - "name": "PHP", + "name": "PHP: 8.0 WP: latest", "testType": "unit:php", "command": "test:php:env", - "changes": [ - "client/admin/config/*.json", - "composer.json", - "composer.lock", - "includes/**/*.php", - "patterns/**/*.php", - "src/**/*.php", - "templates/**/*.php", - "tests/php/**/*.php", - "tests/legacy/unit-tests/**/*.php", - "tests/unit-tests/**/*.php" + "shardingArguments": [ + "--testsuite=wc-phpunit-legacy", + "--testsuite=wc-phpunit-main" ], - "testEnv": { - "start": "env:test" - }, - "events": [ - "pull_request", - "push" - ] - }, - { - "name": "PHP 8.0", - "testType": "unit:php", - "command": "test:php:env", "changes": [ "client/admin/config/*.json", "composer.json", @@ -141,12 +121,14 @@ "templates/**/*.php", "tests/php/**/*.php", "tests/legacy/unit-tests/**/*.php", - "tests/unit-tests/**/*.php" + "tests/unit-tests/**/*.php", + ".wp-env.json" ], "testEnv": { "start": "env:test", "config": { - "phpVersion": "8.0" + "phpVersion": "8.0", + "wpVersion": "latest" } }, "events": [ @@ -158,6 +140,10 @@ "name": "PHP WP: latest - 1", "testType": "unit:php", "command": "test:php:env", + "shardingArguments": [ + "--testsuite=wc-phpunit-legacy", + "--testsuite=wc-phpunit-main" + ], "changes": [ "client/admin/config/*.json", "composer.json", @@ -168,7 +154,8 @@ "templates/**/*.php", "tests/php/**/*.php", "tests/legacy/unit-tests/**/*.php", - "tests/unit-tests/**/*.php" + "tests/unit-tests/**/*.php", + ".wp-env.json" ], "testEnv": { "start": "env:test", @@ -181,37 +168,14 @@ "push" ] }, - { - "name": "PHP WP: latest - 2", - "testType": "unit:php", - "command": "test:php:env", - "changes": [ - "client/admin/config/*.json", - "composer.json", - "composer.lock", - "includes/**/*.php", - "patterns/**/*.php", - "src/**/*.php", - "templates/**/*.php", - "tests/php/**/*.php", - "tests/legacy/unit-tests/**/*.php", - "tests/unit-tests/**/*.php" - ], - "testEnv": { - "start": "env:test", - "config": { - "wpVersion": "latest-2" - } - }, - "events": [ - "pull_request", - "push" - ] - }, { "name": "PHP WP: nightly", "testType": "unit:php", "command": "test:php:env", + "shardingArguments": [ + "--testsuite=wc-phpunit-legacy", + "--testsuite=wc-phpunit-main" + ], "optional": true, "changes": [ "client/admin/config/*.json", @@ -223,7 +187,8 @@ "templates/**/*.php", "tests/php/**/*.php", "tests/legacy/unit-tests/**/*.php", - "tests/unit-tests/**/*.php" + "tests/unit-tests/**/*.php", + ".wp-env.json" ], "testEnv": { "start": "env:test", @@ -239,7 +204,7 @@ { "name": "Core e2e tests", "testType": "e2e", - "command": "test:e2e:with-env default", + "command": "test:e2e", "shardingArguments": [ "--shard=1/6", "--shard=2/6", @@ -256,7 +221,8 @@ "patterns/**/*.php", "src/**/*.php", "templates/**/*.php", - "tests/e2e-pw/**" + "tests/e2e-pw/**", + ".wp-env.json" ], "testEnv": { "start": "env:test" @@ -286,7 +252,6 @@ "changes": [], "events": [ "daily-checks", - "nightly-checks", "release-checks" ], "testEnv": { @@ -383,7 +348,7 @@ { "name": "Core e2e tests - HPOS disabled", "testType": "e2e", - "command": "test:e2e:with-env default", + "command": "test:e2e", "shardingArguments": [ "--shard=1/5", "--shard=2/5", @@ -395,16 +360,7 @@ "daily-checks", "release-checks" ], - "changes": [ - "client/admin/config/*.json", - "composer.json", - "composer.lock", - "includes/**/*.php", - "patterns/**/*.php", - "src/**/*.php", - "templates/**/*.php", - "tests/e2e-pw/**" - ], + "changes": [], "testEnv": { "start": "env:test", "config": { @@ -420,7 +376,7 @@ { "name": "Core e2e tests - PHP 8.1", "testType": "e2e", - "command": "test:e2e:with-env default", + "command": "test:e2e", "shardingArguments": [ "--shard=1/5", "--shard=2/5", @@ -448,7 +404,7 @@ { "name": "Core e2e tests - WP latest-1", "testType": "e2e", - "command": "test:e2e:with-env default", + "command": "test:e2e", "shardingArguments": [ "--shard=1/5", "--shard=2/5", @@ -476,7 +432,7 @@ { "name": "Core API tests", "testType": "api", - "command": "test:api-pw", + "command": "test:api", "optional": false, "changes": [ "client/admin/config/*.json", @@ -486,8 +442,7 @@ "patterns/**/*.php", "src/**/*.php", "templates/**/*.php", - "tests/api-core-tests/**", - "tests/e2e-pw/bin/**" + ".wp-env.json" ], "testEnv": { "start": "env:test" @@ -498,14 +453,14 @@ ], "report": { "resultsBlobName": "core-api-report", - "resultsPath": "tests/api-core-tests/test-results", + "resultsPath": "tests/e2e-pw/test-results", "allure": true } }, { "name": "Core API tests - HPOS disabled", "testType": "api", - "command": "test:api-pw", + "command": "test:api", "optional": false, "changes": [ "client/admin/config/*.json", @@ -515,8 +470,8 @@ "patterns/**/*.php", "src/**/*.php", "templates/**/*.php", - "tests/api-core-tests/**", - "tests/e2e-pw/bin/**" + "tests/e2e-pw/bin/**", + ".wp-env.json" ], "events": [ "push" @@ -529,7 +484,7 @@ }, "report": { "resultsBlobName": "core-api-report-hpos-disabled", - "resultsPath": "tests/api-core-tests/test-results", + "resultsPath": "tests/e2e-pw/test-results", "allure": true } }, @@ -546,7 +501,8 @@ "patterns/**/*.php", "src/**/*.php", "templates/**/*.php", - "tests/performance/**" + "tests/performance/**", + ".wp-env.json" ], "testEnv": { "start": "env:perf" @@ -569,10 +525,11 @@ "src/**/*.php", "templates/**/*.php", "templates/**/*.html", - "tests/metrics/**" + "tests/metrics/**", + ".wp-env.json" ], "events": [ - "push" + "disabled" ] }, { @@ -601,7 +558,8 @@ "events": [ "pull_request", "push", - "release-checks" + "release-checks", + "nightly-checks" ], "report": { "resultsBlobName": "blocks-e2e-report", @@ -616,13 +574,30 @@ "shardingArguments": [], "changes": [], "events": [ - "daily-checks" + "daily-checks", + "on-demand" ], "report": { "resultsBlobName": "default-pressable-core-e2e", "resultsPath": "tests/e2e-pw/test-results", "allure": true } + }, + { + "name": "Core e2e tests - default WPCOM site", + "testType": "e2e", + "command": "test:e2e:with-env default-wpcom", + "shardingArguments": [], + "changes": [], + "events": [ + "daily-checks", + "on-demand" + ], + "report": { + "resultsBlobName": "default-wpcom-core-e2e", + "resultsPath": "tests/e2e-pw/test-results", + "allure": true + } } ] } @@ -647,7 +622,7 @@ "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", "@wordpress/babel-preset-default": "3.0.2", "@wordpress/e2e-test-utils-playwright": "wp-6.4", - "@wordpress/env": "^9.0.7", + "@wordpress/env": "^9.7.0", "@wordpress/stylelint-config": "^21.36.0", "allure-commandline": "^2.25.0", "allure-playwright": "^2.9.2", diff --git a/plugins/woocommerce/patterns/banner.php b/plugins/woocommerce/patterns/banner.php index 09d145e6299..11da651b411 100644 --- a/plugins/woocommerce/patterns/banner.php +++ b/plugins/woocommerce/patterns/banner.php @@ -27,9 +27,9 @@ $second_description = $content['descriptions'][1]['default'] ?? '';

- -

- + +

+

diff --git a/plugins/woocommerce/patterns/coming-soon-entire-site.php b/plugins/woocommerce/patterns/coming-soon-entire-site.php index e168886d453..6853b90d637 100644 --- a/plugins/woocommerce/patterns/coming-soon-entire-site.php +++ b/plugins/woocommerce/patterns/coming-soon-entire-site.php @@ -19,8 +19,8 @@ if ( 'twentytwentyfour' === $current_theme ) { } ?> - -
+ +
@@ -60,140 +60,5 @@ if ( 'twentytwentyfour' === $current_theme ) {
-
+
diff --git a/plugins/woocommerce/patterns/coming-soon-store-only.php b/plugins/woocommerce/patterns/coming-soon-store-only.php index b84d6e6a192..ad54ce0ccc9 100644 --- a/plugins/woocommerce/patterns/coming-soon-store-only.php +++ b/plugins/woocommerce/patterns/coming-soon-store-only.php @@ -20,8 +20,8 @@ if ( 'twentytwentyfour' === $current_theme ) { ?> - -
+ +
'; } ?> - -
+
diff --git a/plugins/woocommerce/patterns/content-right-image-left.php b/plugins/woocommerce/patterns/content-right-image-left.php index baa05e426d0..f6959940329 100644 --- a/plugins/woocommerce/patterns/content-right-image-left.php +++ b/plugins/woocommerce/patterns/content-right-image-left.php @@ -9,9 +9,9 @@ declare(strict_types=1); use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper; -$header = __( 'Discover a world of possibilities', 'woocommerce' ); -$content = __( 'Welcome to a world of limitless possibilities, where the journey is as exhilarating as the destination, and where every moment is an opportunity to make your mark on the canvas of existence. The only limit is the extent of your imagination.', 'woocommerce' ); -$button = __( 'Get Started', 'woocommerce' ); +$header = __( 'Committed to a greener lifestyle', 'woocommerce' ); +$content = __( "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.", 'woocommerce' ); +$button = __( 'Meet us', 'woocommerce' ); $image_0 = PatternsHelper::get_image_url( $images, 0, 'assets/images/pattern-placeholders/drinkware-liquid-tableware-dishware-bottle-fluid.jpg' ); ?> diff --git a/plugins/woocommerce/patterns/featured-category-cover-image.php b/plugins/woocommerce/patterns/featured-category-cover-image.php index 0b9d5b6d7ea..fbc70579df4 100644 --- a/plugins/woocommerce/patterns/featured-category-cover-image.php +++ b/plugins/woocommerce/patterns/featured-category-cover-image.php @@ -14,7 +14,7 @@ $description = $content['descriptions'][0]['default'] ?? ''; $button = $content['buttons'][0]['default'] ?? ''; ?> - +
diff --git a/plugins/woocommerce/patterns/filters.php b/plugins/woocommerce/patterns/filters.php new file mode 100644 index 00000000000..f386dfbad44 --- /dev/null +++ b/plugins/woocommerce/patterns/filters.php @@ -0,0 +1,70 @@ + + + +
+

+ + + +
+
+ + + +
+

+ + + +
+
+ + + +
+

+ + + +
+
+ + + +
+

+ + +attribute_id; +} +?> + + +
+
+ + + +
+

+ + + +
+
+ diff --git a/plugins/woocommerce/patterns/four-image-grid-content-left.php b/plugins/woocommerce/patterns/four-image-grid-content-left.php index 383e5aedc5f..30e05b32f52 100644 --- a/plugins/woocommerce/patterns/four-image-grid-content-left.php +++ b/plugins/woocommerce/patterns/four-image-grid-content-left.php @@ -18,14 +18,12 @@ $image_3 = PatternsHelper::get_image_url( $images, 0, 'assets/images/pattern-pla ?> - - - +
- +

diff --git a/plugins/woocommerce/patterns/header-minimal.php b/plugins/woocommerce/patterns/header-minimal.php index 8d42656b815..589e9ad6f49 100644 --- a/plugins/woocommerce/patterns/header-minimal.php +++ b/plugins/woocommerce/patterns/header-minimal.php @@ -19,7 +19,10 @@
+ + +
diff --git a/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php b/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php index 10af0171142..ea0b91ac4de 100644 --- a/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php +++ b/plugins/woocommerce/patterns/heading-with-three-columns-of-content-with-link.php @@ -20,8 +20,8 @@ $button_link = __( 'Get started', 'woocommerce' ); -
-

+
+

diff --git a/plugins/woocommerce/patterns/hero-product-3-split.php b/plugins/woocommerce/patterns/hero-product-3-split.php index 4fb7f78c5a8..3da1d1619bc 100644 --- a/plugins/woocommerce/patterns/hero-product-3-split.php +++ b/plugins/woocommerce/patterns/hero-product-3-split.php @@ -50,7 +50,7 @@ $third_description = $content['descriptions'][2]['default'] ?? '';
-

+

diff --git a/plugins/woocommerce/patterns/hero-product-chessboard.php b/plugins/woocommerce/patterns/hero-product-chessboard.php index 521f4ccf355..074fd5afc68 100644 --- a/plugins/woocommerce/patterns/hero-product-chessboard.php +++ b/plugins/woocommerce/patterns/hero-product-chessboard.php @@ -21,7 +21,7 @@ $third_description = $content['descriptions'][2]['default'] ?? ''; $button = $content['buttons'][0]['default'] ?? ''; ?> - +
diff --git a/plugins/woocommerce/patterns/product-collection-4-columns.php b/plugins/woocommerce/patterns/product-collection-4-columns.php index 5162999a59e..c74d10bdd96 100644 --- a/plugins/woocommerce/patterns/product-collection-4-columns.php +++ b/plugins/woocommerce/patterns/product-collection-4-columns.php @@ -8,7 +8,7 @@ $products_title = $content['titles'][0]['default'] ?? ''; ?> - +
@@ -18,6 +18,10 @@ $products_title = $content['titles'][0]['default'] ?? '';

+ + + +
diff --git a/plugins/woocommerce/patterns/product-collection-5-columns.php b/plugins/woocommerce/patterns/product-collection-5-columns.php index 8bcc01d3106..679aa7b8e49 100644 --- a/plugins/woocommerce/patterns/product-collection-5-columns.php +++ b/plugins/woocommerce/patterns/product-collection-5-columns.php @@ -18,6 +18,10 @@ $products_title = $content['titles'][0]['default'] ?? '';

+ + + +
diff --git a/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php b/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php index 0b0af2e252b..eb4fdd8a6d4 100644 --- a/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php +++ b/plugins/woocommerce/patterns/product-collection-featured-products-5-columns.php @@ -20,6 +20,10 @@ $collection_title = $content['titles'][0]['default'] ?? ''; + + + +
diff --git a/plugins/woocommerce/patterns/product-query-product-gallery.php b/plugins/woocommerce/patterns/product-query-product-gallery.php index f1d7dacd549..4aa243a0ea7 100644 --- a/plugins/woocommerce/patterns/product-query-product-gallery.php +++ b/plugins/woocommerce/patterns/product-query-product-gallery.php @@ -19,6 +19,10 @@ $products_title = $content['titles'][0]['default'] ?? '';

+ + + +
diff --git a/plugins/woocommerce/patterns/social-follow-us-in-social-media.php b/plugins/woocommerce/patterns/social-follow-us-in-social-media.php index afcfd73a82c..ce7e871f649 100644 --- a/plugins/woocommerce/patterns/social-follow-us-in-social-media.php +++ b/plugins/woocommerce/patterns/social-follow-us-in-social-media.php @@ -41,6 +41,10 @@ $social_title = $content['titles'][0]['default'] ?? '';
+ + + +
diff --git a/plugins/woocommerce/patterns/testimonials-3-columns.php b/plugins/woocommerce/patterns/testimonials-3-columns.php index bedaf53adb1..c659a0600ad 100644 --- a/plugins/woocommerce/patterns/testimonials-3-columns.php +++ b/plugins/woocommerce/patterns/testimonials-3-columns.php @@ -24,6 +24,10 @@ $third_description = $content['descriptions'][2]['default'] ?? '';

+ + + +
@@ -37,7 +41,7 @@ $third_description = $content['descriptions'][2]['default'] ?? ''; -

~ Sophia K.

+

Sophia K.

@@ -54,7 +58,7 @@ $third_description = $content['descriptions'][2]['default'] ?? ''; -

~ Liam M.

+

Liam M.

@@ -70,7 +74,7 @@ $third_description = $content['descriptions'][2]['default'] ?? ''; -

~ Ava L.

+

Ava L.

diff --git a/plugins/woocommerce/patterns/testimonials-single.php b/plugins/woocommerce/patterns/testimonials-single.php index 0f5df3a6c73..8b495b788d2 100644 --- a/plugins/woocommerce/patterns/testimonials-single.php +++ b/plugins/woocommerce/patterns/testimonials-single.php @@ -28,8 +28,8 @@ $description = $content['descriptions'][0]['default'] ?? '';
- -
+ +

@@ -39,7 +39,7 @@ $description = $content['descriptions'][0]['default'] ?? ''; -

– Monica P.

+

Monica P.

diff --git a/plugins/woocommerce/patterns/three-columns-with-images-and-content.php b/plugins/woocommerce/patterns/three-columns-with-images-and-content.php index c0852e91fdc..7aa31d055a6 100644 --- a/plugins/woocommerce/patterns/three-columns-with-images-and-content.php +++ b/plugins/woocommerce/patterns/three-columns-with-images-and-content.php @@ -23,8 +23,8 @@ $image_2 = PatternsHelper::get_image_url( $images, 0, 'assets/images/patte -
-

+
+

diff --git a/plugins/woocommerce/phpunit.xml b/plugins/woocommerce/phpunit.xml index 608fe200f14..9038c56f82d 100644 --- a/plugins/woocommerce/phpunit.xml +++ b/plugins/woocommerce/phpunit.xml @@ -9,20 +9,13 @@ verbose="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> - + ./tests/legacy/unit-tests + + ./tests/php ./tests/php/helpers - - ./tests/legacy/unit-tests - ./tests/php - ./tests/legacy/unit-tests/woocommerce-admin - ./tests/php/helpers - - - ./tests/legacy/unit-tests/woocommerce-admin - diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt index 9d905836bda..16a7b9988f7 100644 --- a/plugins/woocommerce/readme.txt +++ b/plugins/woocommerce/readme.txt @@ -1,10 +1,10 @@ === WooCommerce === Contributors: automattic, woocommerce, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho, barryhughes-1, claudiulodro, tiagonoronha, ryelle, levinmedia, aljullu, nerrad, joshuawold, assassinateur, haszari, mppfeiffer, nielslange, opr18, ralucastn, tjcafferkey, danielwrobert, patriciahillebrandt, albarin, dinhtungdu, imanish003, karolmanijak, sunyatasattva, alexandrelara, gigitux, danieldudzic, samueljseay, alexflorisca, opr18, tarunvijwani, pauloarromba, saadtarhi, bor0, kloon, coreymckrill, jorgeatorres, leifsinger Tags: online store, ecommerce, shop, shopping cart, sell online -Requires at least: 6.4 -Tested up to: 6.5 +Requires at least: 6.5 +Tested up to: 6.6 Requires PHP: 7.4 -Stable tag: 9.0.2 +Stable tag: 9.1.4 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -169,6 +169,6 @@ WooCommerce comes with some sample data you can use to see how products look; im == Changelog == -= 9.2.0 2024-XX-XX = += 9.3.0 2024-XX-XX = [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt). diff --git a/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php b/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php index c779f9d5c5d..3d044bab301 100644 --- a/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php +++ b/plugins/woocommerce/src/Admin/API/OnboardingFreeExtensions.php @@ -97,37 +97,6 @@ class OnboardingFreeExtensions extends WC_REST_Data_Controller { } } - $extensions = $this->replace_jetpack_with_jetpack_boost_for_treatment( $extensions ); - return new WP_REST_Response( $extensions ); } - - private function replace_jetpack_with_jetpack_boost_for_treatment( array $extensions ) { - $is_treatment = \WooCommerce\Admin\Experimental_Abtest::in_treatment( 'woocommerce_jetpack_copy' ); - - if ( ! $is_treatment ) { - return $extensions; - } - - $has_core_profiler = array_search( 'obw/core-profiler', array_column( $extensions, 'key' ) ); - - if ( $has_core_profiler === false ) { - return $extensions; - } - - $has_jetpack = array_search( 'jetpack', array_column( $extensions[ $has_core_profiler ]['plugins'], 'key' ) ); - - if ( $has_jetpack === false ) { - return $extensions; - } - - $jetpack = &$extensions[ $has_core_profiler ]['plugins'][ $has_jetpack ]; - $jetpack->key = 'jetpack-boost'; - $jetpack->name = 'Jetpack Boost'; - $jetpack->label = __( 'Optimize store performance with Jetpack Boost', 'woocommerce' ); - $jetpack->description = __( 'Speed up your store and improve your SEO with performance-boosting tools from Jetpack. Learn more', 'woocommerce' ); - $jetpack->learn_more_link = 'https://jetpack.com/boost/'; - - return $extensions; - } } diff --git a/plugins/woocommerce/src/Admin/API/Orders.php b/plugins/woocommerce/src/Admin/API/Orders.php index 64138466a65..863c377afa4 100644 --- a/plugins/woocommerce/src/Admin/API/Orders.php +++ b/plugins/woocommerce/src/Admin/API/Orders.php @@ -239,7 +239,7 @@ class Orders extends \WC_REST_Orders_Controller { } // Format the order status. - $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + $data['status'] = OrderUtil::remove_status_prefix( $data['status'] ); // Format requested line items. $formatted_line_items = array(); diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php index 81e85182f1c..b48a7245d38 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php @@ -524,14 +524,13 @@ class Controller extends ReportsController implements ExportableInterface { $export_columns = array( 'date_created' => __( 'Date', 'woocommerce' ), 'order_number' => __( 'Order #', 'woocommerce' ), - 'total_formatted' => __( 'N. Revenue (formatted)', 'woocommerce' ), 'status' => __( 'Status', 'woocommerce' ), 'customer_name' => __( 'Customer', 'woocommerce' ), 'customer_type' => __( 'Customer type', 'woocommerce' ), 'products' => __( 'Product(s)', 'woocommerce' ), 'num_items_sold' => __( 'Items sold', 'woocommerce' ), 'coupons' => __( 'Coupon(s)', 'woocommerce' ), - 'net_total' => __( 'N. Revenue', 'woocommerce' ), + 'net_total' => __( 'Net Sales', 'woocommerce' ), 'attribution' => __( 'Attribution', 'woocommerce' ), ); @@ -555,16 +554,15 @@ class Controller extends ReportsController implements ExportableInterface { */ public function prepare_item_for_export( $item ) { $export_item = array( - 'date_created' => $item['date_created'], + 'date_created' => $item['date'], 'order_number' => $item['order_number'], - 'total_formatted' => $item['total_formatted'], 'status' => $item['status'], 'customer_name' => isset( $item['extended_info']['customer'] ) ? $this->get_customer_name( $item['extended_info']['customer'] ) : null, 'customer_type' => $item['customer_type'], 'products' => isset( $item['extended_info']['products'] ) ? $this->get_products( $item['extended_info']['products'] ) : null, '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'], + 'net_total' => $item['net_total'], 'attribution' => $item['extended_info']['attribution']['origin'], ); diff --git a/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php index b5e5eb94587..2f9a430e7cb 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Taxes/DataStore.php @@ -82,7 +82,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { 'total_tax' => 'SUM(total_tax) as total_tax', 'order_tax' => 'SUM(order_tax) as order_tax', 'shipping_tax' => 'SUM(shipping_tax) as shipping_tax', - 'orders_count' => "COUNT({$table_name}.order_id) as orders_count", + 'orders_count' => "COUNT( DISTINCT ( CASE WHEN parent_id = 0 THEN {$table_name}.order_id END ) ) as orders_count", ); } @@ -146,8 +146,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { public function get_data( $query_args ) { global $wpdb; - $table_name = self::get_db_table_name(); - // These defaults are only partially applied when used via REST API, as that has its own defaults. $defaults = array( 'per_page' => get_option( 'posts_per_page' ), @@ -204,6 +202,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { $this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) ); $this->subquery->add_sql_clause( 'group_by', ", {$wpdb->prefix}woocommerce_order_items.order_item_name, {$wpdb->prefix}woocommerce_order_itemmeta.meta_value" ); $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) ); + $this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) ); $taxes_query = $this->subquery->get_query_statement(); diff --git a/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php index 39980256db7..03231e9674d 100644 --- a/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php +++ b/plugins/woocommerce/src/Admin/API/Reports/Variations/Controller.php @@ -39,6 +39,7 @@ class Controller extends ReportsController implements ExportableInterface { */ protected $param_mapping = array( 'variations' => 'variation_includes', + 'products' => 'product_includes', ); /** @@ -382,6 +383,15 @@ class Controller extends ReportsController implements ExportableInterface { 'sanitize_callback' => 'wp_validate_boolean', 'validate_callback' => 'rest_validate_request_arg', ); + $params['products'] = array( + 'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'type' => 'integer', + ), + ); return $params; } diff --git a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php index 25f1626d6dc..c46decb0f7f 100644 --- a/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php +++ b/plugins/woocommerce/src/Admin/Features/MarketingRecommendations/DefaultMarketingRecommendations.php @@ -56,7 +56,7 @@ class DefaultMarketingRecommendations { return array( array( - 'title' => 'Google Listings and Ads', + 'title' => 'Google for WooCommerce', 'description' => __( 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', 'woocommerce' ), 'url' => "https://woocommerce.com/products/google-listings-and-ads/{$utm_string}", 'direct_install' => true, diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php index 432bd4e20e4..335d8a37ed9 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/CustomizeStore.php @@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use Jetpack_Gutenberg; +use WP_Post; /** * Customize Your Store Task @@ -24,23 +25,24 @@ class CustomizeStore extends Task { // Hook to remove unwanted UI elements when users are viewing with ?cys-hide-admin-bar=true. add_action( 'wp_head', array( $this, 'possibly_remove_unwanted_ui_elements' ) ); - add_action( 'save_post_wp_global_styles', array( $this, 'mark_task_as_complete' ), 10, 3 ); - add_action( 'save_post_wp_template', array( $this, 'mark_task_as_complete' ), 10, 3 ); - add_action( 'save_post_wp_template_part', array( $this, 'mark_task_as_complete' ), 10, 3 ); + add_action( 'save_post_wp_global_styles', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 ); + add_action( 'save_post_wp_template', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 ); + add_action( 'save_post_wp_template_part', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 ); + add_action( 'customize_save_after', array( $this, 'mark_task_as_complete_classic_theme' ) ); } /** * Mark the CYS task as complete whenever the user updates their global styles. * - * @param int $post_id Post ID. - * @param \WP_Post $post Post object. - * @param bool $update Whether this is an existing post being updated. + * @param int $post_id Post ID. + * @param WP_Post $post Post object. + * @param bool $update Whether this is an existing post being updated. * * @return void */ - public function mark_task_as_complete( $post_id, $post, $update ) { - if ( $post instanceof \WP_Post ) { - $is_cys_complete = '{"version": 2, "isGlobalStylesUserThemeJSON": true }' !== $post->post_content || in_array( $post->post_type, array( 'wp_template', 'wp_template_part' ), true ); + public function mark_task_as_complete_block_theme( $post_id, $post, $update ) { + if ( $post instanceof WP_Post ) { + $is_cys_complete = $this->has_custom_global_styles( $post ) || $this->has_custom_template( $post ); if ( $is_cys_complete ) { update_option( 'woocommerce_admin_customize_store_completed', 'yes' ); @@ -48,6 +50,15 @@ class CustomizeStore extends Task { } } + /** + * Mark the CYS task as complete whenever the user saves the customizer changes. + * + * @return void + */ + public function mark_task_as_complete_classic_theme() { + update_option( 'woocommerce_admin_customize_store_completed', 'yes' ); + } + /** * ID. * @@ -260,4 +271,33 @@ class CustomizeStore extends Task { '; } } + + /** + * Checks if the post has custom global styles stored (if it is different from the default global styles). + * + * @param WP_Post $post The post object. + * @return bool + */ + private function has_custom_global_styles( WP_Post $post ) { + $required_keys = array( 'version', 'isGlobalStylesUserThemeJSON' ); + + $json_post_content = json_decode( $post->post_content, true ); + if ( is_null( $json_post_content ) ) { + return false; + } + + $post_content_keys = array_keys( $json_post_content ); + + return ! empty( array_diff( $post_content_keys, $required_keys ) ) || ! empty( array_diff( $required_keys, $post_content_keys ) ); + } + + /** + * Checks if the post is a template or a template part. + * + * @param WP_Post $post The post object. + * @return bool Whether the post is a template or a template part. + */ + private function has_custom_template( WP_Post $post ) { + return in_array( $post->post_type, array( 'wp_template', 'wp_template_part' ), true ); + } } diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php index 001f3ae30dd..d80271c985d 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php @@ -65,6 +65,13 @@ class DefaultPaymentGateways { 'CA', ) ), + (object) array( + 'type' => 'or', + 'operands' => array( + self::get_rules_for_wcpay_activated( false ), + self::get_rules_for_wcpay_connected( false ), + ), + ), ), 'category_other' => array(), 'category_additional' => array( @@ -87,6 +94,13 @@ class DefaultPaymentGateways { 'AU', ) ), + (object) array( + 'type' => 'or', + 'operands' => array( + self::get_rules_for_wcpay_activated( false ), + self::get_rules_for_wcpay_connected( false ), + ), + ), ), 'category_other' => array(), 'category_additional' => array( @@ -250,6 +264,19 @@ class DefaultPaymentGateways { ) ), self::get_rules_for_cbd( false ), + (object) array( + 'type' => 'or', + 'operands' => array( + (object) array( + 'type' => 'not', + 'operand' => array( + self::get_rules_for_countries( self::get_wcpay_countries() ), + ), + ), + self::get_rules_for_wcpay_activated( false ), + self::get_rules_for_wcpay_connected( false ), + ), + ), ), 'category_other' => array(), 'category_additional' => array( @@ -862,6 +889,47 @@ class DefaultPaymentGateways { ), ), ), + array( + 'id' => 'woocommerce_payments:bnpl', + 'title' => __( 'Activate BNPL instantly on WooPayments', 'woocommerce' ), + 'content' => __( + 'The world’s favorite buy now, pay later options and many more are right at your fingertips with WooPayments — all from one dashboard, without needing multiple extensions and logins.', + 'woocommerce' + ), + 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay-bnpl.svg', + 'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay-bnpl.svg', + 'plugins' => array( 'woocommerce-payments' ), + 'is_visible' => array( + self::get_rules_for_countries( + array_intersect( + array( + 'US', + 'CA', + 'AU', + 'AT', + 'BE', + 'CH', + 'DK', + 'ES', + 'FI', + 'FR', + 'DE', + 'GB', + 'IT', + 'NL', + 'NO', + 'PL', + 'SE', + 'NZ', + ), + self::get_wcpay_countries() + ), + ), + self::get_rules_for_cbd( false ), + self::get_rules_for_wcpay_activated( true ), + self::get_rules_for_wcpay_connected( true ), + ), + ), array( 'id' => 'zipmoney', 'title' => __( 'Zip Co - Buy Now, Pay Later', 'woocommerce' ), @@ -976,7 +1044,7 @@ class DefaultPaymentGateways { * Get default rules for CBD based on given argument. * * @param bool $should_have Whether or not the store should have CBD as an industry (true) or not (false). - * @return array Rules to match. + * @return object Rules to match. */ public static function get_rules_for_cbd( $should_have ) { return (object) array( @@ -1002,6 +1070,62 @@ class DefaultPaymentGateways { ); } + /** + * Get default rules for the WooPayments plugin being installed and activated. + * + * @param bool $should_be Whether WooPayments should be activated. + * + * @return object Rules to match. + */ + public static function get_rules_for_wcpay_activated( $should_be ) { + $active_rule = (object) array( + 'type' => 'plugins_activated', + 'plugins' => array( 'woocommerce-payments' ), + ); + + if ( $should_be ) { + return $active_rule; + } + + return (object) array( + 'type' => 'not', + 'operand' => array( $active_rule ), + ); + } + + /** + * Get default rules for WooPayments being connected or not. + * + * This does not include the check for the WooPayments plugin to be active. + * + * @param bool $should_be Whether WooPayments should be connected. + * + * @return object Rules to match. + */ + public static function get_rules_for_wcpay_connected( $should_be ) { + return (object) array( + 'type' => 'option', + 'transformers' => array( + // Extract only the 'data' key from the option. + (object) array( + 'use' => 'dot_notation', + 'arguments' => (object) array( + 'path' => 'data', + ), + ), + // Extract the keys from the data array. + (object) array( + 'use' => 'array_keys', + ), + ), + 'option_name' => 'wcpay_account_data', + // The rule will be look for the 'account_id' key in the account data array. + 'operation' => $should_be ? 'contains' : '!contains', + 'value' => 'account_id', + 'default' => array(), + ); + } + /** * Get recommendation priority for a given payment gateway by id and country. * If country is not supported, return null. @@ -1013,6 +1137,8 @@ class DefaultPaymentGateways { private static function get_recommendation_priority( $gateway_id, $country_code ) { $recommendation_priority_map = array( 'US' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1023,6 +1149,8 @@ class DefaultPaymentGateways { 'klarna_payments', ), 'CA' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1032,6 +1160,8 @@ class DefaultPaymentGateways { 'klarna_payments', ), 'AT' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1041,6 +1171,8 @@ class DefaultPaymentGateways { 'amazon_payments_advanced', ), 'BE' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1049,26 +1181,63 @@ class DefaultPaymentGateways { 'klarna_payments', 'amazon_payments_advanced', ), - 'BG' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), - 'HR' => array( 'woocommerce_payments', 'ppcp-gateway' ), + 'BG' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), + 'HR' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'ppcp-gateway', + ), 'CH' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'mollie_wc_gateway_banktransfer', 'klarna_payments', ), - 'CY' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ), - 'CZ' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), + 'CY' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'amazon_payments_advanced', + ), + 'CZ' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), 'DK' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'klarna_payments', 'amazon_payments_advanced', ), - 'EE' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main' ), + 'EE' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'airwallex_main', + ), 'ES' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1078,6 +1247,8 @@ class DefaultPaymentGateways { 'amazon_payments_advanced', ), 'FI' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1086,6 +1257,8 @@ class DefaultPaymentGateways { 'klarna_payments', ), 'FR' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1096,6 +1269,8 @@ class DefaultPaymentGateways { 'amazon_payments_advanced', ), 'DE' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1105,6 +1280,8 @@ class DefaultPaymentGateways { 'amazon_payments_advanced', ), 'GB' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1114,9 +1291,25 @@ class DefaultPaymentGateways { 'klarna_payments', 'amazon_payments_advanced', ), - 'GR' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main' ), - 'HU' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ), + 'GR' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'airwallex_main', + ), + 'HU' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'amazon_payments_advanced', + ), 'IE' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1125,6 +1318,8 @@ class DefaultPaymentGateways { 'amazon_payments_advanced', ), 'IT' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1133,11 +1328,38 @@ class DefaultPaymentGateways { 'klarna_payments', 'amazon_payments_advanced', ), - 'LV' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), - 'LT' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), - 'LU' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ), - 'MT' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), + 'LV' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), + 'LT' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), + 'LU' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'amazon_payments_advanced', + ), + 'MT' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), 'NL' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1146,8 +1368,18 @@ class DefaultPaymentGateways { 'klarna_payments', 'amazon_payments_advanced', ), - 'NO' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'kco', 'klarna_payments' ), + 'NO' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'kco', + 'klarna_payments', + ), 'PL' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1156,16 +1388,39 @@ class DefaultPaymentGateways { 'klarna_payments', ), 'PT' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main', 'amazon_payments_advanced', ), - 'RO' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), - 'SK' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ), - 'SL' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ), + 'RO' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), + 'SK' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + ), + 'SL' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'ppcp-gateway', + 'amazon_payments_advanced', + ), 'SE' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', @@ -1194,6 +1449,8 @@ class DefaultPaymentGateways { 'UY' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ), 'VE' => array( 'ppcp-gateway' ), 'AU' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'airwallex_main', @@ -1203,6 +1460,8 @@ class DefaultPaymentGateways { 'klarna_payments', ), 'NZ' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'airwallex_main', @@ -1210,6 +1469,8 @@ class DefaultPaymentGateways { 'klarna_payments', ), 'HK' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'airwallex_main', @@ -1217,13 +1478,22 @@ class DefaultPaymentGateways { 'payoneer-checkout', ), 'JP' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'square_credit_card', 'amazon_payments_advanced', ), - 'SG' => array( 'woocommerce_payments', 'stripe', 'airwallex_main', 'ppcp-gateway' ), + 'SG' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + 'stripe', + 'airwallex_main', + 'ppcp-gateway', + ), 'CN' => array( 'airwallex_main', 'ppcp-gateway', 'payoneer-checkout' ), 'FJ' => array(), 'GU' => array(), @@ -1232,7 +1502,11 @@ class DefaultPaymentGateways { 'ZA' => array( 'payfast', 'paystack' ), 'NG' => array( 'paystack' ), 'GH' => array( 'paystack' ), - 'AE' => array( 'woocommerce_payments' ), + 'AE' => array( + 'woocommerce_payments:with-in-person-payments', + 'woocommerce_payments:without-in-person-payments', + 'woocommerce_payments', + ), ); // If the country code is not in the list, return default priority. diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php index 662bbf5631c..45d609da940 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php @@ -16,15 +16,33 @@ class EvaluateSuggestion { /** * Evaluates the spec and returns the suggestion. * - * @param object|array $spec The suggestion to evaluate. + * @param object|array $spec The suggestion to evaluate. + * @param array $logger_args Optional. Arguments for the rule evaluator logger. + * * @return object The evaluated suggestion. */ - public static function evaluate( $spec ) { + public static function evaluate( $spec, $logger_args = array() ) { $rule_evaluator = new RuleEvaluator(); $suggestion = is_array( $spec ) ? (object) $spec : clone $spec; if ( isset( $suggestion->is_visible ) ) { - $is_visible = $rule_evaluator->evaluate( $suggestion->is_visible ); + // Determine the suggestion's logger slug. + $logger_slug = ! empty( $suggestion->id ) ? $suggestion->id : ''; + // If the suggestion has no ID, use the title to generate a slug. + if ( empty( $logger_slug ) ) { + $logger_slug = ! empty( $suggestion->title ) ? sanitize_title_with_dashes( trim( $suggestion->title ) ) : 'anonymous-suggestion'; + } + + // Evaluate the visibility of the suggestion. + $is_visible = $rule_evaluator->evaluate( + $suggestion->is_visible, + null, + array( + 'slug' => $logger_slug, + 'source' => $logger_args['source'] ?? 'wc-payment-gateway-suggestions', + ) + ); + $suggestion->is_visible = $is_visible; } @@ -35,15 +53,17 @@ class EvaluateSuggestion { * Evaluates the specs and returns the visible suggestions. * * @param array $specs payment suggestion spec array. + * @param array $logger_args Optional. Arguments for the rule evaluator logger. + * * @return array The visible suggestions and errors. */ - public static function evaluate_specs( $specs ) { + public static function evaluate_specs( $specs, $logger_args = array() ) { $suggestions = array(); $errors = array(); foreach ( $specs as $spec ) { try { - $suggestion = self::evaluate( $spec ); + $suggestion = self::evaluate( $spec, $logger_args ); if ( ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible ) { $suggestions[] = $suggestion; } diff --git a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php index d9562e9a3e2..d31f8cf0e04 100644 --- a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php +++ b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php @@ -127,7 +127,7 @@ class Init { $editor_settings = $this->get_product_editor_settings(); $script_handle = 'wc-admin-edit-product'; - wp_register_script( $script_handle, '', array(), '0.1.0', true ); + wp_register_script( $script_handle, '', array( 'wp-blocks' ), '0.1.0', true ); wp_enqueue_script( $script_handle ); wp_add_inline_script( $script_handle, diff --git a/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php b/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php index 19c2893252e..d309feddb17 100644 --- a/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php +++ b/plugins/woocommerce/src/Admin/Features/ShippingPartnerSuggestions/ShippingPartnerSuggestions.php @@ -20,14 +20,14 @@ class ShippingPartnerSuggestions extends RemoteSpecsEngine { $locale = get_user_locale(); $specs = is_array( $specs ) ? $specs : self::get_specs(); - $results = EvaluateSuggestion::evaluate_specs( $specs ); + $results = EvaluateSuggestion::evaluate_specs( $specs, array( 'source' => 'wc-shipping-partner-suggestions' ) ); $specs_to_return = $results['suggestions']; $specs_to_save = null; if ( empty( $specs_to_return ) ) { // When suggestions is empty, replace it with defaults and save for 3 hours. $specs_to_save = DefaultShippingPartners::get_all(); - $specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save )['suggestions']; + $specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save, array( 'source' => 'wc-shipping-partner-suggestions' ) )['suggestions']; } elseif ( count( $results['errors'] ) > 0 ) { // When suggestions is not empty but has errors, save it for 3 hours. $specs_to_save = $specs; diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index 5d75c51dcf1..238d1cde650 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -43,6 +43,11 @@ class PluginsHelper { */ const WOO_SUBSCRIPTION_PAGE_URL = 'https://woocommerce.com/my-account/my-subscriptions/'; + /** + * The URL for the WooCommerce.com add payment method page. + */ + const WOO_ADD_PAYMENT_METHOD_URL = 'https://woocommerce.com/my-account/add-payment-method/'; + /** * Meta key for dismissing expired subscription notices. */ @@ -706,7 +711,7 @@ class PluginsHelper { } /** - * Construct the subscritpion notice data based on user subscriptions data. + * Construct the subscription notice data based on user subscriptions data. * * @param array $all_subs all subscription data. * @param array $subs_to_show filtered subscriptions as condition. @@ -717,10 +722,19 @@ class PluginsHelper { */ public static function get_subscriptions_notice_data( array $all_subs, array $subs_to_show, int $total, array $messages, string $type ) { if ( 1 < $total ) { + $hyperlink_url = add_query_arg( + array( + 'utm_source' => 'pu', + 'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew', + + ), + self::WOO_SUBSCRIPTION_PAGE_URL + ); + $parsed_message = sprintf( $messages['different_subscriptions'], esc_attr( $total ), - esc_url( self::WOO_SUBSCRIPTION_PAGE_URL ), + esc_url( $hyperlink_url ), esc_attr( $total ), ); @@ -752,8 +766,11 @@ class PluginsHelper { $expiry_date = date_i18n( 'F jS', $subscription['expires'] ); $hyperlink_url = add_query_arg( array( - 'product_id' => $product_id, - 'type' => $type, + 'product_id' => $product_id, + 'type' => $type, + 'utm_source' => 'pu', + 'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew', + ), self::WOO_SUBSCRIPTION_PAGE_URL ); @@ -810,6 +827,7 @@ class PluginsHelper { $subscriptions, function ( $sub ) { return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) ) + && $sub['active'] && $sub['expiring'] && ! $sub['autorenew']; }, @@ -824,29 +842,7 @@ class PluginsHelper { // When payment method is missing on WooCommerce.com. $helper_notices = WC_Helper::get_notices(); if ( ! empty( $helper_notices['missing_payment_method_notice'] ) ) { - $description = $allowed_link - ? sprintf( - /* translators: %s: WooCommerce.com URL to add payment method */ - _n( - 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', - 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', - $total_expiring_subscriptions, - 'woocommerce' - ), - 'https://woocommerce.com/my-account/add-payment-method/' - ) - : _n( - 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', - 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', - $total_expiring_subscriptions, - 'woocommerce' - ); - - return array( - 'description' => $description, - 'button_text' => __( 'Add payment method', 'woocommerce' ), - 'button_link' => 'https://woocommerce.com/my-account/add-payment-method/', - ); + return self::get_missing_payment_method_notice( $allowed_link, $total_expiring_subscriptions ); } // Payment method is available but there are expiring subscriptions. @@ -865,14 +861,20 @@ class PluginsHelper { 'expiring', ); - $button_link = self::WOO_SUBSCRIPTION_PAGE_URL; + $button_link = add_query_arg( + array( + 'utm_source' => 'pu', + 'utm_campaign' => 'pu_in_apps_screen_enable_autorenew', + ), + self::WOO_SUBSCRIPTION_PAGE_URL + ); if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) { $button_link = add_query_arg( array( 'product_id' => $notice_data['product_id'], 'type' => 'expiring', ), - self::WOO_SUBSCRIPTION_PAGE_URL + $button_link ); } @@ -903,6 +905,7 @@ class PluginsHelper { $subscriptions, function ( $sub ) { return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) ) + && $sub['active'] && $sub['expired'] && ! $sub['lifetime']; }, @@ -923,21 +926,28 @@ class PluginsHelper { /* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */ 'single_manage' => __( 'Your subscription for %1$s expired. %4$s to continue receiving updates and streamlined support.', 'woocommerce' ), /* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */ - 'multiple_manage' => __( 'One of your subscriptions for %1$s has expired. %4$s. to continue receiving updates and streamlined support.', 'woocommerce' ), + 'multiple_manage' => __( 'One of your subscriptions for %1$s has expired. %4$s to continue receiving updates and streamlined support.', 'woocommerce' ), /* translators: 1) total expired subscriptions 2) URL to My Subscriptions page */ 'different_subscriptions' => __( 'You have %1$s Woo extension subscriptions that expired. Renew to continue receiving updates and streamlined support.', 'woocommerce' ), ), 'expired', ); - $button_link = self::WOO_SUBSCRIPTION_PAGE_URL; + $button_link = add_query_arg( + array( + 'utm_source' => 'pu', + 'utm_campaign' => $allowed_link ? 'pu_settings_screen_renew' : 'pu_in_apps_screen_renew', + ), + self::WOO_SUBSCRIPTION_PAGE_URL + ); + if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) { $button_link = add_query_arg( array( 'product_id' => $notice_data['product_id'], 'type' => 'expiring', ), - self::WOO_SUBSCRIPTION_PAGE_URL + $button_link ); } @@ -973,4 +983,45 @@ class PluginsHelper { return true; } + + /** + * Get the notice data for missing payment method. + * + * @param bool $allowed_link whether should show link on the notice or not. + * @param int $total_expiring_subscriptions total expiring subscriptions. + * + * @return array the notices data. + */ + public static function get_missing_payment_method_notice( $allowed_link = true, $total_expiring_subscriptions = 1 ) { + $add_payment_method_link = add_query_arg( + array( + 'utm_source' => 'pu', + 'utm_campaign' => $allowed_link ? 'pu_settings_screen_add_payment_method' : 'pu_in_apps_screen_add_payment_method', + ), + self::WOO_ADD_PAYMENT_METHOD_URL + ); + $description = $allowed_link + ? sprintf( + /* translators: %s: WooCommerce.com URL to add payment method */ + _n( + 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', + 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', + $total_expiring_subscriptions, + 'woocommerce' + ), + $add_payment_method_link + ) + : _n( + 'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', + 'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.', + $total_expiring_subscriptions, + 'woocommerce' + ); + + return array( + 'description' => $description, + 'button_text' => __( 'Add payment method', 'woocommerce' ), + 'button_link' => $add_payment_method_link, + ); + } } diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php index 8555ff9ba18..595a7788053 100644 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php +++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php @@ -7,11 +7,14 @@ namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications; defined( 'ABSPATH' ) || exit; +use Automattic\WooCommerce\Admin\Features\Features; +use Automattic\WooCommerce\Admin\Notes\Notes; use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider; use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile; use Automattic\WooCommerce\Admin\Notes\Note; use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine; use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForProducts; +use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; /** * Remote Inbox Notifications engine. @@ -19,11 +22,15 @@ use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForP * specs that are able to be triggered. */ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { + use AccessiblePrivateMethods; + const STORED_STATE_OPTION_NAME = 'wc_remote_inbox_notifications_stored_state'; const WCA_UPDATED_OPTION_NAME = 'wc_remote_inbox_notifications_wca_updated'; /** * Initialize the engine. + * phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingFinal + * phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag */ public static function init() { // Init things that need to happen before admin_init. @@ -42,10 +49,13 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { // Hook into WCA updated. This is hooked up here rather than in // on_admin_init because that runs too late to hook into the action. - add_action( 'woocommerce_run_on_woocommerce_admin_updated', array( __CLASS__, 'run_on_woocommerce_admin_updated' ) ); + add_action( + 'woocommerce_run_on_woocommerce_admin_updated', + array( __CLASS__, 'run_on_woocommerce_admin_updated' ) + ); add_action( 'woocommerce_updated', - function() { + function () { $next_hook = WC()->queue()->get_next( 'woocommerce_run_on_woocommerce_admin_updated', array(), @@ -63,6 +73,11 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { ); add_filter( 'woocommerce_get_note_from_db', array( __CLASS__, 'get_note_from_db' ), 10, 1 ); + self::add_filter( 'woocommerce_debug_tools', array( __CLASS__, 'add_debug_tools' ) ); + self::add_action( + 'wp_ajax_woocommerce_json_inbox_notifications_search', + array( __CLASS__, 'ajax_action_inbox_notification_search' ) + ); } /** @@ -155,7 +170,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { public static function get_stored_state() { $stored_state = get_option( self::STORED_STATE_OPTION_NAME ); - if ( $stored_state === false ) { + if ( false === $stored_state ) { $stored_state = new \stdClass(); $stored_state = StoredStateSetupForProducts::init_stored_state( @@ -199,6 +214,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { * Get the note. This is used to display localized note. * * @param Note $note_from_db The note object created from db. + * * @return Note The note. */ public static function get_note_from_db( $note_from_db ) { @@ -211,7 +227,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { continue; } $locale = SpecRunner::get_locale( $spec->locales, true ); - if ( $locale === null ) { + if ( null === $locale ) { // No locale found, so don't update the note. break; } @@ -233,4 +249,94 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine { return $note_from_db; } + + /** + * Add the debug tools to the WooCommerce debug tools (WooCommerce > Status > Tools). + * + * @param array $tools a list of tools. + * + * @return mixed + */ + private static function add_debug_tools( $tools ) { + // Check if the feature flag is disabled. + if ( ! Features::is_enabled( 'remote-inbox-notifications' ) ) { + return false; + } + + // Check if the site has opted out of marketplace suggestions. + if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) !== 'yes' ) { + return false; + } + + $tools['refresh_remote_inbox_notifications'] = array( + 'name' => __( 'Refresh Remote Inbox Notifications', 'woocommerce' ), + 'button' => __( 'Refresh', 'woocommerce' ), + 'desc' => __( 'This will refresh the remote inbox notifications', 'woocommerce' ), + 'callback' => function () { + RemoteInboxNotificationsDataSourcePoller::get_instance()->read_specs_from_data_sources(); + RemoteInboxNotificationsEngine::run(); + + return __( 'Remote inbox notifications have been refreshed', 'woocommerce' ); + }, + ); + + $tools['delete_inbox_notification'] = array( + 'name' => __( 'Delete an Inbox Notification', 'woocommerce' ), + 'button' => __( 'Delete', 'woocommerce' ), + 'desc' => __( 'This will delete an inbox notification by slug', 'woocommerce' ), + 'selector' => array( + 'description' => __( 'Select an inbox notification to delete:', 'woocommerce' ), + 'class' => 'wc-product-search', + 'search_action' => 'woocommerce_json_inbox_notifications_search', + 'name' => 'delete_inbox_notification_note_id', + 'placeholder' => esc_attr__( 'Search for an inbox notification…', 'woocommerce' ), + ), + 'callback' => function () { + check_ajax_referer( 'debug_action', '_wpnonce' ); + + if ( ! isset( $_GET['delete_inbox_notification_note_id'] ) ) { + return __( 'No inbox notification selected', 'woocommerce' ); + } + $note_id = wc_clean( sanitize_text_field( wp_unslash( $_GET['delete_inbox_notification_note_id'] ) ) ); + $note = Notes::get_note( $note_id ); + + if ( ! $note ) { + return __( 'Inbox notification not found', 'woocommerce' ); + } + + $note->delete( true ); + return __( 'Inbox notification has been deleted', 'woocommerce' ); + }, + ); + + return $tools; + } + + /** + * Add ajax action for remote inbox notification search. + * + * @return void + */ + private static function ajax_action_inbox_notification_search() { + global $wpdb; + + check_ajax_referer( 'search-products', 'security' ); + + if ( ! isset( $_GET['term'] ) ) { + wp_send_json( array() ); + } + + $search = wc_clean( sanitize_text_field( wp_unslash( $_GET['term'] ) ) ); + $results = $wpdb->get_results( + $wpdb->prepare( + "SELECT note_id, name FROM {$wpdb->prefix}wc_admin_notes WHERE name LIKE %s", + '%' . $wpdb->esc_like( $search ) . '%' + ) + ); + $rows = array(); + foreach ( $results as $result ) { + $rows[ $result->note_id ] = $result->name; + } + wp_send_json( $rows ); + } } diff --git a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php index e2cf71eee5d..89010ca6960 100644 --- a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php +++ b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/EvaluationLogger.php @@ -25,23 +25,23 @@ class EvaluationLogger { /** * Logger class to use. * - * @var WC_Logger_Interface|null + * @var \WC_Logger_Interface|null */ private $logger; /** * Logger source. * - * @var string logger source. + * @var string Logger source. */ private $source = ''; /** * EvaluationLogger constructor. * - * @param string $slug Slug of a spec that is being evaluated. - * @param null $source Logger source. - * @param \WC_Logger_Interface $logger Logger class to use. + * @param string $slug Slug/ID of a spec that is being evaluated. + * @param string|null $source Logger source. + * @param \WC_Logger_Interface|null $logger Logger class to use. Default to using the WC logger. */ public function __construct( $slug, $source = null, \WC_Logger_Interface $logger = null ) { $this->slug = $slug; @@ -59,16 +59,13 @@ class EvaluationLogger { /** * Add evaluation result of a rule. * - * @param string $rule_type name of the rule being tested. - * @param boolean $result result of a given rule. + * @param string $rule_type Name of the rule being tested. + * @param boolean $result Result of a given rule. */ public function add_result( $rule_type, $result ) { - array_push( - $this->results, - array( - 'rule' => $rule_type, - 'result' => $result ? 'passed' : 'failed', - ) + $this->results[] = array( + 'rule' => $rule_type, + 'result' => $result ? 'passed' : 'failed', ); } @@ -76,7 +73,16 @@ class EvaluationLogger { * Log the results. */ public function log() { - if ( false === defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) || true !== constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) ) { + $should_log = defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) && true === constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ); + + /** + * Filter to determine if the rule evaluator should log the results. + * + * @since 9.2.0 + * + * @param bool $should_log Whether the rule evaluator should log the results. + */ + if ( ! apply_filters( 'woocommerce_admin_remote_specs_evaluator_should_log', $should_log ) ) { return; } diff --git a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php index a4f3977d9b8..0604305c368 100644 --- a/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php +++ b/plugins/woocommerce/src/Admin/RemoteSpecs/RuleProcessors/RuleEvaluator.php @@ -36,9 +36,9 @@ class RuleEvaluator { * Evaluate the given rules as an AND operation - return false early if a * rule evaluates to false. * - * @param array|object $rules The rule or rules being processed. + * @param array|object $rules The rule or rules being processed. * @param object|null $stored_state Stored state. - * @param array $logger_args Arguments for the event logger. `slug` is required. + * @param array $logger_args Arguments for the rule evaluator logger. `slug` is required. * * @throws \InvalidArgumentException Thrown when $logger_args is missing slug. * @@ -65,13 +65,16 @@ class RuleEvaluator { throw new \InvalidArgumentException( 'Missing required field: slug in $logger_args.' ); } - array_key_exists( 'source', $logger_args ) ? $source = $logger_args['source'] : $source = null; + $source = isset( $logger_args['source'] ) ? $logger_args['source'] : null; $evaluation_logger = new EvaluationLogger( $logger_args['slug'], $source ); } foreach ( $rules as $rule ) { if ( ! is_object( $rule ) ) { + $evaluation_logger && $evaluation_logger->add_result( 'rule not an object', false ); + $evaluation_logger && $evaluation_logger->log(); + return false; } diff --git a/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php b/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php new file mode 100644 index 00000000000..cb87c48b8ea --- /dev/null +++ b/plugins/woocommerce/src/Blocks/AIContent/PatternsDictionary.php @@ -0,0 +1,673 @@ + 'Banner', + 'slug' => 'woocommerce-blocks/banner', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Up to 60% off', 'woocommerce' ), + 'ai_prompt' => __( 'A four words title advertising the sale', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Holiday Sale', 'woocommerce' ), + 'ai_prompt' => __( 'A two words label with the sale name', 'woocommerce' ), + ], + [ + 'default' => __( 'Get your favorite vinyl at record-breaking prices.', 'woocommerce' ), + 'ai_prompt' => __( 'The main description of the sale with at least 65 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop vinyl records', 'woocommerce' ), + 'ai_prompt' => __( 'A 3 words button text to go to the sale page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Discount Banner', + 'slug' => 'woocommerce-blocks/discount-banner', + 'content' => [ + 'descriptions' => [ + [ + 'default' => __( 'Select products', 'woocommerce' ), + 'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Discount Banner with Image', + 'slug' => 'woocommerce-blocks/discount-banner-with-image', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'descriptions' => [ + [ + 'default' => __( 'Select products', 'woocommerce' ), + 'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Featured Category Focus', + 'slug' => 'woocommerce-blocks/featured-category-focus', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Black and white high-quality prints', 'woocommerce' ), + 'ai_prompt' => __( 'The four words title of the featured category related to the following image description: [image.0]', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop prints', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the featured category', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Featured Category Triple', + 'slug' => 'woocommerce-blocks/featured-category-triple', + 'images_total' => 3, + 'images_format' => 'portrait', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Home decor', 'woocommerce' ), + 'ai_prompt' => __( 'A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: [image.0] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ), + ], + [ + 'default' => __( 'Retro photography', 'woocommerce' ), + 'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.1] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ), + ], + [ + 'default' => __( 'Handmade gifts', 'woocommerce' ), + 'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.2] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Featured Products: Fresh & Tasty', + 'slug' => 'woocommerce-blocks/featured-products-fresh-and-tasty', + 'images_total' => 4, + 'images_format' => 'portrait', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Fresh & tasty goods', 'woocommerce' ), + 'ai_prompt' => __( 'The title of the featured products with at least 20 characters', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Sweet Organic Lemons', 'woocommerce' ), + 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.0]', 'woocommerce' ), + ], + [ + 'default' => __( 'Fresh Organic Tomatoes', 'woocommerce' ), + 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.1]', 'woocommerce' ), + ], + [ + 'default' => __( 'Fresh Lettuce (Washed)', 'woocommerce' ), + 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.2]', 'woocommerce' ), + ], + [ + 'default' => __( 'Russet Organic Potatoes', 'woocommerce' ), + 'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.3]', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Hero Product 3 Split', + 'slug' => 'woocommerce-blocks/hero-product-3-split', + 'images_total' => 1, + 'images_format' => 'portrait', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Timeless elegance', 'woocommerce' ), + 'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ), + ], + [ + 'default' => __( 'Durable glass', 'woocommerce' ), + 'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ), + ], + [ + 'default' => __( 'Versatile charm', 'woocommerce' ), + 'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ), + ], + [ + 'default' => __( 'New: Retro Glass Jug', 'woocommerce' ), + 'ai_prompt' => __( 'Write a title with less than 20 characters for advertising the store', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.', 'woocommerce' ), + 'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ), + ], + [ + 'default' => __( 'Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.', 'woocommerce' ), + 'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ), + ], + [ + 'default' => __( "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.", 'woocommerce' ), + 'ai_prompt' => __( 'Write a long text, with at least 130 characters, to describe a product the business is selling', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Hero Product Chessboard', + 'slug' => 'woocommerce-blocks/hero-product-chessboard', + 'images_total' => 2, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Quality Materials', 'woocommerce' ), + 'ai_prompt' => __( 'A two words title describing the first displayed product feature', 'woocommerce' ), + ], + [ + 'default' => __( 'Unique design', 'woocommerce' ), + 'ai_prompt' => __( 'A two words title describing the second displayed product feature', 'woocommerce' ), + ], + [ + 'default' => __( 'Make your house feel like home', 'woocommerce' ), + 'ai_prompt' => __( 'A two words title describing the fourth displayed product feature', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'From bold prints to intricate details, our products are a perfect combination of style and function.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop home decor', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Hero Product Split', + 'slug' => 'woocommerce-blocks/hero-product-split', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Keep dry with 50% off rain jackets', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the product the store is selling with at least 35 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Just Arrived Full Hero', + 'slug' => 'woocommerce-blocks/just-arrived-full-hero', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Sound like no other', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 10 characters', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Experience your music like never before with our latest generation of hi-fidelity headphones.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the product collection with at least 35 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop now', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collection Banner', + 'slug' => 'woocommerce-blocks/product-collection-banner', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Brand New for the Holidays', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: [image.0]', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Check out our brand new collection of holiday products and find the right gift for anyone.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the product collection with at least 90 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collections Featured Collection', + 'slug' => 'woocommerce-blocks/product-collections-featured-collection', + 'content' => [ + 'titles' => [ + [ + 'default' => "This week's popular products", + 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 30 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collections Featured Collections', + 'slug' => 'woocommerce-blocks/product-collections-featured-collections', + 'images_total' => 4, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Tech gifts under $100', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: [image.0], [image.1]', 'woocommerce' ), + ], + [ + 'default' => __( 'For the gamers', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: [image.2], [image.3]', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop tech', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ), + ], + [ + 'default' => __( 'Shop games', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collections Newest Arrivals', + 'slug' => 'woocommerce-blocks/product-collections-newest-arrivals', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Our newest arrivals', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'More new products', 'woocommerce' ), + 'ai_prompt' => __( 'The button text to go to the product collection page with at least 15 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collection 4 Columns', + 'slug' => 'woocommerce-blocks/product-collection-4-columns', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Staff picks', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collection 5 Columns', + 'slug' => 'woocommerce-blocks/product-collection-5-columns', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Our latest and greatest', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase with that advertises the product collection with at least 20 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Gallery', + 'slug' => 'woocommerce-blocks/product-query-product-gallery', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Bestsellers', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Featured Products 2 Columns', + 'slug' => 'woocommerce-blocks/featured-products-2-cols', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Fan favorites', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Get ready to start the season right. All the fan favorites in one place at the best price.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the featured products with at least 90 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop All', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the featured products page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Hero 2 Column 2 Row', + 'slug' => 'woocommerce-blocks/product-hero-2-col-2-row', + 'images_total' => 2, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'The Eden Jacket', 'woocommerce' ), + 'ai_prompt' => __( 'A three words title that advertises a product related to the following image description: [image.0]', 'woocommerce' ), + ], + [ + 'default' => __( '100% Woolen', 'woocommerce' ), + 'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ), + ], + [ + 'default' => __( 'Fits your wardrobe', 'woocommerce' ), + 'ai_prompt' => __( 'A three words title that advertises a product feature', 'woocommerce' ), + ], + [ + 'default' => __( 'Versatile', 'woocommerce' ), + 'ai_prompt' => __( 'An one word title that advertises a product feature', 'woocommerce' ), + ], + [ + 'default' => __( 'Normal Fit', 'woocommerce' ), + 'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Perfect for any look featuring a mid-rise, relax fitting silhouette.', 'woocommerce' ), + 'ai_prompt' => __( 'The description of a product with at least 65 characters related to the following image: [image.0]', 'woocommerce' ), + ], + [ + 'default' => __( 'Reflect your fashionable style.', 'woocommerce' ), + 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'Half tuck into your pants or layer over.', 'woocommerce' ), + 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'Button-down front for any type of mood or look.', 'woocommerce' ), + 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ), + ], + [ + 'default' => __( '42% Cupro 34% Linen 24% Viscose', 'woocommerce' ), + 'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'View product', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Shop by Price', + 'slug' => 'woocommerce-blocks/shop-by-price', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Outdoor Furniture & Accessories', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the first product collection with at least 30 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'Summer Dinning', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the second product collection with at least 20 characters', 'woocommerce' ), + ], + [ + 'default' => "Women's Styles", + 'ai_prompt' => __( 'An impact phrase that advertises the third product collection with at least 20 characters', 'woocommerce' ), + ], + [ + 'default' => "Kids' Styles", + 'ai_prompt' => __( 'An impact phrase that advertises the fourth product collection with at least 20 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Small Discount Banner with Image', + 'slug' => 'woocommerce-blocks/small-discount-banner-with-image', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Chairs', 'woocommerce' ), + 'ai_prompt' => __( 'A single word that advertises the product and is related to the following image description: [image.0]', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Social: Follow us on social media', + 'slug' => 'woocommerce-blocks/social-follow-us-in-social-media', + 'images_total' => 4, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Stay in the loop', 'woocommerce' ), + 'ai_prompt' => __( 'A phrase that advertises the social media accounts of the store with at least 25 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Alternating Image and Text', + 'slug' => 'woocommerce-blocks/alt-image-and-text', + 'images_total' => 2, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Our products', 'woocommerce' ), + 'ai_prompt' => __( 'A two words impact phrase that advertises the products', 'woocommerce' ), + ], + [ + 'default' => __( 'Sustainable blends, stylish accessories', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the products with at least 40 characters and related to the following image description: [image.0]', 'woocommerce' ), + ], + [ + 'default' => __( 'About us', 'woocommerce' ), + 'ai_prompt' => __( 'A two words impact phrase that advertises the brand', 'woocommerce' ), + ], + [ + 'default' => __( 'Committed to a greener lifestyle', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the brand with at least 50 characters related to the following image description: [image.1]', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ), + ], + [ + 'default' => "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.", + 'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'Locally sourced ingredients', 'woocommerce' ), + 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ), + ], + [ + 'default' => __( 'Premium organic blends', 'woocommerce' ), + 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ), + ], + [ + 'default' => __( 'Hand-picked accessories', 'woocommerce' ), + 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ), + ], + [ + 'default' => __( 'Sustainable business practices', 'woocommerce' ), + 'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Meet us', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Testimonials 3 Columns', + 'slug' => 'woocommerce-blocks/testimonials-3-columns', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Eclectic finds, ethical delights', 'woocommerce' ), + 'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ), + ], + [ + 'default' => __( 'Sip, Shop, Savor', 'woocommerce' ), + 'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ), + ], + [ + 'default' => __( 'LOCAL LOVE', 'woocommerce' ), + 'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ), + ], + [ + 'default' => __( 'What our customers say', 'woocommerce' ), + 'ai_prompt' => __( 'Write just 4 words to advertise testimonials from customers', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.', 'woocommerce' ), + 'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.', 'woocommerce' ), + 'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ), + ], + [ + 'default' => __( 'From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.', 'woocommerce' ), + 'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Testimonials Single', + 'slug' => 'woocommerce-blocks/testimonials-single', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'A ‘brewtiful’ experience :-)', 'woocommerce' ), + 'ai_prompt' => __( 'A two words title that advertises the testimonial', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.', 'woocommerce' ), + 'ai_prompt' => __( 'A description of the testimonial with at least 225 characters', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Featured Category Cover Image', + 'slug' => 'woocommerce-blocks/featured-category-cover-image', + 'images_total' => 1, + 'images_format' => 'landscape', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Sit back and relax', 'woocommerce' ), + 'ai_prompt' => __( 'A description for a product with at least 20 characters', 'woocommerce' ), + ], + ], + 'descriptions' => [ + [ + 'default' => __( 'With a wide range of designer chairs to elevate your living space.', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the products with at least 55 characters', 'woocommerce' ), + ], + ], + 'buttons' => [ + [ + 'default' => __( 'Shop chairs', 'woocommerce' ), + 'ai_prompt' => __( 'A two words button text to go to the shop page', 'woocommerce' ), + ], + ], + ], + ], + [ + 'name' => 'Product Collection: Featured Products 5 Columns', + 'slug' => 'woocommerce-blocks/product-collection-featured-products-5-columns', + 'content' => [ + 'titles' => [ + [ + 'default' => __( 'Shop new arrivals', 'woocommerce' ), + 'ai_prompt' => __( 'An impact phrase that advertises the newest additions to the store with at least 20 characters', 'woocommerce' ), + ], + ], + ], + ], + ]; + } +} diff --git a/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php b/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php index 26d8fb28b44..d6eceadb97e 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php +++ b/plugins/woocommerce/src/Blocks/AIContent/PatternsHelper.php @@ -63,7 +63,7 @@ class PatternsHelper { /** * Upsert the patterns AI data. * - * @param array $patterns_dictionary The patterns dictionary. + * @param array $patterns_dictionary The patterns' dictionary. * * @return WP_Error|null */ @@ -92,18 +92,12 @@ class PatternsHelper { * @return array|WP_Error Returns pattern dictionary or WP_Error on failure. */ public static function get_patterns_dictionary( $pattern_slug = null ) { - $patterns_dictionary_file = plugin_dir_path( __FILE__ ) . 'dictionary.json'; + $default_patterns_dictionary = PatternsDictionary::get(); - if ( ! file_exists( $patterns_dictionary_file ) ) { + if ( empty( $default_patterns_dictionary ) ) { return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) ); } - $default_patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) ); - - if ( json_last_error() !== JSON_ERROR_NONE ) { - return new WP_Error( 'json_decode_error', __( 'Error decoding JSON.', 'woocommerce' ) ); - } - $patterns_dictionary = ''; $ai_connection_allowed = get_option( 'woocommerce_blocks_allow_ai_connection' ); diff --git a/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php b/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php index bc0c90b0b8a..d9e792e85eb 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php +++ b/plugins/woocommerce/src/Blocks/AIContent/UpdatePatterns.php @@ -402,13 +402,13 @@ class UpdatePatterns { * @return mixed|WP_Error|null */ public static function get_patterns_dictionary() { - $patterns_dictionary = plugin_dir_path( __FILE__ ) . 'dictionary.json'; + $patterns_dictionary = PatternsDictionary::get(); - if ( ! file_exists( $patterns_dictionary ) ) { + if ( empty( $patterns_dictionary ) ) { return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) ); } - return wp_json_file_decode( $patterns_dictionary, array( 'associative' => true ) ); + return $patterns_dictionary; } /** diff --git a/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php b/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php index 0a00b68051f..20e0549f8e3 100644 --- a/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php +++ b/plugins/woocommerce/src/Blocks/AIContent/UpdateProducts.php @@ -26,7 +26,7 @@ class UpdateProducts { 'price' => 249, ], [ - 'title' => 'Black and White Summer Portrait', + 'title' => 'Black and White', 'image' => 'assets/images/pattern-placeholders/white-black-black-and-white-photograph-monochrome-photography.jpg', 'description' => 'This 24" x 30" high-quality print just exudes summer. Hang it on the wall and forget about the world outside.', 'price' => 115, @@ -113,7 +113,7 @@ class UpdateProducts { $products_to_create = max( 0, 6 - $real_products_count - $dummy_products_count ); while ( $products_to_create > 0 ) { $this->create_new_product( self::DUMMY_PRODUCTS[ $products_to_create - 1 ] ); - $products_to_create--; + --$products_to_create; } // Identify dummy products that need to have their content updated. @@ -327,7 +327,7 @@ class UpdateProducts { public function assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $ai_selected_images ) { $products_information_list = []; $dummy_products_count = count( $dummy_products_to_update ); - for ( $i = 0; $i < $dummy_products_count; $i ++ ) { + for ( $i = 0; $i < $dummy_products_count; $i++ ) { $image_src = $ai_selected_images[ $i ]['URL'] ?? ''; if ( wc_is_valid_url( $image_src ) ) { @@ -396,7 +396,7 @@ class UpdateProducts { $ai_request_retries = 0; $success = false; while ( $ai_request_retries < 5 && ! $success ) { - $ai_request_retries ++; + ++$ai_request_retries; $ai_response = $ai_connection->fetch_ai_response( $token, $formatted_prompt, 30 ); if ( is_wp_error( $ai_response ) ) { continue; @@ -464,7 +464,7 @@ class UpdateProducts { $this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] ); - $i++; + ++$i; } } diff --git a/plugins/woocommerce/src/Blocks/AIContent/dictionary.json b/plugins/woocommerce/src/Blocks/AIContent/dictionary.json deleted file mode 100644 index 452f8aa2fb6..00000000000 --- a/plugins/woocommerce/src/Blocks/AIContent/dictionary.json +++ /dev/null @@ -1,656 +0,0 @@ -[ - { - "name": "Banner", - "slug": "woocommerce-blocks/banner", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Up to 60% off", - "ai_prompt": "A four words title advertising the sale" - } - ], - "descriptions": [ - { - "default": "Holiday Sale", - "ai_prompt": "A two words label with the sale name" - }, - { - "default": "Get your favorite vinyl at record-breaking prices.", - "ai_prompt": "The main description of the sale with at least 65 characters" - } - ], - "buttons": [ - { - "default": "Shop vinyl records", - "ai_prompt": "A 3 words button text to go to the sale page" - } - ] - } - }, - { - "name": "Discount Banner", - "slug": "woocommerce-blocks/discount-banner", - "content": { - "descriptions": [ - { - "default": "Select products", - "ai_prompt": "A two words description of the products on sale" - } - ] - } - }, - { - "name": "Discount Banner with Image", - "slug": "woocommerce-blocks/discount-banner-with-image", - "images_total": 1, - "images_format": "landscape", - "content": { - "descriptions": [ - { - "default": "Select products", - "ai_prompt": "A two words description of the products on sale" - } - ] - } - }, - { - "name": "Featured Category Focus", - "slug": "woocommerce-blocks/featured-category-focus", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Black and white high-quality prints", - "ai_prompt": "The four words title of the featured category related to the following image description: {image.0}" - } - ], - "buttons": [ - { - "default": "Shop prints", - "ai_prompt": "A two words button text to go to the featured category" - } - ] - } - }, - { - "name": "Featured Category Triple", - "slug": "woocommerce-blocks/featured-category-triple", - "images_total": 3, - "images_format": "portrait", - "content": { - "titles": [ - { - "default": "Home decor", - "ai_prompt": "A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: {image.0} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image" - }, - { - "default": "Retro photography", - "ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.1} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image" - }, - { - "default": "Handmade gifts", - "ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.2} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image" - } - ] - } - }, - { - "name": "Featured Products: Fresh & Tasty", - "slug": "woocommerce-blocks/featured-products-fresh-and-tasty", - "images_total": 4, - "images_format": "portrait", - "content": { - "titles": [ - { - "default": "Fresh & tasty goods", - "ai_prompt": "The title of the featured products with at least 20 characters" - } - ], - "descriptions": [ - { - "default": "Sweet Organic Lemons", - "ai_prompt": "The three words description of the featured product related to the following image description: {image.0}" - }, - { - "default": "Fresh Organic Tomatoes", - "ai_prompt": "The three words description of the featured product related to the following image description: {image.1}" - }, - { - "default": "Fresh Lettuce (Washed)", - "ai_prompt": "The three words description of the featured product related to the following image description: {image.2}" - }, - { - "default": "Russet Organic Potatoes", - "ai_prompt": "The three words description of the featured product related to the following image description: {image.3}" - } - ] - } - }, - { - "name": "Hero Product 3 Split", - "slug": "woocommerce-blocks/hero-product-3-split", - "images_total": 1, - "images_format": "portrait", - "content": { - "titles": [ - { - "default": "Timeless elegance", - "ai_prompt": "Write a two words title for advertising the store" - }, - { - "default": "Durable glass", - "ai_prompt": "Write a two words title for advertising the store" - }, - { - "default": "Versatile charm", - "ai_prompt": "Write a two words title for advertising the store" - }, - { - "default": "New: Retro Glass Jug", - "ai_prompt": "Write a title with less than 20 characters for advertising the store" - } - ], - "descriptions": [ - { - "default": "Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.", - "ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling" - }, - { - "default": "Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.", - "ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling" - }, - { - "default": "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.", - "ai_prompt": "Write a long text, with at least 130 characters, to describe a product the business is selling" - } - ] - } - }, - { - "name": "Hero Product Chessboard", - "slug": "woocommerce-blocks/hero-product-chessboard", - "images_total": 2, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Quality Materials", - "ai_prompt": "A two words title describing the first displayed product feature" - }, - { - "default": "Unique design", - "ai_prompt": "A two words title describing the second displayed product feature" - }, - { - "default": "Make your house feel like home", - "ai_prompt": "A two words title describing the fourth displayed product feature" - } - ], - "descriptions": [ - { - "default": "We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.", - "ai_prompt": "A description of the product feature with at least 115 characters" - }, - { - "default": "From bold prints to intricate details, our products are a perfect combination of style and function.", - "ai_prompt": "A description of the product feature with at least 115 characters" - }, - { - "default": "Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.", - "ai_prompt": "A description of the product feature with at least 115 characters" - } - ], - "buttons": [ - { - "default": "Shop home decor", - "ai_prompt": "A two words button text to go to the product page" - } - ] - } - }, - { - "name": "Hero Product Split", - "slug": "woocommerce-blocks/hero-product-split", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Keep dry with 50% off rain jackets", - "ai_prompt": "An impact phrase that advertises the product the store is selling with at least 35 characters" - } - ] - } - }, - { - "name": "Just Arrived Full Hero", - "slug": "woocommerce-blocks/just-arrived-full-hero", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Sound like no other", - "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 10 characters" - } - ], - "descriptions": [ - { - "default": "Experience your music like never before with our latest generation of hi-fidelity headphones.", - "ai_prompt": "A description of the product collection with at least 35 characters" - } - ], - "buttons": [ - { - "default": "Shop now", - "ai_prompt": "A two words button text to go to the product collection page" - } - ] - } - }, - { - "name": "Product Collection Banner", - "slug": "woocommerce-blocks/product-collection-banner", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Brand New for the Holidays", - "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: {image.0}" - } - ], - "descriptions": [ - { - "default": "Check out our brand new collection of holiday products and find the right gift for anyone.", - "ai_prompt": "A description of the product collection with at least 90 characters" - } - ] - } - }, - { - "name": "Product Collections Featured Collection", - "slug": "woocommerce-blocks/product-collections-featured-collection", - "content": { - "titles": [ - { - "default": "This week's popular products", - "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 30 characters" - } - ] - } - }, - { - "name": "Product Collections Featured Collections", - "slug": "woocommerce-blocks/product-collections-featured-collections", - "images_total": 4, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Tech gifts under $100", - "ai_prompt": "An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: {image.0}, {image.1}" - }, - { - "default": "For the gamers", - "ai_prompt": "An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: {image.2}, {image.3}" - } - ], - "buttons": [ - { - "default": "Shop tech", - "ai_prompt": "A two words button text to go to the product collection page" - }, - { - "default": "Shop games", - "ai_prompt": "A two words button text to go to the product collection page" - } - ] - } - }, - { - "name": "Product Collections Newest Arrivals", - "slug": "woocommerce-blocks/product-collections-newest-arrivals", - "content": { - "titles": [ - { - "default": "Our newest arrivals", - "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters" - } - ], - "buttons": [ - { - "default": "More new products", - "ai_prompt": "The button text to go to the product collection page with at least 15 characters" - } - ] - } - }, - { - "name": "Product Collection 4 Columns", - "slug": "woocommerce-blocks/product-collection-4-columns", - "content": { - "titles": [ - { - "default": "Staff picks", - "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters" - } - ] - } - }, - { - "name": "Product Collection 5 Columns", - "slug": "woocommerce-blocks/product-collection-5-columns", - "content": { - "titles": [ - { - "default": "Our latest and greatest", - "ai_prompt": "An impact phrase with that advertises the product collection with at least 20 characters" - } - ] - } - }, - { - "name": "Product Gallery", - "slug": "woocommerce-blocks/product-query-product-gallery", - "content": { - "titles": [ - { - "default": "Bestsellers", - "ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters" - } - ] - } - }, - { - "name": "Featured Products 2 Columns", - "slug": "woocommerce-blocks/featured-products-2-cols", - "content": { - "titles": [ - { - "default": "Fan favorites", - "ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters" - } - ], - "descriptions": [ - { - "default": "Get ready to start the season right. All the fan favorites in one place at the best price.", - "ai_prompt": "A description of the featured products with at least 90 characters" - } - ], - "buttons": [ - { - "default": "Shop All", - "ai_prompt": "A two words button text to go to the featured products page" - } - ] - } - }, - { - "name": "Product Hero 2 Column 2 Row", - "slug": "woocommerce-blocks/product-hero-2-col-2-row", - "images_total": 2, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "The Eden Jacket", - "ai_prompt": "A three words title that advertises a product related to the following image description: {image.0}" - }, - { - "default": "100% Woolen", - "ai_prompt": "A two words title that advertises a product feature" - }, - { - "default": "Fits your wardrobe", - "ai_prompt": "A three words title that advertises a product feature" - }, - { - "default": "Versatile", - "ai_prompt": "An one word title that advertises a product feature" - }, - { - "default": "Normal Fit", - "ai_prompt": "A two words title that advertises a product feature" - } - ], - "descriptions": [ - { - "default": "Perfect for any look featuring a mid-rise, relax fitting silhouette.", - "ai_prompt": "The description of a product with at least 65 characters related to the following image: {image.0}" - }, - { - "default": "Reflect your fashionable style.", - "ai_prompt": "The description of a product feature with at least 30 characters" - }, - { - "default": "Half tuck into your pants or layer over.", - "ai_prompt": "The description of a product feature with at least 30 characters" - }, - { - "default": "Button-down front for any type of mood or look.", - "ai_prompt": "The description of a product feature with at least 30 characters" - }, - { - "default": "42% Cupro 34% Linen 24% Viscose", - "ai_prompt": "The description of a product feature with at least 30 characters" - } - ], - "buttons": [ - { - "default": "View product", - "ai_prompt": "A two words button text to go to the product page" - } - ] - } - }, - { - "name": "Shop by Price", - "slug": "woocommerce-blocks/shop-by-price", - "content": { - "titles": [ - { - "default": "Outdoor Furniture & Accessories", - "ai_prompt": "An impact phrase that advertises the first product collection with at least 30 characters" - }, - { - "default": "Summer Dinning", - "ai_prompt": "An impact phrase that advertises the second product collection with at least 20 characters" - }, - { - "default": "Women's Styles", - "ai_prompt": "An impact phrase that advertises the third product collection with at least 20 characters" - }, - { - "default": "Kids' Styles", - "ai_prompt": "An impact phrase that advertises the fourth product collection with at least 20 characters" - } - ] - } - }, - { - "name": "Small Discount Banner with Image", - "slug": "woocommerce-blocks/small-discount-banner-with-image", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Chairs", - "ai_prompt": "A single word that advertises the product and is related to the following image description: {image.0}" - } - ] - } - }, - { - "name": "Social: Follow us on social media", - "slug": "woocommerce-blocks/social-follow-us-in-social-media", - "images_total": 4, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Stay in the loop", - "ai_prompt": "A phrase that advertises the social media accounts of the store with at least 25 characters" - } - ] - } - }, - { - "name": "Alternating Image and Text", - "slug": "woocommerce-blocks/alt-image-and-text", - "images_total": 2, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Our products", - "ai_prompt": "A two words impact phrase that advertises the products" - }, - { - "default": "Sustainable blends, stylish accessories", - "ai_prompt": "An impact phrase that advertises the products with at least 40 characters and related to the following image description: {image.0}" - }, - { - "default": "About us", - "ai_prompt": "A two words impact phrase that advertises the brand" - }, - { - "default": "Committed to a greener lifestyle", - "ai_prompt": "An impact phrase that advertises the brand with at least 50 characters related to the following image description: {image.1}" - } - ], - "descriptions": [ - { - "default": "Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.", - "ai_prompt": "A description of the products with at least 180 characters" - }, - { - "default": "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.", - "ai_prompt": "A description of the products with at least 180 characters" - }, - { - "default": "Locally sourced ingredients", - "ai_prompt": "A three word description of the products" - }, - { - "default": "Premium organic blends", - "ai_prompt": "A three word description of the products" - }, - { - "default": "Hand-picked accessories", - "ai_prompt": "A three word description of the products" - }, - { - "default": "Sustainable business practices", - "ai_prompt": "A three word description of the products" - } - ], - "buttons": [ - { - "default": "Meet us", - "ai_prompt": "A two words button text to go to the product page" - } - ] - } - }, - { - "name": "Testimonials 3 Columns", - "slug": "woocommerce-blocks/testimonials-3-columns", - "content": { - "titles": [ - { - "default": "Eclectic finds, ethical delights", - "ai_prompt": "Write a short title advertising a testimonial from a customer" - }, - { - "default": "Sip, Shop, Savor", - "ai_prompt": "Write a short title advertising a testimonial from a customer" - }, - { - "default": "LOCAL LOVE", - "ai_prompt": "Write a short title advertising a testimonial from a customer" - }, - { - "default": "What our customers say", - "ai_prompt": "Write just 4 words to advertise testimonials from customers" - } - ], - "descriptions": [ - { - "default": "Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.", - "ai_prompt": "Write the testimonial from a customer with approximately 150 characters" - }, - { - "default": "The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.", - "ai_prompt": "Write the testimonial from a customer with approximately 150 characters" - }, - { - "default": "From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.", - "ai_prompt": "Write the testimonial from a customer with approximately 150 characters" - } - ] - } - }, - { - "name": "Testimonials Single", - "slug": "woocommerce-blocks/testimonials-single", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "A ‘brewtiful’ experience :-)", - "ai_prompt": "A two words title that advertises the testimonial" - } - ], - "descriptions": [ - { - "default": "Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.", - "ai_prompt": "A description of the testimonial with at least 225 characters" - } - ] - } - }, - { - "name": "Featured Category Cover Image", - "slug": "woocommerce-blocks/featured-category-cover-image", - "images_total": 1, - "images_format": "landscape", - "content": { - "titles": [ - { - "default": "Sit back and relax", - "ai_prompt": "A description for a product with at least 20 characters" - } - ], - "descriptions": [ - { - "default": "With a wide range of designer chairs to elevate your living space.", - "ai_prompt": "An impact phrase that advertises the products with at least 55 characters" - } - ], - "buttons": [ - { - "default": "Shop chairs", - "ai_prompt": "A two words button text to go to the shop page" - } - ] - } - }, - { - "name": "Product Collection: Featured Products 5 Columns", - "slug": "woocommerce-blocks/product-collection-featured-products-5-columns", - "content": { - "titles": [ - { - "default": "Shop new arrivals", - "ai_prompt": "An impact phrase that advertises the newest additions to the store with at least 20 characters" - } - ] - } - } -] diff --git a/plugins/woocommerce/src/Blocks/AssetsController.php b/plugins/woocommerce/src/Blocks/AssetsController.php index 01cd1228a9e..1bb55c0f0c1 100644 --- a/plugins/woocommerce/src/Blocks/AssetsController.php +++ b/plugins/woocommerce/src/Blocks/AssetsController.php @@ -40,6 +40,7 @@ final class AssetsController { add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 ); add_action( 'wp_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 ); add_action( 'admin_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 ); + add_filter( 'js_do_concat', array( $this, 'skip_boost_minification_for_cart_checkout' ), 10, 2 ); } /** @@ -62,9 +63,14 @@ 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-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' ) ); + // Vendor scripts for blocks frontends (not including cart and checkout). + $this->api->register_script( 'wc-blocks-frontend-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-frontend-vendors-frontend' ), array(), false ); + + // Cart and checkout frontend scripts. + $this->api->register_script( 'wc-cart-checkout-vendors', $this->api->get_block_asset_build_path( 'wc-cart-checkout-vendors-frontend' ), array(), false ); + $this->api->register_script( 'wc-cart-checkout-base', $this->api->get_block_asset_build_path( 'wc-cart-checkout-base-frontend' ), array(), false ); + $this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js' ); + $this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js' ); // Register the interactivity components here for now. $this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() ); @@ -253,6 +259,23 @@ final class AssetsController { return $src; } + /** + * Skip Jetpack Boost minification on older versions of Jetpack Boost where it causes issues. + * + * @param mixed $do_concat Whether to concatenate the script or not. + * @param mixed $handle The script handle. + * @return mixed + */ + public function skip_boost_minification_for_cart_checkout( $do_concat, $handle ) { + $boost_is_outdated = defined( 'JETPACK_BOOST_VERSION' ) && version_compare( JETPACK_BOOST_VERSION, '3.4.2', '<' ); + $scripts_to_ignore = [ + 'wc-cart-checkout-vendors', + 'wc-cart-checkout-base', + ]; + + return $boost_is_outdated && in_array( $handle, $scripts_to_ignore, true ) ? false : $do_concat; + } + /** * Add body classes to the frontend and within admin. * diff --git a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php index b9bb10582b3..966af6dfd05 100644 --- a/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php +++ b/plugins/woocommerce/src/Blocks/BlockTemplatesRegistry.php @@ -19,6 +19,7 @@ use Automattic\WooCommerce\Blocks\Templates\ProductCategoryTemplate; use Automattic\WooCommerce\Blocks\Templates\ProductTagTemplate; use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate; use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate; +use Automattic\WooCommerce\Blocks\Templates\ProductFiltersTemplate; use Automattic\WooCommerce\Blocks\Templates\ProductFiltersOverlayTemplate; /** @@ -59,10 +60,14 @@ class BlockTemplatesRegistry { } if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) { $template_parts = array( - MiniCartTemplate::SLUG => new MiniCartTemplate(), - CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(), - ProductFiltersOverlayTemplate::SLUG => new ProductFiltersOverlayTemplate(), + MiniCartTemplate::SLUG => new MiniCartTemplate(), + CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(), ); + + if ( Features::is_enabled( 'experimental-blocks' ) ) { + $template_parts[ ProductFiltersTemplate::SLUG ] = new ProductFiltersTemplate(); + $template_parts[ ProductFiltersOverlayTemplate::SLUG ] = new ProductFiltersOverlayTemplate(); + } } else { $template_parts = array(); } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php index 78e44abfdee..60737382791 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php @@ -36,9 +36,9 @@ class Checkout extends AbstractBlock { // This prevents the page redirecting when the cart is empty. This is so the editor still loads the page preview. add_filter( 'woocommerce_checkout_redirect_empty_cart', - function( $return ) { + function ( $redirect_empty_cart ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return isset( $_GET['_wp-find-template'] ) ? false : $return; + return isset( $_GET['_wp-find-template'] ) ? false : $redirect_empty_cart; } ); @@ -94,10 +94,17 @@ class Checkout extends AbstractBlock { * @return array|string */ protected function get_block_type_script( $key = null ) { + $dependencies = []; + + // Load password strength meter script asynchronously if needed. + if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { + $dependencies[] = 'zxcvbn-async'; + } + $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), - 'dependencies' => [], + 'dependencies' => $dependencies, ]; return $key ? $script[ $key ] : $script; } @@ -354,6 +361,7 @@ class Checkout extends AbstractBlock { $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) ); $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) ); $this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) ); + $this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() ); $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() ); $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() ); @@ -377,7 +385,7 @@ class Checkout extends AbstractBlock { $shipping_methods = WC()->shipping()->get_shipping_methods(); $formatted_shipping_methods = array_reduce( $shipping_methods, - function( $acc, $method ) { + function ( $acc, $method ) { if ( in_array( $method->id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) { return $acc; } @@ -405,7 +413,7 @@ class Checkout extends AbstractBlock { $payment_methods = $this->get_enabled_payment_gateways(); $formatted_payment_methods = array_reduce( $payment_methods, - function( $acc, $method ) { + function ( $acc, $method ) { $acc[] = [ 'id' => $method->id, 'title' => $method->method_title, @@ -427,7 +435,7 @@ class Checkout extends AbstractBlock { $all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache. $incompatible_extensions = array_reduce( $declared_extensions['incompatible'], - function( $acc, $item ) use ( $all_plugins ) { + function ( $acc, $item ) use ( $all_plugins ) { $plugin = $all_plugins[ $item ] ?? null; $plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 ); $plugin_name = $plugin['Name'] ?? $plugin_id; @@ -465,7 +473,7 @@ class Checkout extends AbstractBlock { $payment_gateways = WC()->payment_gateways->payment_gateways(); return array_filter( $payment_gateways, - function( $payment_gateway ) { + function ( $payment_gateway ) { return 'yes' === $payment_gateway->enabled; } ); @@ -500,7 +508,7 @@ class Checkout extends AbstractBlock { $payment_methods[ $payment_method_group ] = array_values( array_filter( $saved_payment_methods, - function( $saved_payment_method ) use ( $payment_gateways ) { + function ( $saved_payment_method ) use ( $payment_gateways ) { return in_array( $saved_payment_method['method']['gateway'], array_keys( $payment_gateways ), true ); } ) diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php b/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php index 6304507f161..a82be2587ee 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ComingSoon.php @@ -21,12 +21,24 @@ class ComingSoon extends AbstractBlock { } /** - * Get the frontend style handle for this block type. + * Enqueue frontend assets for this block, just in time for rendering. * - * @return null + * @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that + * we intentionally do not pass 'script' to register_block_type. + * + * @param array $attributes Any attributes that currently are available from the block. + * @param string $content The block content. + * @param WP_Block $block The block object. */ - protected function get_block_type_style() { - return null; + protected function enqueue_assets( array $attributes, $content, $block ) { + parent::enqueue_assets( $attributes, $content, $block ); + + if ( isset( $attributes['color'] ) ) { + wp_add_inline_style( + 'wc-blocks-style', + ':root{--woocommerce-coming-soon-color: ' . esc_html( $attributes['color'] ) . '}' + ); + } } /** diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php index dee420b76e7..80bd799b224 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/CustomerAccount.php @@ -34,6 +34,7 @@ class CustomerAccount extends AbstractBlock { 'anchor' => 'core/navigation', 'area' => 'header', 'callback' => 'should_unhook_block', + 'version' => '8.4.0', ), ); @@ -118,19 +119,27 @@ class CustomerAccount extends AbstractBlock { $account_link = get_option( 'woocommerce_myaccount_page_id' ) ? wc_get_account_endpoint_url( 'dashboard' ) : wp_login_url(); $allowed_svg = array( - 'svg' => array( + 'svg' => array( 'class' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, ), - 'path' => array( + 'path' => array( 'd' => true, 'fill' => true, 'fill-rule' => true, 'clip-rule' => true, ), + 'circle' => array( + 'cx' => true, + 'cy' => true, + 'r' => true, + 'stroke' => true, + 'stroke-width' => true, + 'fill' => true, + ), ); // Only provide aria-label if the display style is icon only. @@ -158,14 +167,21 @@ class CustomerAccount extends AbstractBlock { } if ( self::DISPLAY_LINE === $attributes['iconStyle'] ) { - return ' - + return ' + + '; } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php index e3021511405..cb5952d2d84 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/MiniCart.php @@ -65,6 +65,7 @@ class MiniCart extends AbstractBlock { 'position' => 'after', 'anchor' => 'core/navigation', 'area' => 'header', + 'version' => '8.4.0', ), ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php index e2c96926fc7..6cdbbba4883 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Status.php @@ -154,9 +154,10 @@ class Status extends AbstractOrderConfirmationBlock { */ protected function render_account_notice( $order = null ) { if ( $order && $order->get_customer_id() && 'store-api' === $order->get_created_via() ) { - $nag = get_user_option( 'default_password_nag', $order->get_customer_id() ); + $nag = get_user_option( 'default_password_nag', $order->get_customer_id() ); + $generate = get_option( 'woocommerce_registration_generate_password', false ); - if ( $nag ) { + if ( $nag && ! $generate ) { return wc_print_notice( sprintf( // translators: %s: site name. diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php index 978e26c8983..63cf4fbc354 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Totals.php @@ -225,7 +225,7 @@ class Totals extends AbstractOrderConfirmationBlock { '

' . esc_html__( 'Note:', 'woocommerce' ) . '

' . - '

' . wp_kses_post( nl2br( wptexturize( $order->get_customer_note() ) ) ) . '

' . + '

' . wp_kses( nl2br( wptexturize( $order->get_customer_note() ) ), [] ) . '

' . '
'; } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php index 77c03b2fd4c..87430430876 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php @@ -167,8 +167,8 @@ class ProductButton extends AbstractBlock { ); $div_directives = ' - data-wc-interactive=\'' . wp_json_encode( $interactive, JSON_NUMERIC_CHECK ) . '\' - data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK ) . '\' + data-wc-interactive=\'' . wp_json_encode( $interactive, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\' + data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\' '; $button_directives = ' diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php index 9d828541d02..4b9372e35f6 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection.php @@ -181,7 +181,7 @@ class ProductCollection extends AbstractBlock { 'data-wc-navigation-id', 'wc-product-collection-' . $this->parsed_block['attrs']['queryId'] ); - $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) ); + $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); $p->set_attribute( 'data-wc-context', wp_json_encode( @@ -193,7 +193,7 @@ class ProductCollection extends AbstractBlock { // This way we avoid prefetching when the page loads. 'isPrefetchNextOrPreviousLink' => false, ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); $block_content = $p->get_updated_html(); @@ -295,7 +295,7 @@ class ProductCollection extends AbstractBlock { 'class_name' => $class_name, ) ) ) { - $processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) ); + $processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); $processor->set_attribute( 'data-wc-on--click', 'actions.navigate' ); $processor->set_attribute( 'data-wc-key', $key_prefix . '--' . esc_attr( wp_rand() ) ); @@ -524,7 +524,10 @@ class ProductCollection extends AbstractBlock { // phpcs:ignore WordPress.DB.SlowDBQuery $block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array(); - $is_exclude_applied_filters = ! ( $block->context['query']['inherit'] ?? false ); + $inherit = $block->context['query']['inherit'] ?? false; + $filterable = $block->context['query']['filterable'] ?? false; + + $is_exclude_applied_filters = ! ( $inherit || $filterable ); return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters ); } @@ -1089,7 +1092,7 @@ class ProductCollection extends AbstractBlock { $max_price_query = empty( $max_price ) ? array() : array( 'key' => '_price', 'value' => $max_price, - 'compare' => '<', + 'compare' => '<=', 'type' => 'numeric', ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php index 0b24b3b07d7..6cfc9a1465a 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php @@ -115,8 +115,8 @@ final class ProductFilter extends AbstractBlock { } $attributes_data = array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ), - 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ) ), + 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'class' => 'wc-block-product-filters', ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php index 722e0d0ebaf..679227f3a09 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php @@ -55,8 +55,8 @@ final class ProductFilterActive extends AbstractBlock { $wrapper_attributes = get_block_wrapper_attributes( array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ), - 'data-wc-context' => wp_json_encode( $context ), + 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes', ) ); @@ -185,7 +185,7 @@ final class ProductFilterActive extends AbstractBlock { private function get_html_attributes( $attributes ) { return array_reduce( array_keys( $attributes ), - function( $acc, $key ) use ( $attributes ) { + function ( $acc, $key ) use ( $attributes ) { $acc .= sprintf( ' %1$s="%2$s"', esc_attr( $key ), esc_attr( $attributes[ $key ] ) ); return $acc; }, @@ -227,7 +227,7 @@ final class ProductFilterActive extends AbstractBlock { return array_filter( $url_query_params, - function( $key ) use ( $filter_param_keys ) { + function ( $key ) use ( $filter_param_keys ) { return in_array( $key, $filter_param_keys, true ); }, ARRAY_FILTER_USE_KEY diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php index ed4bb640c5d..352c708f8ea 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php @@ -30,6 +30,33 @@ final class ProductFilterAttribute extends AbstractBlock { add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 ); add_filter( 'collection_active_filters_data', array( $this, 'register_active_filters_data' ), 10, 2 ); + add_action( 'deleted_transient', array( $this, 'delete_default_attribute_id_transient' ) ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = array() ) { + parent::enqueue_data( $attributes ); + + if ( is_admin() ) { + $this->asset_data_registry->add( 'defaultProductFilterAttribute', $this->get_default_attribute() ); + } + } + + /** + * Delete the default attribute id transient when the attribute taxonomies are deleted. + * + * @param string $transient The transient name. + */ + public function delete_default_attribute_id_transient( $transient ) { + if ( 'wc_attribute_taxonomies' === $transient ) { + delete_transient( 'wc_block_product_filter_attribute_default_attribute' ); + } } /** @@ -43,7 +70,7 @@ final class ProductFilterAttribute extends AbstractBlock { public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) { $attribute_param_keys = array_filter( $url_param_keys, - function( $param ) { + function ( $param ) { return strpos( $param, 'filter_' ) === 0 || strpos( $param, 'query_type_' ) === 0; } ); @@ -64,7 +91,7 @@ final class ProductFilterAttribute extends AbstractBlock { public function register_active_filters_data( $data, $params ) { $product_attributes_map = array_reduce( wc_get_attribute_taxonomies(), - function( $acc, $attribute_object ) { + function ( $acc, $attribute_object ) { $acc[ $attribute_object->attribute_name ] = $attribute_object->attribute_label; return $acc; }, @@ -73,7 +100,7 @@ final class ProductFilterAttribute extends AbstractBlock { $active_product_attributes = array_reduce( array_keys( $params ), - function( $acc, $attribute ) { + function ( $acc, $attribute ) { if ( strpos( $attribute, 'filter_' ) === 0 ) { $acc[] = str_replace( 'filter_', '', $attribute ); } @@ -84,7 +111,7 @@ final class ProductFilterAttribute extends AbstractBlock { $active_product_attributes = array_filter( $active_product_attributes, - function( $item ) use ( $product_attributes_map ) { + function ( $item ) use ( $product_attributes_map ) { return in_array( $item, array_keys( $product_attributes_map ), true ); } ); @@ -96,7 +123,7 @@ final class ProductFilterAttribute extends AbstractBlock { // Get attribute term by slug. $terms = array_map( - function( $term ) use ( $product_attribute, $action_namespace ) { + function ( $term ) use ( $product_attribute, $action_namespace ) { $term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" ); return array( 'title' => $term_object->name, @@ -107,7 +134,8 @@ final class ProductFilterAttribute extends AbstractBlock { 'value' => $term, 'attributeSlug' => $product_attribute, 'queryType' => get_query_var( "query_type_{$product_attribute}" ), - ) + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), ), ); @@ -146,7 +174,7 @@ final class ProductFilterAttribute extends AbstractBlock { '
', get_block_wrapper_attributes( array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ), + 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-has-filter' => 'no', ) ), @@ -168,7 +196,7 @@ final class ProductFilterAttribute extends AbstractBlock { ); $attribute_options = array_map( - function( $term ) use ( $attribute_counts, $selected_terms ) { + function ( $term ) use ( $attribute_counts, $selected_terms ) { $term = (array) $term; $term['count'] = $attribute_counts[ $term['term_id'] ]; $term['selected'] = in_array( $term['slug'], $selected_terms, true ); @@ -179,7 +207,7 @@ final class ProductFilterAttribute extends AbstractBlock { $filtered_options = array_filter( $attribute_options, - function( $option ) { + function ( $option ) { return $option['count'] > 0; } ); @@ -191,15 +219,15 @@ final class ProductFilterAttribute extends AbstractBlock { $context = array( 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), 'queryType' => $attributes['queryType'], - 'selectType' => $attributes['selectType'], + 'selectType' => 'multiple', ); return sprintf( '
%2$s%3$s
', get_block_wrapper_attributes( array( - 'data-wc-context' => wp_json_encode( $context ), - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ), + 'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-has-filter' => 'yes', ) ), @@ -242,7 +270,7 @@ final class ProductFilterAttribute extends AbstractBlock { 'items' => $list_items, 'action' => "{$this->get_full_block_name()}::actions.navigate", 'selected_items' => $selected_items, - 'select_type' => $attributes['selectType'] ?? 'multiple', + 'select_type' => 'multiple', // translators: %s is a product attribute name. 'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ), ) @@ -264,7 +292,7 @@ final class ProductFilterAttribute extends AbstractBlock { $show_counts = $attributes['showCounts'] ?? false; $list_options = array_map( - function( $option ) use ( $show_counts ) { + function ( $option ) use ( $show_counts ) { return array( 'id' => $option['slug'] . '-' . $option['term_id'], 'checked' => $option['selected'], @@ -320,13 +348,72 @@ final class ProductFilterAttribute extends AbstractBlock { $attribute_counts = array_reduce( $attribute_counts, - function( $acc, $count ) { + function ( $acc, $count ) { $acc[ $count['term'] ] = $count['count']; return $acc; }, - [] + array() ); return $attribute_counts; } + + /** + * Get the attribute if with most term but closest to 30 terms. + * + * @return int + */ + private function get_default_attribute() { + $cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' ); + + if ( $cached ) { + return $cached; + } + + $attributes = wc_get_attribute_taxonomies(); + + $attributes_count = array_map( + function ( $attribute ) { + return intval( + wp_count_terms( + array( + 'taxonomy' => 'pa_' . $attribute->attribute_name, + 'hide_empty' => false, + ) + ) + ); + }, + $attributes + ); + + asort( $attributes_count ); + + $search = 30; + $closest = null; + $attribute_id = null; + + foreach ( $attributes_count as $id => $count ) { + if ( null === $closest || abs( $search - $closest ) > abs( $count - $search ) ) { + $closest = $count; + $attribute_id = $id; + } + + if ( $closest && $count >= $search ) { + break; + } + } + + $default_attribute = array( + 'id' => 0, + 'label' => __( 'Attribute', 'woocommerce' ), + ); + + if ( $attribute_id ) { + $default_attribute = $attributes[ $attribute_id ]; + } + + set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute ); + + return $default_attribute; + } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php index 80a0ad50a7f..708868018c2 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterPrice.php @@ -44,7 +44,7 @@ final class ProductFilterPrice extends AbstractBlock { public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) { $price_param_keys = array_filter( $url_param_keys, - function( $param ) { + function ( $param ) { return self::MIN_PRICE_QUERY_VAR === $param || self::MAX_PRICE_QUERY_VAR === $param; } ); @@ -141,8 +141,13 @@ final class ProductFilterPrice extends AbstractBlock { ) = $attributes; $wrapper_attributes = array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ), - 'data-wc-context' => wp_json_encode( $data ), + 'data-wc-interactive' => wp_json_encode( + array( + 'namespace' => $this->get_full_block_name(), + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP, + ), + 'data-wc-context' => wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-has-filter' => 'no', ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php index 8299a6028f1..76bcad3727d 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterRating.php @@ -47,7 +47,7 @@ final class ProductFilterRating extends AbstractBlock { public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) { $rating_param_keys = array_filter( $url_param_keys, - function( $param ) { + function ( $param ) { return self::RATING_FILTER_QUERY_VAR === $param; } ); @@ -84,8 +84,8 @@ final class ProductFilterRating extends AbstractBlock { /* translators: %d is the rating value. */ 'title' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating ), 'attributes' => array( - 'data-wc-on--click' => "{$this->get_full_block_name()}::actions.removeFilter", - 'data-wc-context' => "{$this->get_full_block_name()}::" . wp_json_encode( array( 'value' => $rating ) ), + 'data-wc-on--click' => esc_attr( "{$this->get_full_block_name()}::actions.removeFilter" ), + 'data-wc-context' => esc_attr( "{$this->get_full_block_name()}::" ) . wp_json_encode( array( 'value' => $rating ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), ), ); }, diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php index 6c58b042a72..ae9295801e9 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterStockStatus.php @@ -45,7 +45,7 @@ final class ProductFilterStockStatus extends AbstractBlock { public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) { $stock_param_keys = array_filter( $url_param_keys, - function( $param ) { + function ( $param ) { return self::STOCK_STATUS_QUERY_VAR === $param; } ); @@ -81,12 +81,12 @@ final class ProductFilterStockStatus extends AbstractBlock { $action_namespace = $this->get_full_block_name(); $active_stock_statuses = array_map( - function( $status ) use ( $stock_status_options, $action_namespace ) { + function ( $status ) use ( $stock_status_options, $action_namespace ) { return array( 'title' => $stock_status_options[ $status ], 'attributes' => array( 'data-wc-on--click' => "$action_namespace::actions.removeFilter", - 'data-wc-context' => "$action_namespace::" . wp_json_encode( array( 'value' => $status ) ), + 'data-wc-context' => "$action_namespace::" . wp_json_encode( array( 'value' => $status ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), ), ); }, @@ -168,7 +168,7 @@ final class ProductFilterStockStatus extends AbstractBlock { $list_items = array_values( array_map( - function( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) { + function ( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) { $label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ]; return array( 'label' => $label, @@ -183,13 +183,13 @@ final class ProductFilterStockStatus extends AbstractBlock { $selected_items = array_values( array_filter( $list_items, - function( $item ) use ( $selected_stock_statuses ) { + function ( $item ) use ( $selected_stock_statuses ) { return in_array( $item['value'], $selected_stock_statuses, true ); } ) ); - $data_directive = wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ); + $data_directive = wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); ob_start(); ?> @@ -255,7 +255,7 @@ final class ProductFilterStockStatus extends AbstractBlock { return array_filter( $data, - function( $stock_count ) { + function ( $stock_count ) { return $stock_count['count'] > 0; } ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php index 0d14d0ef60e..449bc4f8c6c 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlay.php @@ -1,6 +1,9 @@ %s
', esc_html__( 'Filters Overlay', 'woocommerce' ) ); - $html = ob_get_clean(); + return $content; + } - return $html; + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + + $template_part_edit_uri = ''; + + if ( + current_user_can( 'edit_theme_options' ) && + ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) + ) { + $theme_slug = BlockTemplateUtils::theme_has_template_part( 'product-filters-overlay' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG; + + $site_editor_uri = add_query_arg( + array( + 'canvas' => 'edit', + 'path' => '/template-parts/single', + ), + admin_url( 'site-editor.php' ) + ); + + $template_part_edit_uri = esc_url_raw( + add_query_arg( + array( + 'postId' => sprintf( '%s//%s', $theme_slug, 'product-filters-overlay' ), + 'postType' => 'wp_template_part', + ), + $site_editor_uri + ) + ); + } + + $this->asset_data_registry->add( + 'templatePartProductFiltersOverlayEditUri', + $template_part_edit_uri + ); } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php index 18ccb0c8fa7..812a3baaf98 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGallery.php @@ -59,7 +59,7 @@ class ProductGallery extends AbstractBlock { $html = array_reduce( $parsed_template, - function( $carry, $item ) { + function ( $carry, $item ) { return $carry . render_block( $item ); }, '' @@ -134,7 +134,7 @@ class ProductGallery extends AbstractBlock { $p = new \WP_HTML_Tag_Processor( $html ); if ( $p->next_tag() ) { - $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) ) ); + $p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); $p->set_attribute( 'data-wc-context', wp_json_encode( @@ -147,7 +147,8 @@ class ProductGallery extends AbstractBlock { 'mouseIsOverPreviousOrNextButton' => false, 'productId' => $product_id, 'elementThatTriggeredDialogOpening' => null, - ) + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php index af79a57ae49..b7a1ea0668e 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php @@ -98,7 +98,7 @@ class ProductGalleryLargeImage extends AbstractBlock { '{content}' => $content, '{directives}' => array_reduce( array_keys( $directives ), - function( $carry, $key ) use ( $directives ) { + function ( $carry, $key ) use ( $directives ) { return $carry . ' ' . $key . '="' . esc_attr( $directives[ $key ] ) . '"'; }, '' @@ -143,7 +143,7 @@ class ProductGalleryLargeImage extends AbstractBlock { ); $main_image_with_wrapper = array_map( - function( $main_image_element ) { + function ( $main_image_element ) { return "'; }, $main_images @@ -151,7 +151,6 @@ class ProductGalleryLargeImage extends AbstractBlock { $visible_main_image = array_shift( $main_images ); return array( $visible_main_image, $main_image_with_wrapper ); - } /** @@ -187,8 +186,8 @@ class ProductGalleryLargeImage extends AbstractBlock { ); return array( - 'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) ), - 'data-wc-context' => wp_json_encode( $context, JSON_NUMERIC_CHECK ), + 'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wc-context' => wp_json_encode( $context, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), 'data-wc-on--mousemove' => 'actions.startZoom', 'data-wc-on--mouseleave' => 'actions.resetZoom', ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php index 9b7a907321a..62421e313b4 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImageNextPrevious.php @@ -129,7 +129,7 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock { '{next_button}' => $next_button, '{alignment_class}' => $alignment_class, '{position_class}' => $position_class, - '{data_wc_interactive}' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_NUMERIC_CHECK ), + '{data_wc_interactive}' => wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), ) ); } @@ -188,7 +188,6 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock { $this->get_class_suffix( $context ), $icon_path ); - } /** @@ -229,6 +228,5 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock { $this->get_class_suffix( $context ), $icon_path ); - } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php index e3bc873cbde..58e268addc0 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryPager.php @@ -74,7 +74,7 @@ class ProductGalleryPager extends AbstractBlock {
', $wrapper_attributes, $html, - wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) ) + wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); } return ''; @@ -127,7 +127,10 @@ class ProductGalleryPager extends AbstractBlock { $p->set_attribute( 'data-wc-context', wp_json_encode( - array( 'imageId' => strval( $product_gallery_image_id ) ), + array( + 'imageId' => strval( $product_gallery_image_id ), + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP, ) ); $p->set_attribute( diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php index 09b0d8cceee..58f0765923e 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryThumbnails.php @@ -168,7 +168,7 @@ class ProductGalleryThumbnails extends AbstractBlock { } } - $thumbnails_count++; + ++$thumbnails_count; } return sprintf( @@ -178,7 +178,7 @@ class ProductGalleryThumbnails extends AbstractBlock { esc_attr( $classes_and_styles['classes'] ), esc_attr( $classes_and_styles['styles'] ), $html, - wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ) ) + wp_json_encode( array( 'namespace' => 'woocommerce/product-gallery' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php index 9d3f5a4ab68..ec5b78d9f6d 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php @@ -619,7 +619,7 @@ class ProductQuery extends AbstractBlock { $max_price_query = empty( $max_price ) ? array() : [ 'key' => '_price', 'value' => $max_price, - 'compare' => '<', + 'compare' => '<=', 'type' => 'numeric', ]; diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php index 1ea78bcdc53..62453b1a5a7 100644 --- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php +++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php @@ -386,17 +386,8 @@ class CheckoutFields { $field_data['options'] = $cleaned_options; - // If the field is not required, inject an empty option at the start. - if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) { - $field_data['options'] = array_merge( - [ - [ - 'value' => '', - 'label' => '', - ], - ], - $field_data['options'] - ); + if ( isset( $field_data['placeholder'] ) ) { + $field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] ); } return $field_data; diff --git a/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php b/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php index 647d44a0bb7..ed27a58af1f 100644 --- a/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php +++ b/plugins/woocommerce/src/Blocks/InteractivityComponents/CheckboxList.php @@ -29,12 +29,12 @@ class CheckboxList { $checkbox_list_context = array( 'items' => $items ); $on_change = $props['on_change'] ?? ''; - $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ) ); + $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); ob_start(); ?>
-
+
    diff --git a/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php b/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php index 68521bd09d0..1c6d2d13cbd 100644 --- a/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php +++ b/plugins/woocommerce/src/Blocks/InteractivityComponents/Dropdown.php @@ -35,12 +35,12 @@ class Dropdown { ); $action = $props['action'] ?? ''; - $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) ); + $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); ob_start(); ?>
    -
    +
    @@ -118,7 +118,7 @@ class Dropdown { data-wc-class--is-selected="state.isSelected" class="components-form-token-field__suggestion" data-wc-bind--aria-selected="state.isSelected" - data-wc-context='' + data-wc-context='' >
    diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php index c5074cea0ef..2b24e1a9657 100644 --- a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php +++ b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php @@ -91,11 +91,13 @@ class PTKPatternsStore { * @return void */ private function schedule_action_if_not_pending( $action ) { - if ( as_has_scheduled_action( $action ) ) { + $last_request = get_transient( 'last_fetch_patterns_request' ); + if ( as_has_scheduled_action( $action ) || false !== $last_request ) { return; } as_schedule_single_action( time(), $action ); + set_transient( 'last_fetch_patterns_request', time(), HOUR_IN_SECONDS ); } /** @@ -185,6 +187,7 @@ class PTKPatternsStore { $patterns = $this->ptk_client->fetch_patterns( array( + // This is the site where the patterns are stored. Despite the 'wpcomstaging.com' domain suggesting a staging environment, this URL points to the production environment where stable versions of the patterns are maintained. 'site' => 'wooblockpatterns.wpcomstaging.com', 'categories' => array( '_woo_intro', diff --git a/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php b/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php index 2831921e3ec..a455cecb8ee 100644 --- a/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php +++ b/plugins/woocommerce/src/Blocks/Templates/AbstractPageTemplate.php @@ -14,7 +14,6 @@ abstract class AbstractPageTemplate extends AbstractTemplate { */ public function init() { add_filter( 'page_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 ); - add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) ); } /** @@ -48,7 +47,10 @@ abstract class AbstractPageTemplate extends AbstractTemplate { } /** - * Filter the page title when the template is active. + * Forces the page title to match the template title when this template is active. + * + * Only applies when hooked into `pre_get_document_title`. Most templates used for pages will not require this because + * the page title should be used instead. * * @param string $title Page title. * @return string diff --git a/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php b/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php index 29db821623e..d789aa538f2 100644 --- a/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php +++ b/plugins/woocommerce/src/Blocks/Templates/OrderConfirmationTemplate.php @@ -20,7 +20,7 @@ class OrderConfirmationTemplate extends AbstractPageTemplate { */ public function init() { add_action( 'wp_before_admin_bar_render', array( $this, 'remove_edit_page_link' ) ); - + add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) ); parent::init(); } diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php index 6070ff238ef..38c19590275 100644 --- a/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php +++ b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersOverlayTemplate.php @@ -20,14 +20,12 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart { * * @var string */ - public $template_area = 'product-filters-overlay'; + public $template_area = 'uncategorized'; /** * Initialization method. */ - public function init() { - add_filter( 'default_wp_template_part_areas', array( $this, 'register_product_filters_overlay_template_part_area' ), 10, 1 ); - } + public function init() {} /** * Returns the title of the template. @@ -46,21 +44,4 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart { public function get_template_description() { return __( 'Template used to display the Product Filters Overlay.', 'woocommerce' ); } - - /** - * Add Filters Overlay to the default template part areas. - * - * @param array $default_area_definitions An array of supported area objects. - * @return array The supported template part areas including the Filters Overlay one. - */ - public function register_product_filters_overlay_template_part_area( $default_area_definitions ) { - $product_filters_overlay_template_part_area = array( - 'area' => 'product-filters-overlay', - 'label' => $this->get_template_title(), - 'description' => $this->get_template_description(), - 'icon' => 'filter', - 'area_tag' => 'product-filters-overlay', - ); - return array_merge( $default_area_definitions, array( $product_filters_overlay_template_part_area ) ); - } } diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductFiltersTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersTemplate.php new file mode 100644 index 00000000000..ece799a312f --- /dev/null +++ b/plugins/woocommerce/src/Blocks/Templates/ProductFiltersTemplate.php @@ -0,0 +1,95 @@ +name ) { + return $variations; + } + + // If template part is modified, Core will pick it up and register a variation + // for it. Check if the variation already exists before adding it. + foreach ( $variations as $variation ) { + if ( ! empty( $variation['attributes']['slug'] ) && 'product-filters' === $variation['attributes']['slug'] ) { + return $variations; + } + } + + $theme = 'woocommerce/woocommerce'; + // Check if current theme overrides this template part. + if ( BlockTemplateUtils::theme_has_template_part( 'product-filters' ) ) { + $theme = wp_get_theme()->get( 'TextDomain' ); + } + + $variations[] = array( + 'name' => 'file_' . self::SLUG, + 'title' => $this->get_template_title(), + 'description' => true, + 'attributes' => array( + 'slug' => self::SLUG, + 'theme' => $theme, + 'area' => $this->template_area, + ), + 'scope' => array( 'inserter' ), + 'icon' => 'layout', + ); + return $variations; + } +} diff --git a/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php b/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php index 4ac4282378c..2bbc2a89637 100644 --- a/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php +++ b/plugins/woocommerce/src/Blocks/Utils/BlockHooksTrait.php @@ -18,31 +18,34 @@ trait BlockHooksTrait { * @return array An array of block slugs hooked into a given context. */ public function register_hooked_block( $hooked_blocks, $position, $anchor_block, $context ) { - - /** - * If the block has no hook placements, return early. - */ + // If the block has no hook placements, return early. if ( ! isset( $this->hooked_block_placements ) || empty( $this->hooked_block_placements ) ) { return $hooked_blocks; } - // Cache for active theme. - static $active_theme_name = null; - if ( is_null( $active_theme_name ) ) { - $active_theme_name = wp_get_theme()->get( 'Name' ); + // Cache the block hooks version. + static $block_hooks_version = null; + if ( defined( 'WP_RUN_CORE_TESTS' ) || is_null( $block_hooks_version ) ) { + $block_hooks_version = get_option( 'woocommerce_hooked_blocks_version' ); } - /** - * A list of theme slugs to execute this with. This is a temporary - * measure until improvements to the Block Hooks API allow for exposing - * to all block themes. - * - * @since 8.4.0 - */ - $theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) ); + // If block hooks are disabled or the version is not set, return early. + if ( 'no' === $block_hooks_version || false === $block_hooks_version ) { + return $hooked_blocks; + } - if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) { - foreach ( $this->hooked_block_placements as $placement ) { + // Valid placements are those that have no version specified, + // or have a version that is less than or equal to version specified in the woocommerce_hooked_blocks_version option. + $valid_placements = array_filter( + $this->hooked_block_placements, + function ( $placement ) use ( $block_hooks_version ) { + $placement_version = isset( $placement['version'] ) ? $placement['version'] : null; + return is_null( $placement_version ) || ! is_null( $placement_version ) && version_compare( $block_hooks_version, $placement_version, '>=' ); + } + ); + + if ( $context && ! empty( $valid_placements ) ) { + foreach ( $valid_placements as $placement ) { if ( $placement['position'] === $position && $placement['anchor'] === $anchor_block ) { // If an area has been specified for this placement. diff --git a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php index c47b10aa1c2..ed9ffa1e58e 100644 --- a/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/BlockTemplateUtils.php @@ -316,9 +316,13 @@ class BlockTemplateUtils { $wp_template_part_filenames = array( 'checkout-header.html', 'mini-cart.html', - 'product-filters-overlay.html', ); + if ( Features::is_enabled( 'experimental-blocks' ) ) { + $wp_template_part_filenames[] = 'product-filters.html'; + $wp_template_part_filenames[] = 'product-filters-overlay.html'; + } + /* * This may return the blockified directory for wp_templates. * At the moment every template file has a corresponding blockified file. diff --git a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php index 4492c7cb08f..78c6cefe9e0 100644 --- a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php +++ b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php @@ -59,7 +59,8 @@ class ProductGalleryUtils { wp_json_encode( array( 'imageId' => $product_gallery_image_id, - ) + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); diff --git a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php index f7d2894ba22..10b1a60a933 100644 --- a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php +++ b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php @@ -88,7 +88,7 @@ final class ReserveStock { try { $items = array_filter( $order->get_items(), - function( $item ) { + function ( $item ) { return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0; } ); diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 8a1d5d9fbf3..56d3b5b1e98 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -31,6 +31,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\UtilsC use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchProcessingServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ComingSoonServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\StatsServiceProvider; /** * PSR11 compliant dependency injection container for WooCommerce. @@ -81,6 +82,7 @@ final class Container { LoggingServiceProvider::class, EnginesServiceProvider::class, ComingSoonServiceProvider::class, + StatsServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php index 9e15258c336..aa804aef3a8 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php @@ -1,6 +1,6 @@ get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . ''; echo '#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . ''; } + + // Used for showing date & status next to order number/buyer name on small screens. + echo '
    '; + $this->render_order_date_column( $order ); + echo '
    '; + echo '
    '; + $this->render_order_status_column( $order ); + echo '
    '; } /** @@ -1056,14 +1064,14 @@ class ListTable extends WP_List_Table { */ private function get_order_status_label( WC_Order $order ): string { $status_names = array( - 'Pending payment' => __( 'The order has been received, but no payment has been made. Pending payment orders are generally awaiting customer action.', 'woocommerce' ), - 'On hold' => __( 'The order is awaiting payment confirmation. Stock is reduced, but you need to confirm payment.', 'woocommerce' ), - 'Processing' => __( 'Payment has been received (paid), and the stock has been reduced. The order is awaiting fulfillment.', 'woocommerce' ), - 'Completed' => __( 'Order fulfilled and complete.', 'woocommerce' ), - 'Failed' => __( 'The customer’s payment failed or was declined, and no payment has been successfully made.', 'woocommerce' ), - 'Draft' => __( 'Draft orders are created when customers start the checkout process while the block version of the checkout is in place.', 'woocommerce' ), - 'Canceled' => __( 'The order was canceled by an admin or the customer.', 'woocommerce' ), - 'Refunded' => __( 'Orders are automatically put in the Refunded status when an admin or shop manager has fully refunded the order’s value after payment.', 'woocommerce' ), + 'pending' => __( 'The order has been received, but no payment has been made. Pending payment orders are generally awaiting customer action.', 'woocommerce' ), + 'on-hold' => __( 'The order is awaiting payment confirmation. Stock is reduced, but you need to confirm payment.', 'woocommerce' ), + 'processing' => __( 'Payment has been received (paid), and the stock has been reduced. The order is awaiting fulfillment.', 'woocommerce' ), + 'completed' => __( 'Order fulfilled and complete.', 'woocommerce' ), + 'failed' => __( 'The customer’s payment failed or was declined, and no payment has been successfully made.', 'woocommerce' ), + 'checkout-draft' => __( 'Draft orders are created when customers start the checkout process while the block version of the checkout is in place.', 'woocommerce' ), + 'cancelled' => __( 'The order was canceled by an admin or the customer.', 'woocommerce' ), + 'refunded' => __( 'Orders are automatically put in the Refunded status when an admin or shop manager has fully refunded the order’s value after payment.', 'woocommerce' ), ); /** @@ -1073,9 +1081,9 @@ class ListTable extends WP_List_Table { * @param WC_Order $order Current order object. * @since 9.1.0 */ - $status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names ); + $status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names, $order ); - $status_name = wc_get_order_status_name( $order->get_status() ); + $status_name = $order->get_status(); return isset( $status_names[ $status_name ] ) ? $status_names[ $status_name ] : ''; } diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index f81aa2c69ca..06f8ff673ab 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -93,10 +93,10 @@ class DefaultFreeExtensions { $plugins = array( 'google-listings-and-ads' => array( 'min_php_version' => '7.4', - 'name' => __( 'Google Listings & Ads', 'woocommerce' ), + 'name' => __( 'Google for WooCommerce', 'woocommerce' ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ - __( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ), + __( 'Drive sales with %1$sGoogle for WooCommerce%2$s', 'woocommerce' ), '', '' ), @@ -116,7 +116,7 @@ class DefaultFreeExtensions { ), ), 'google-listings-and-ads:alt' => array( - 'name' => __( 'Google Listings & Ads', 'woocommerce' ), + 'name' => __( 'Google for WooCommerce', 'woocommerce' ), 'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', @@ -862,7 +862,7 @@ class DefaultFreeExtensions { 'install_priority' => 1, ), 'google-listings-and-ads' => array( - 'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ), + 'label' => __( 'Drive sales with Google for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ), 'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ), 'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures', diff --git a/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php b/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php index 7d4f5c21819..8573c463735 100644 --- a/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php +++ b/plugins/woocommerce/src/Internal/Admin/WCPayPromotion/Init.php @@ -114,7 +114,7 @@ class Init extends RemoteSpecsEngine { $locale = get_user_locale(); $specs = self::get_specs(); - $results = EvaluateSuggestion::evaluate_specs( $specs ); + $results = EvaluateSuggestion::evaluate_specs( $specs, array( 'source' => 'wc-wcpay-promotions' ) ); if ( count( $results['errors'] ) > 0 ) { // Unlike payment gateway suggestions, we don't have a non-empty default set of promotions to fall back to. diff --git a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php index b11d84f902f..d0a502d4844 100644 --- a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php +++ b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php @@ -24,7 +24,7 @@ namespace Automattic\WooCommerce\Internal\BatchProcessing; /** * Class BatchProcessingController * - * @package Automattic\WooCommerce\Internal\Updates. + * @package Automattic\WooCommerce\Internal\BatchProcessing. */ class BatchProcessingController { /* @@ -220,17 +220,19 @@ class BatchProcessingController { * @return array Current state for the processor, or a "blank" state if none exists yet. */ private function get_process_details( BatchProcessorInterface $batch_processor ): array { - return get_option( - $this->get_processor_state_option_name( $batch_processor ), - array( - 'total_time_spent' => 0, - 'current_batch_size' => $batch_processor->get_default_batch_size(), - 'last_error' => null, - 'recent_failures' => 0, - 'batch_first_failure' => null, - 'batch_last_failure' => null, - ) + $defaults = array( + 'total_time_spent' => 0, + 'current_batch_size' => $batch_processor->get_default_batch_size(), + 'last_error' => null, + 'recent_failures' => 0, + 'batch_first_failure' => null, + 'batch_last_failure' => null, ); + + $process_details = get_option( $this->get_processor_state_option_name( $batch_processor ) ); + $process_details = wp_parse_args( is_array( $process_details ) ? $process_details : array(), $defaults ); + + return $process_details; } /** @@ -349,7 +351,15 @@ class BatchProcessingController { * @return array List (of string) of the class names of the enqueued processors. */ public function get_enqueued_processors(): array { - return get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() ); + $enqueued_processors = get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() ); + + if ( ! is_array( $enqueued_processors ) ) { + $this->logger->error( 'Could not fetch list of processors. Clearing up queue.', array( 'source' => 'batch-processing' ) ); + delete_option( self::ENQUEUED_PROCESSORS_OPTION_NAME ); + $enqueued_processors = array(); + } + + return $enqueued_processors; } /** diff --git a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php index 79dee9f8adc..1dff6d124a2 100644 --- a/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php +++ b/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessorInterface.php @@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\BatchProcessing; /** * Interface BatchProcessorInterface * - * @package Automattic\WooCommerce\DataBase + * @package Automattic\WooCommerce\Internal\BatchProcessing */ interface BatchProcessorInterface { diff --git a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php index d80f6a4c94a..31057ab11bd 100644 --- a/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php +++ b/plugins/woocommerce/src/Internal/ComingSoon/ComingSoonRequestHandler.php @@ -5,7 +5,7 @@ use Automattic\WooCommerce\Admin\Features\Features; /** * Handles the template_include hook to determine whether the current page needs - * to be replaced with a comiing soon screen. + * to be replaced with a coming soon screen. */ class ComingSoonRequestHandler { @@ -185,7 +185,7 @@ class ComingSoonRequestHandler { foreach ( $fonts_to_add as $font_to_add ) { $found = false; foreach ( $font_data as $font ) { - if ( $font['name'] === $font_to_add['name'] ) { + if ( isset( $font['name'] ) && $font['name'] === $font_to_add['name'] ) { $found = true; break; } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index 3584b9106db..3345f6a61e5 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -548,7 +548,7 @@ class CustomOrdersTableController { $compatibility_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true ); $sync_complete = 0 === $this->data_synchronizer->get_current_orders_pending_sync_count(); $disabled = array(); - // Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner. + // Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner. $incompatible_plugins = $this->plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info ); $incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() ); if ( count( $incompatible_plugins ) > 0 ) { diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index c485bd15889..e0fb6c2b4c7 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -2586,7 +2586,7 @@ FROM $order_meta_table * @param \WC_Order $order Order object. */ public function update( &$order ) { - $previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' ); + $previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status', 'new' ); // Before updating, ensure date paid is set if missing. if ( @@ -2621,8 +2621,15 @@ FROM $order_meta_table $order->apply_changes(); $this->clear_caches( $order ); - // For backwards compatibility, moving a draft order to a valid status triggers the 'woocommerce_new_order' hook. - if ( ! empty( $changes['status'] ) && in_array( $previous_status, array( 'new', 'auto-draft', 'draft', 'checkout-draft' ), true ) ) { + $draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' ); + + // For backwards compatibility, this hook should be fired only if the new status is not one of the draft statuses and the previous status was one of the draft statuses. + if ( + ! empty( $changes['status'] ) + && $changes['status'] !== $previous_status + && ! in_array( $changes['status'], $draft_statuses, true ) + && in_array( $previous_status, $draft_statuses, true ) + ) { do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment return; } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php index 92c338b296d..8e02dfd0d3f 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner; +use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner; use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php index 58946dac50c..89f15221230 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/LoggingServiceProvider.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\WooCommerce\Internal\Admin\Logging\{ PageController, Settings }; use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; +use Automattic\WooCommerce\Internal\Logging\RemoteLogger; /** * LoggingServiceProvider class. @@ -19,6 +20,7 @@ class LoggingServiceProvider extends AbstractServiceProvider { FileController::class, PageController::class, Settings::class, + RemoteLogger::class, ); /** @@ -37,5 +39,7 @@ class LoggingServiceProvider extends AbstractServiceProvider { ); $this->share( Settings::class ); + + $this->share( RemoteLogger::class ); } } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php index 6b37c82918b..09837cf54b4 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php @@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Caches\OrderCache; use Automattic\WooCommerce\Caches\OrderCacheController; -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner; +use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner; use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController; use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableRefundDataStore; diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/StatsServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/StatsServiceProvider.php new file mode 100644 index 00000000000..793fdf9418b --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/StatsServiceProvider.php @@ -0,0 +1,33 @@ +add( McStats::class ); + } +} diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index c7b33c14e36..c332e769f65 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -247,6 +247,17 @@ class FeaturesController { 'is_legacy' => true, 'option_key' => CustomOrdersTableController::HPOS_FTS_INDEX_OPTION, ), + 'remote_logging' => array( + 'name' => __( 'Remote Logging', 'woocommerce' ), + 'description' => __( + 'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce', + 'woocommerce' + ), + 'enabled_by_default' => false, + 'disable_ui' => true, + 'is_legacy' => false, + 'is_experimental' => true, + ), ); foreach ( $legacy_features as $slug => $definition ) { diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php index 665ef1be5e9..1fc49acd1d1 100644 --- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php +++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php @@ -339,7 +339,8 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr 'order' => 20, 'attributes' => array( 'property' => 'global_unique_id', - 'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ), + // translators: %1$s GTIN %2$s UPC %3$s EAN %4$s ISBN. + 'label' => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '' . esc_html__( 'GTIN', 'woocommerce' ) . '', '' . esc_html__( 'UPC', 'woocommerce' ) . '', '' . esc_html__( 'EAN', 'woocommerce' ) . '', '' . esc_html__( 'ISBN', 'woocommerce' ) . '' ), 'tooltip' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ), ), ) diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php index 18a36daa681..79dde35c56d 100644 --- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php +++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php @@ -765,7 +765,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ 'order' => 20, 'attributes' => array( 'property' => 'global_unique_id', - 'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ), + // translators: %1$s GTIN %2$s UPC %3$s EAN %4$s ISBN. + 'label' => sprintf( __( '%1$s, %2$s, %3$s, or %4$s', 'woocommerce' ), '' . esc_html__( 'GTIN', 'woocommerce' ) . '', '' . esc_html__( 'UPC', 'woocommerce' ) . '', '' . esc_html__( 'EAN', 'woocommerce' ) . '', '' . esc_html__( 'ISBN', 'woocommerce' ) . '' ), 'tooltip' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ), ), 'disableConditions' => array( diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php new file mode 100644 index 00000000000..6d9cb9cf146 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php @@ -0,0 +1,128 @@ +is_tracking_opted_in() ) { + return false; + } + + if ( ! $this->is_variant_assignment_allowed() ) { + return false; + } + + if ( ! $this->is_latest_woocommerce_version() ) { + return false; + } + + return true; + } + + /** + * Check if the user has opted into tracking/logging. + * + * @return bool + */ + private function is_tracking_opted_in() { + return 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ); + } + + /** + * Check if the store is allowed to log based on the variant assignment percentage. + * + * @return bool + */ + private function is_variant_assignment_allowed() { + $assignment = get_option( 'woocommerce_remote_variant_assignment', 0 ); + return ( $assignment <= 12 ); // Considering 10% of the 0-120 range. + } + + /** + * Check if the current WooCommerce version is the latest. + * + * @return bool + */ + private function is_latest_woocommerce_version() { + $latest_wc_version = $this->fetch_latest_woocommerce_version(); + + if ( is_null( $latest_wc_version ) ) { + return false; + } + + return version_compare( WC()->version, $latest_wc_version, '>=' ); + } + + /** + * Fetch the latest WooCommerce version using the WordPress API and cache it. + * + * @return string|null + */ + private function fetch_latest_woocommerce_version() { + $cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT ); + if ( $cached_version ) { + return $cached_version; + } + + $retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY ); + if ( false === $retry_count || ! is_numeric( $retry_count ) ) { + $retry_count = 0; + } + + if ( $retry_count >= 3 ) { + return null; + } + + if ( ! function_exists( 'plugins_api' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + } + // Fetch the latest version from the WordPress API. + $plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) ); + + if ( is_wp_error( $plugin_info ) ) { + ++$retry_count; + set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS ); + return null; + } + + if ( ! empty( $plugin_info->version ) ) { + $latest_version = $plugin_info->version; + set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS ); + delete_transient( self::FETCH_LATEST_VERSION_RETRY ); + return $latest_version; + } + + return null; + } +} diff --git a/plugins/woocommerce/src/Internal/McStats.php b/plugins/woocommerce/src/Internal/McStats.php new file mode 100644 index 00000000000..89320e1ee6a --- /dev/null +++ b/plugins/woocommerce/src/Internal/McStats.php @@ -0,0 +1,63 @@ +get_current_stats(); + if ( isset( $stats[ $group_name ] ) && ! empty( $stats[ $group_name ] ) ) { + return array( "x_woocommerce-{$group_name}" => implode( ',', $stats[ $group_name ] ) ); + } + return array(); + } + + /** + * Outputs the tracking pixels for the current stats and empty the stored stats from the object + * + * @return void + */ + public function do_stats() { + if ( ! \WC_Site_Tracking::is_tracking_enabled() ) { + return; + } + + parent::do_stats(); + } + + /** + * Runs stats code for a one-off, server-side. + * + * @param string $url string The URL to be pinged. Should include `x_woocommerce-{$group}={$stats}` or whatever we want to store. + * + * @return bool If it worked. + */ + public function do_server_side_stat( $url ) { + if ( ! \WC_Site_Tracking::is_tracking_enabled() ) { + return false; + } + + return parent::do_server_side_stat( $url ); + } +} diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php index e9ba76b3b63..cf0b6325751 100644 --- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php +++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/LookupDataStore.php @@ -431,9 +431,14 @@ class LookupDataStore { * Create all the necessary lookup data for a given variation. * * @param \WC_Product_Variation $variation The variation to create entries for. + * @throws \Exception Can't retrieve the details of the parent product. */ private function create_data_for_variation( \WC_Product_Variation $variation ) { $main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() ); + if ( false === $main_product ) { + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + throw new \Exception( "The product is a variation, and the retrieval of data for the parent product (id {$variation->get_parent_id()}) failed." ); + } $product_attributes_data = $this->get_attribute_taxonomies( $main_product ); $variation_attributes_data = array_filter( diff --git a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php index 5bc479e3fd6..890856e062c 100644 --- a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php +++ b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingEngine.php @@ -198,8 +198,34 @@ class ReceiptRenderingEngine { */ $data['css'] = apply_filters( 'woocommerce_printable_order_receipt_css', $css, $order ); + $default_template_path = __DIR__ . '/Templates/order-receipt.php'; + + /** + * Filter the order receipt template path. + * + * @since 9.2.0 + * @hook wc_get_template + * @param string $template The template path. + * @param string $template_name The template name. + * @param array $args The available data for the template. + * @param string $template_path The template path. + * @param string $default_path The default template path. + */ + $template_path = apply_filters( + 'wc_get_template', + $default_template_path, + 'ReceiptRendering/order-receipt.php', + $data, + $default_template_path, + $default_template_path + ); + + if ( ! file_exists( $template_path ) ) { + $template_path = $default_template_path; + } + ob_start(); - include __DIR__ . '/Templates/order-receipt.php'; + include $template_path; $rendered_template = ob_get_contents(); ob_end_clean(); diff --git a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php index 3674da31e7b..d871559c360 100644 --- a/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php +++ b/plugins/woocommerce/src/Internal/ReceiptRendering/ReceiptRenderingRestController.php @@ -6,8 +6,8 @@ use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine; use \WP_REST_Server; use \WP_REST_Request; use \WP_Error; -use \InvalidArgumentException; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; +use Automattic\WooCommerce\Internal\RestApiControllerBase; /** * Controller for the REST endpoints associated to the receipt rendering engine. diff --git a/plugins/woocommerce/src/Internal/RestApiControllerBase.php b/plugins/woocommerce/src/Internal/RestApiControllerBase.php index a01fe7a682a..ca7921fcdf9 100644 --- a/plugins/woocommerce/src/Internal/RestApiControllerBase.php +++ b/plugins/woocommerce/src/Internal/RestApiControllerBase.php @@ -1,6 +1,6 @@ substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0 ) ); + $active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins(); + + return ! empty( + array_filter( + $active_valid_plugins, + fn( $plugin ) => substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0 + ) + ); } /** diff --git a/plugins/woocommerce/src/Internal/Utilities/Users.php b/plugins/woocommerce/src/Internal/Utilities/Users.php index 34880bac135..8c333498563 100644 --- a/plugins/woocommerce/src/Internal/Utilities/Users.php +++ b/plugins/woocommerce/src/Internal/Utilities/Users.php @@ -106,4 +106,66 @@ class Users { */ return (bool) apply_filters( 'woocommerce_order_email_verification_required', $email_verification_required, $order, $context ); } + + /** + * Site-specific method of retrieving the requested user meta. + * + * This is a multisite-aware wrapper around WordPress's own `get_user_meta()` function, and works by prefixing the + * supplied meta key with a blog-specific meta key. + * + * @param int $user_id User ID. + * @param string $key Optional. The meta key to retrieve. By default, returns data for all keys. + * @param bool $single Optional. Whether to return a single value. This parameter has no effect if `$key` is not + * specified. Default false. + * + * @return mixed An array of values if `$single` is false. The value of meta data field if `$single` is true. + * False for an invalid `$user_id` (non-numeric, zero, or negative value). An empty string if a valid + * but non-existing user ID is passed. + */ + public static function get_site_user_meta( int $user_id, string $key = '', bool $single = false ) { + global $wpdb; + $site_specific_key = $key . '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' ); + return get_user_meta( $user_id, $site_specific_key, true ); + } + + /** + * Site-specific means of updating user meta. + * + * This is a multisite-aware wrapper around WordPress's own `update_user_meta()` function, and works by prefixing + * the supplied meta key with a blog-specific meta key. + * + * @param int $user_id User ID. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + * @param mixed $prev_value Optional. Previous value to check before updating. If specified, only update existing + * metadata entries with this value. Otherwise, update all entries. Default empty. + * + * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure or if the value + * passed to the function is the same as the one that is already in the database. + */ + public static function update_site_user_meta( int $user_id, string $meta_key, $meta_value, $prev_value = '' ) { + global $wpdb; + $site_specific_key = $meta_key . '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' ); + return update_user_meta( $user_id, $site_specific_key, $meta_value, $prev_value ); + } + + /** + * Site-specific means of deleting user meta. + * + * This is a multisite-aware wrapper around WordPress's own `delete_user_meta()` function, and works by prefixing + * the supplied meta key with a blog-specific meta key. + * + * @param int $user_id User ID. + * @param string $meta_key Metadata name. + * @param mixed $meta_value Optional. Metadata value. If provided, rows will only be removed that match the value. + * Must be serializable if non-scalar. Default empty. + * + * @return bool True on success, false on failure. + * / + */ + public static function delete_site_user_meta( $user_id, $meta_key, $meta_value = '' ) { + global $wpdb; + $site_specific_key = $meta_key . '_' . rtrim( $wpdb->get_blog_prefix(), '_' ); + return delete_user_meta( $user_id, $site_specific_key, $meta_value ); + } } diff --git a/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php b/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php index e4a61272228..c6ce0f3637c 100644 --- a/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php +++ b/plugins/woocommerce/src/StoreApi/Formatters/MoneyFormatter.php @@ -8,13 +8,24 @@ namespace Automattic\WooCommerce\StoreApi\Formatters; */ class MoneyFormatter implements FormatterInterface { /** - * Format a given value and return the result. + * Format a given price value and return the result as a string without decimals. * - * @param mixed $value Value to format. - * @param array $options Options that influence the formatting. - * @return mixed + * @param int|float|string $value Value to format. Int is allowed, as it may also represent a valid price. + * @param array $options Options that influence the formatting. + * @return string */ public function format( $value, array $options = [] ) { + + if ( ! is_int( $value ) && ! is_string( $value ) && ! is_float( $value ) ) { + wc_doing_it_wrong( + __FUNCTION__, + 'Function expects a $value arg of type INT, STRING or FLOAT.', + '9.2' + ); + + return ''; + } + $options = wp_parse_args( $options, [ @@ -23,12 +34,20 @@ class MoneyFormatter implements FormatterInterface { ] ); - return (string) intval( - round( - ( (float) wc_format_decimal( $value ) ) * ( 10 ** absint( $options['decimals'] ) ), - 0, - absint( $options['rounding_mode'] ) - ) - ); + // Ensure rounding mode is valid. + $rounding_modes = [ PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD ]; + $options['rounding_mode'] = absint( $options['rounding_mode'] ); + if ( ! in_array( $options['rounding_mode'], $rounding_modes, true ) ) { + $options['rounding_mode'] = PHP_ROUND_HALF_UP; + } + + $value = floatval( $value ); + + // Remove the price decimal points for rounding purposes. + $value = $value * pow( 10, absint( $options['decimals'] ) ); + $value = round( $value, 0, $options['rounding_mode'] ); + + // This ensures returning the value as a string without decimal points ready for price parsing. + return wc_format_decimal( $value, 0, true ); } } diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php index 797df5f9eca..a760664327f 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php @@ -105,7 +105,6 @@ abstract class AbstractCartRoute extends AbstractRoute { */ public function get_response( \WP_REST_Request $request ) { $this->load_cart_session( $request ); - $this->cart_controller->calculate_totals(); $response = null; $nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null; @@ -332,13 +331,11 @@ abstract class AbstractCartRoute extends AbstractRoute { * @return \WP_Error WP Error object. */ protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) { - $additional_data['status'] = $http_status_code; // If there was a conflict, return the cart so the client can resolve it. if ( 409 === $http_status_code ) { - $cart = $this->cart_controller->get_cart_instance(); - $additional_data['cart'] = $this->cart_schema->get_item_response( $cart ); + $additional_data['cart'] = $this->cart_schema->get_item_response( $this->cart_controller->get_cart_for_response() ); } return new \WP_Error( $error_code, $error_message, $additional_data ); diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php index 00fff318066..87600688478 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractRoute.php @@ -6,8 +6,6 @@ use Automattic\WooCommerce\StoreApi\Routes\RouteInterface; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException; use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema; -use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; -use Automattic\WooCommerce\Blocks\Package; use WP_Error; /** diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php index 8cab713b2d7..3ec380321d0 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Cart.php @@ -56,6 +56,6 @@ class Cart extends AbstractCartRoute { * @return \WP_REST_Response */ protected function get_route_response( \WP_REST_Request $request ) { - return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_instance() ) ); + return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_for_response() ) ); } } diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php index 7a7191070d1..5237160c6b0 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartAddItem.php @@ -95,11 +95,9 @@ class CartAddItem extends AbstractCartRoute { protected function get_route_post_response( \WP_REST_Request $request ) { // Do not allow key to be specified during creation. if ( ! empty( $request['key'] ) ) { - throw new RouteException( 'woocommerce_rest_cart_item_exists', __( 'Cannot create an existing cart item.', 'woocommerce' ), 400 ); + throw new RouteException( 'woocommerce_rest_cart_item_exists', esc_html__( 'Cannot create an existing cart item.', 'woocommerce' ), 400 ); } - $cart = $this->cart_controller->get_cart_instance(); - /** * Filters cart item data sent via the API before it is passed to the cart controller. * @@ -128,7 +126,7 @@ class CartAddItem extends AbstractCartRoute { $this->cart_controller->add_to_cart( $add_to_cart_data ); - $response = rest_ensure_response( $this->schema->get_item_response( $cart ) ); + $response = rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_for_response() ) ); $response->set_status( 201 ); return $response; } diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php index 2d277395734..56fffb558b2 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartApplyCoupon.php @@ -64,7 +64,7 @@ class CartApplyCoupon extends AbstractCartRoute { */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_coupons_enabled() ) { - throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 ); + throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', esc_html__( 'Coupons are disabled.', 'woocommerce' ), 404 ); } $coupon_code = wc_format_coupon_code( wp_unslash( $request['code'] ) ); @@ -72,10 +72,9 @@ class CartApplyCoupon extends AbstractCartRoute { try { $this->cart_controller->apply_coupon( $coupon_code ); } catch ( \WC_REST_Exception $e ) { - throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); + throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } - $cart = $this->cart_controller->get_cart_instance(); - return rest_ensure_response( $this->schema->get_item_response( $cart ) ); + return rest_ensure_response( $this->schema->get_item_response( $this->cart_controller->get_cart_for_response() ) ); } } diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php index 97fd807c520..879f20534f3 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartCouponsByCode.php @@ -79,7 +79,7 @@ class CartCouponsByCode extends AbstractCartRoute { */ protected function get_route_response( \WP_REST_Request $request ) { if ( ! $this->cart_controller->has_coupon( $request['code'] ) ) { - throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 ); + throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', esc_html__( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 ); } return $this->prepare_item_for_response( $request['code'], $request ); @@ -94,13 +94,11 @@ class CartCouponsByCode extends AbstractCartRoute { */ protected function get_route_delete_response( \WP_REST_Request $request ) { if ( ! $this->cart_controller->has_coupon( $request['code'] ) ) { - throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 ); + throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', esc_html__( 'Coupon does not exist in the cart.', 'woocommerce' ), 404 ); } $cart = $this->cart_controller->get_cart_instance(); - $cart->remove_coupon( $request['code'] ); - $cart->calculate_totals(); return new \WP_REST_Response( null, 204 ); } diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php index 4050c211c0d..2bd81b283be 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartRemoveCoupon.php @@ -64,24 +64,24 @@ class CartRemoveCoupon extends AbstractCartRoute { */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_coupons_enabled() ) { - throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 ); + throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', esc_html__( 'Coupons are disabled.', 'woocommerce' ), 404 ); } $cart = $this->cart_controller->get_cart_instance(); $coupon_code = wc_format_coupon_code( $request['code'] ); $coupon = new \WC_Coupon( $coupon_code ); + $discounts = new \WC_Discounts( $cart ); - if ( $coupon->get_code() !== $coupon_code || ! $coupon->is_valid() ) { - throw new RouteException( 'woocommerce_rest_cart_coupon_error', __( 'Invalid coupon code.', 'woocommerce' ), 400 ); + if ( $coupon->get_code() !== $coupon_code || is_wp_error( $discounts->is_coupon_valid( $coupon ) ) ) { + throw new RouteException( 'woocommerce_rest_cart_coupon_error', esc_html__( 'Invalid coupon code.', 'woocommerce' ), 400 ); } if ( ! $this->cart_controller->has_coupon( $coupon_code ) ) { - throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', __( 'Coupon cannot be removed because it is not already applied to the cart.', 'woocommerce' ), 409 ); + throw new RouteException( 'woocommerce_rest_cart_coupon_invalid_code', esc_html__( 'Coupon cannot be removed because it is not already applied to the cart.', 'woocommerce' ), 409 ); } $cart = $this->cart_controller->get_cart_instance(); $cart->remove_coupon( $coupon_code ); - $cart->calculate_totals(); return rest_ensure_response( $this->schema->get_item_response( $cart ) ); } diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php index 44f2d534d76..7fd63372c6e 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php @@ -70,11 +70,11 @@ class CartSelectShippingRate extends AbstractCartRoute { */ protected function get_route_post_response( \WP_REST_Request $request ) { if ( ! wc_shipping_enabled() ) { - throw new RouteException( 'woocommerce_rest_shipping_disabled', __( 'Shipping is disabled.', 'woocommerce' ), 404 ); + throw new RouteException( 'woocommerce_rest_shipping_disabled', esc_html__( 'Shipping is disabled.', 'woocommerce' ), 404 ); } if ( ! isset( $request['rate_id'] ) ) { - throw new RouteException( 'woocommerce_rest_cart_missing_rate_id', __( 'Invalid Rate ID.', 'woocommerce' ), 400 ); + throw new RouteException( 'woocommerce_rest_cart_missing_rate_id', esc_html__( 'Invalid Rate ID.', 'woocommerce' ), 400 ); } $cart = $this->cart_controller->get_cart_instance(); @@ -90,7 +90,7 @@ class CartSelectShippingRate extends AbstractCartRoute { } } } catch ( \WC_Rest_Exception $e ) { - throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); + throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } /** @@ -107,7 +107,6 @@ class CartSelectShippingRate extends AbstractCartRoute { */ do_action( 'woocommerce_store_api_cart_select_shipping_rate', $package_id, $rate_id, $request ); - $cart->calculate_shipping(); $cart->calculate_totals(); return rest_ensure_response( $this->cart_schema->get_item_response( $cart ) ); diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php index 86fa79596d0..782298d2d9c 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php @@ -2,7 +2,6 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1; use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait; -use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils; /** * CartUpdateCustomer class. diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php index 5d6f34bb1bf..dbdf838fb28 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php @@ -2,11 +2,9 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1; use Automattic\WooCommerce\StoreApi\Payments\PaymentResult; -use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException; use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException; use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait; -use Automattic\WooCommerce\Checkout\Helpers\ReserveStock; use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException; use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait; use Automattic\WooCommerce\Utilities\RestApiUtil; @@ -88,7 +86,7 @@ class Checkout extends AbstractCartRoute { 'permission_callback' => '__return_true', 'args' => array_merge( [ - 'payment_data' => [ + 'payment_data' => [ 'description' => __( 'Data to pass through to the payment method when processing payment.', 'woocommerce' ), 'type' => 'array', 'items' => [ @@ -103,6 +101,10 @@ class Checkout extends AbstractCartRoute { ], ], ], + 'customer_password' => [ + 'description' => __( 'Customer password for new accounts, if applicable.', 'woocommerce' ), + 'type' => 'string', + ], ], $this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ) ), @@ -121,7 +123,6 @@ class Checkout extends AbstractCartRoute { */ public function get_response( \WP_REST_Request $request ) { $this->load_cart_session( $request ); - $this->cart_controller->calculate_totals(); $response = null; $nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null; @@ -144,6 +145,11 @@ class Checkout extends AbstractCartRoute { if ( is_wp_error( $response ) ) { $response = $this->error_to_response( $response ); + + // If we encountered an exception, free up stock. + if ( $this->order ) { + wc_release_stock_for_order( $this->order ); + } } return $this->add_response_headers( $response ); @@ -178,7 +184,6 @@ class Checkout extends AbstractCartRoute { * 5. Process Payment * * @throws RouteException On error. - * @throws InvalidStockLevelsInCartException On error. * * @param \WP_REST_Request $request Request object. * @@ -186,35 +191,62 @@ class Checkout extends AbstractCartRoute { */ protected function get_route_post_response( \WP_REST_Request $request ) { /** - * Validate items etc are allowed in the order before the order is processed. This will fix violations and tell - * the customer. + * Before triggering validation, ensure totals are current and in turn, things such as shipping costs are present. + * This is so plugins that validate other cart data (e.g. conditional shipping and payments) can access this data. + */ + $this->cart_controller->calculate_totals(); + + /** + * Validate items and fix violations before the order is processed. */ $this->cart_controller->validate_cart(); /** - * Obtain Draft Order and process request data. - * - * Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart + * Persist customer session data from the request first so that OrderController::update_addresses_from_cart * uses the up to date customer address. */ $this->update_customer_from_request( $request ); - $this->create_or_update_draft_order( $request ); - $this->update_order_from_request( $request ); /** - * Process customer data. - * - * Update order with customer details, and sign up a user account as necessary. + * Create (or update) Draft Order and process request data. */ + $this->create_or_update_draft_order( $request ); + $this->update_order_from_request( $request ); $this->process_customer( $request ); /** - * Validate order. - * - * This logic ensures the order is valid before payment is attempted. + * Validate updated order before payment is attempted. */ $this->order_controller->validate_order_before_payment( $this->order ); + /** + * Reserve stock for the order. + * + * In the shortcode based checkout, when POSTing the checkout form the order would be created and fire the + * `woocommerce_checkout_order_created` action. This in turn would trigger the `wc_reserve_stock_for_order` + * function so that stock would be held pending payment. + * + * Via the block based checkout and Store API we already have a draft order, but when POSTing to the /checkout + * endpoint we do the same; reserve stock for the order to allow time to process payment. + * + * Note, stock is only "held" while the order has the status wc-checkout-draft or pending. Stock is freed when + * the order changes status, or there is an exception. + * + * @see ReserveStock::get_query_for_reserved_stock() + * + * @since 9.2 Stock is no longer held for all draft orders, nor on non-POST requests. See https://github.com/woocommerce/woocommerce/issues/44231 + * @since 9.2 Uses wc_reserve_stock_for_order() instead of using the ReserveStock class directly. + */ + try { + wc_reserve_stock_for_order( $this->order ); + } catch ( ReserveStockException $e ) { + throw new RouteException( + esc_html( $e->getErrorCode() ), + esc_html( $e->getMessage() ), + esc_html( $e->getCode() ) + ); + } + wc_do_deprecated_action( '__experimental_woocommerce_blocks_checkout_order_processed', array( @@ -393,24 +425,6 @@ class Checkout extends AbstractCartRoute { // Store order ID to session. $this->set_draft_order_id( $this->order->get_id() ); - - /** - * Try to reserve stock for the order. - * - * If creating a draft order on checkout entry, set the timeout to 10 mins. - * If POSTing to the checkout (attempting to pay), set the timeout to 60 mins (using the woocommerce_hold_stock_minutes option). - */ - try { - $reserve_stock = new ReserveStock(); - $duration = $request->get_method() === 'POST' ? (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) : 10; - $reserve_stock->reserve_stock_for_order( $this->order, $duration ); - } catch ( ReserveStockException $e ) { - throw new RouteException( - $e->getErrorCode(), - $e->getMessage(), - $e->getCode() - ); - } } /** @@ -524,7 +538,8 @@ class Checkout extends AbstractCartRoute { $customer_id = $this->create_customer_account( $request['billing_address']['email'], $request['billing_address']['first_name'], - $request['billing_address']['last_name'] + $request['billing_address']['last_name'], + $request['customer_password'] ); // Associate customer with the order. This is done before login to ensure the order is associated with @@ -547,13 +562,23 @@ class Checkout extends AbstractCartRoute { case 'registration-error-invalid-email': throw new RouteException( 'registration-error-invalid-email', - __( 'Please provide a valid email address.', 'woocommerce' ), + esc_html__( 'Please provide a valid email address.', 'woocommerce' ), 400 ); case 'registration-error-email-exists': throw new RouteException( 'registration-error-email-exists', - __( 'An account is already registered with your email address. Please log in before proceeding.', 'woocommerce' ), + sprintf( + // Translators: %s Email address. + esc_html__( 'An account is already registered with %s. Please log in or use a different email address.', 'woocommerce' ), + esc_html( $request['billing_address']['email'] ) + ), + 400 + ); + case 'registration-error-empty-password': + throw new RouteException( + 'registration-error-empty-password', + esc_html__( 'Please create a password for your account.', 'woocommerce' ), 400 ); } @@ -608,10 +633,11 @@ class Checkout extends AbstractCartRoute { * @param string $user_email The email address to use for the new account. * @param string $first_name The first name to use for the new account. * @param string $last_name The last name to use for the new account. + * @param string $password The password to use for the new account. If empty, a password will be generated. * * @return int User id if successful */ - private function create_customer_account( $user_email, $first_name, $last_name ) { + private function create_customer_account( $user_email, $first_name, $last_name, $password = '' ) { if ( empty( $user_email ) || ! is_email( $user_email ) ) { throw new \Exception( 'registration-error-invalid-email' ); } @@ -620,11 +646,20 @@ class Checkout extends AbstractCartRoute { throw new \Exception( 'registration-error-email-exists' ); } - $username = wc_create_new_customer_username( $user_email ); + // Handle password creation if not provided. + if ( empty( $password ) ) { + $password = wp_generate_password(); + $password_generated = true; + } else { + $password_generated = false; + } - // Handle password creation. - $password = wp_generate_password(); - $password_generated = true; + // This ensures `wp_generate_password` returned something (it is filterable and could be empty string). + if ( empty( $password ) ) { + throw new \Exception( 'registration-error-empty-password' ); + } + + $username = wc_create_new_customer_username( $user_email ); // Use WP_Error to handle registration errors. $errors = new \WP_Error(); diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php index 41d16a07ed5..96a0e37ce34 100644 --- a/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php +++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Patterns.php @@ -2,6 +2,7 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1; +use Automattic\WooCommerce\Blocks\BlockPatterns; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Patterns\PTKClient; use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore; @@ -114,7 +115,11 @@ class Patterns extends AbstractRoute { protected function get_route_post_response( WP_REST_Request $request ) { $ptk_patterns_store = Package::container()->get( PTKPatternsStore::class ); - $ptk_patterns_store->fetch_patterns(); + $patterns = $ptk_patterns_store->fetch_patterns(); + + $block_patterns = Package::container()->get( BlockPatterns::class ); + + $block_patterns->register_ptk_patterns( $patterns ); return rest_ensure_response( array( diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php index 3d77a9e2aaa..2ecf7ebc3e7 100644 --- a/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php +++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractAddressSchema.php @@ -172,6 +172,13 @@ abstract class AbstractAddressSchema extends AbstractSchema { // correct format, and finally the second validation step is to ensure the correctly-formatted values // match what we expect (postcode etc.). foreach ( $address as $key => $value ) { + + // Only run specific validation on properties that are defined in the schema and present in the address. + // This is for partial address pushes when only part of a customer address is sent. + // Full schema address validation still happens later, so empty, required values are disallowed. + if ( empty( $schema[ $key ] ) || empty( $address[ $key ] ) ) { + continue; + } if ( is_wp_error( rest_validate_value_from_schema( $value, $schema[ $key ], $key ) ) ) { $errors->add( 'invalid_' . $key, diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php index d61c84d5af5..95a1694b1f8 100644 --- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php +++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CheckoutSchema.php @@ -319,6 +319,9 @@ class CheckoutSchema extends AbstractSchema { }, $field['options'] ); + if ( true !== $field['required'] ) { + $field_schema['enum'][] = ''; + } } if ( 'checkbox' === $field['type'] ) { diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php index 5757c1649a4..a272cba8b9e 100644 --- a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php +++ b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php @@ -12,7 +12,6 @@ use Automattic\WooCommerce\StoreApi\Utilities\ArrayUtils; use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait; use Automattic\WooCommerce\StoreApi\Utilities\NoticeHandler; use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits; -use Automattic\WooCommerce\Blocks\Package; use WP_Error; /** @@ -27,20 +26,40 @@ class CartController { * Makes the cart and sessions available to a route by loading them from core. */ public function load_cart() { - if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) { - wc_load_cart(); + if ( did_action( 'woocommerce_load_cart_from_session' ) ) { + return; } + + // Initialize the cart. + wc_load_cart(); + + // Load cart from session. + $cart = $this->get_cart_instance(); + $cart->get_cart(); } /** - * Recalculates the cart totals. + * Gets the latest cart instance, and ensures totals have been calculated before returning. + * + * @return \WC_Cart + */ + public function get_cart_for_response() { + return did_action( 'woocommerce_after_calculate_totals' ) ? $this->get_cart_instance() : $this->calculate_totals(); + } + + /** + * Recalculates the cart totals and returns the updated cart instance. + * + * @since 9.2.0 Calculate shipping was removed here because it's called already by calculate_totals. + * + * @return \WC_Cart */ public function calculate_totals() { $cart = $this->get_cart_instance(); $cart->get_cart(); $cart->calculate_fees(); - $cart->calculate_shipping(); $cart->calculate_totals(); + return $cart; } /** @@ -188,19 +207,19 @@ class CartController { $cart_item = $this->get_cart_item( $item_id ); if ( empty( $cart_item ) ) { - throw new RouteException( 'woocommerce_rest_cart_invalid_key', __( 'Cart item does not exist.', 'woocommerce' ), 409 ); + throw new RouteException( 'woocommerce_rest_cart_invalid_key', esc_html__( 'Cart item does not exist.', 'woocommerce' ), 409 ); } $product = $cart_item['data']; if ( ! $product instanceof \WC_Product ) { - throw new RouteException( 'woocommerce_rest_cart_invalid_product', __( 'Cart item is invalid.', 'woocommerce' ), 404 ); + throw new RouteException( 'woocommerce_rest_cart_invalid_product', esc_html__( 'Cart item is invalid.', 'woocommerce' ), 404 ); } $quantity_validation = ( new QuantityLimits() )->validate_cart_item_quantity( $quantity, $cart_item ); if ( is_wp_error( $quantity_validation ) ) { - throw new RouteException( $quantity_validation->get_error_code(), $quantity_validation->get_error_message(), 400 ); + throw new RouteException( $quantity_validation->get_error_code(), $quantity_validation->get_error_message(), 400 ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } $cart = $this->get_cart_instance(); @@ -725,7 +744,7 @@ class CartController { $cart = wc()->cart; if ( ! $cart || ! $cart instanceof \WC_Cart ) { - throw new RouteException( 'woocommerce_rest_cart_error', __( 'Unable to retrieve cart.', 'woocommerce' ), 500 ); + throw new RouteException( 'woocommerce_rest_cart_error', esc_html__( 'Unable to retrieve cart.', 'woocommerce' ), 500 ); } return $cart; @@ -824,7 +843,7 @@ class CartController { // Add extra package data to array. $packages = array_map( - function( $key, $package, $index ) { + function ( $key, $package, $index ) { $package['package_id'] = isset( $package['package_id'] ) ? $package['package_id'] : $key; $package['package_name'] = isset( $package['package_name'] ) ? $package['package_name'] : $this->get_package_name( $package, $index ); return $package; @@ -909,7 +928,7 @@ class CartController { 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %s coupon code */ - __( '"%s" is an invalid coupon code.', 'woocommerce' ), + esc_html__( '"%s" is an invalid coupon code.', 'woocommerce' ), esc_html( $coupon_code ) ), 400 @@ -921,7 +940,7 @@ class CartController { 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %s coupon code */ - __( 'Coupon code "%s" has already been applied.', 'woocommerce' ), + esc_html__( 'Coupon code "%s" has already been applied.', 'woocommerce' ), esc_html( $coupon_code ) ), 400 @@ -942,7 +961,7 @@ class CartController { // Prevents new coupons being added if individual use coupons are already in the cart. $individual_use_coupons = $this->get_cart_coupons( - function( $code ) { + function ( $code ) { $coupon = new \WC_Coupon( $code ); return $coupon->get_individual_use(); } @@ -969,8 +988,8 @@ class CartController { 'woocommerce_rest_cart_coupon_error', sprintf( /* translators: %s: coupon code */ - __( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), - $code + esc_html__( '"%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), + esc_html( $code ) ), 400 ); diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php index 505d7e10f21..71bc49f7d43 100644 --- a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php +++ b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php @@ -142,7 +142,7 @@ trait CheckoutTrait { * @param \WP_REST_Request $request Full details about the request. */ private function update_order_from_request( \WP_REST_Request $request ) { - $this->order->set_customer_note( $request['customer_note'] ?? '' ); + $this->order->set_customer_note( wc_sanitize_textarea( $request['customer_note'] ) ?? '' ); $this->order->set_payment_method( $this->get_request_payment_method_id( $request ) ); $this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) ); $this->persist_additional_fields_for_order( $request ); diff --git a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php index e7413b35b1c..4ef0342b28d 100644 --- a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php +++ b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php @@ -100,7 +100,6 @@ class OrderController { // Ensure cart is current. if ( $update_totals ) { - wc()->cart->calculate_shipping(); wc()->cart->calculate_totals(); } diff --git a/plugins/woocommerce/src/StoreApi/docs/checkout.md b/plugins/woocommerce/src/StoreApi/docs/checkout.md index ef09146e411..418bca01b29 100644 --- a/plugins/woocommerce/src/StoreApi/docs/checkout.md +++ b/plugins/woocommerce/src/StoreApi/docs/checkout.md @@ -88,6 +88,7 @@ POST /wc/store/v1/checkout | `customer_note` | string | No | Note added to the order by the customer during checkout. | | `payment_method` | string | Yes | The ID of the payment method being used to process the payment. | | `payment_data` | array | No | Data to pass through to the payment method when processing payment. | +| `customer_password`| string | No | Optionally define a password for new accounts. | ```sh curl --header "Nonce: 12345" --request POST https://example-store.com/wp-json/wc/store/v1/checkout?payment_method=paypal&payment_data[0][key]=test-key&payment_data[0][value]=test-value diff --git a/plugins/woocommerce/src/Utilities/OrderUtil.php b/plugins/woocommerce/src/Utilities/OrderUtil.php index 8e94cbb8a9c..07525be6adb 100644 --- a/plugins/woocommerce/src/Utilities/OrderUtil.php +++ b/plugins/woocommerce/src/Utilities/OrderUtil.php @@ -228,4 +228,19 @@ final class OrderUtil { return $count_per_status; } + /** + * Removes the 'wc-' prefix from status. + * + * @param string $status The status to remove the prefix from. + * + * @return string The status without the prefix. + * @since 9.2.0 + */ + public static function remove_status_prefix( string $status ): string { + if ( strpos( $status, 'wc-' ) === 0 ) { + $status = substr( $status, 3 ); + } + + return $status; + } } diff --git a/plugins/woocommerce/src/Utilities/PluginUtil.php b/plugins/woocommerce/src/Utilities/PluginUtil.php index 6bd063e86e3..e3c229a64fb 100644 --- a/plugins/woocommerce/src/Utilities/PluginUtil.php +++ b/plugins/woocommerce/src/Utilities/PluginUtil.php @@ -67,6 +67,34 @@ class PluginUtil { require_once ABSPATH . WPINC . '/plugin.php'; } + /** + * Wrapper for WP's private `wp_get_active_and_valid_plugins` and `wp_get_active_network_plugins` functions. + * + * This combines the results of the two functions to get a list of all plugins that are active within a site. + * It's more useful than just retrieving the option values because it also validates that the plugin files exist. + * This wrapper is also a hedge against backward-incompatible changes since both of the WP methods are marked as + * being "@access private", so if need be we can update our methods here to preserve functionality. + * + * Note that the doc block for `wp_get_active_and_valid_plugins` says it returns "Array of paths to plugin files + * relative to the plugins directory", but it actually returns absolute paths. + * + * @return string[] Array of absolute paths to plugin files. + */ + public function get_all_active_valid_plugins() { + $local = wp_get_active_and_valid_plugins(); + + if ( is_multisite() ) { + require_once ABSPATH . WPINC . '/ms-load.php'; + $network = wp_get_active_network_plugins(); + } else { + $network = array(); + } + + $all = array_merge( $local, $network ); + + return array_unique( $all ); + } + /** * Get a list with the names of the WordPress plugins that are WooCommerce aware * (they have a "WC tested up to" header). diff --git a/plugins/woocommerce/templates/cart/mini-cart.php b/plugins/woocommerce/templates/cart/mini-cart.php index 34b3b021d3c..ecf51eb96eb 100644 --- a/plugins/woocommerce/templates/cart/mini-cart.php +++ b/plugins/woocommerce/templates/cart/mini-cart.php @@ -14,7 +14,7 @@ * * @see https://woocommerce.com/document/template-structure/ * @package WooCommerce\Templates - * @version 7.9.0 + * @version 9.2.0 */ defined( 'ABSPATH' ) || exit; @@ -47,13 +47,15 @@ do_action( 'woocommerce_before_mini_cart' ); ?> echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'woocommerce_cart_item_remove_link', sprintf( - '×', + '×', esc_url( wc_get_cart_remove_url( $cart_item_key ) ), /* translators: %s is the product name */ esc_attr( sprintf( __( 'Remove %s from cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) ), esc_attr( $product_id ), esc_attr( $cart_item_key ), - esc_attr( $_product->get_sku() ) + esc_attr( $_product->get_sku() ), + /* translators: %s is the product name */ + esc_attr( sprintf( __( '“%s” has been removed from your cart', 'woocommerce' ), wp_strip_all_tags( $product_name ) ) ) ), $cart_item_key ); diff --git a/plugins/woocommerce/templates/emails/customer-reset-password.php b/plugins/woocommerce/templates/emails/customer-reset-password.php index d2b62cfc10f..8549bcdfa79 100644 --- a/plugins/woocommerce/templates/emails/customer-reset-password.php +++ b/plugins/woocommerce/templates/emails/customer-reset-password.php @@ -12,7 +12,7 @@ * * @see https://woocommerce.com/document/template-structure/ * @package WooCommerce\Templates\Emails - * @version 4.0.0 + * @version 9.3.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -31,7 +31,7 @@ if ( ! defined( 'ABSPATH' ) ) {

    - +

    diff --git a/plugins/woocommerce/templates/emails/email-order-details.php b/plugins/woocommerce/templates/emails/email-order-details.php index ece102e9dbc..fc689dd52c4 100644 --- a/plugins/woocommerce/templates/emails/email-order-details.php +++ b/plugins/woocommerce/templates/emails/email-order-details.php @@ -78,7 +78,7 @@ do_action( 'woocommerce_email_before_order_table', $order, $sent_to_admin, $plai ?>