plugin updates
This commit is contained in:
@@ -100,11 +100,6 @@ class Init {
|
||||
'Automattic\WooCommerce\Admin\API\ShippingPartnerSuggestions',
|
||||
);
|
||||
|
||||
$product_form_controllers = array();
|
||||
if ( Features::is_enabled( 'new-product-management-experience' ) ) {
|
||||
$product_form_controllers[] = 'Automattic\WooCommerce\Admin\API\ProductForm';
|
||||
}
|
||||
|
||||
if ( Features::is_enabled( 'launch-your-store' ) ) {
|
||||
$controllers[] = 'Automattic\WooCommerce\Admin\API\LaunchYourStore';
|
||||
}
|
||||
@@ -138,7 +133,7 @@ class Init {
|
||||
|
||||
// The performance indicators controller must be registered last, after other /stats endpoints have been registered.
|
||||
$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
|
||||
$controllers = array_merge( $controllers, $analytics_controllers, $product_form_controllers );
|
||||
$controllers = array_merge( $controllers, $analytics_controllers );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,42 @@ class LaunchYourStore {
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/survey-completed',
|
||||
array(
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'has_survey_completed' ),
|
||||
'permission_callback' => array( $this, 'must_be_shop_manager_or_admin' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/woopayments/test-orders/count',
|
||||
array(
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_woopay_test_orders_count' ),
|
||||
'permission_callback' => array( $this, 'must_be_shop_manager_or_admin' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/woopayments/test-orders',
|
||||
array(
|
||||
array(
|
||||
'methods' => 'DELETE',
|
||||
'callback' => array( $this, 'delete_woopay_test_orders' ),
|
||||
'permission_callback' => array( $this, 'must_be_shop_manager_or_admin' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +150,55 @@ class LaunchYourStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the test orders created during Woo Payments test mode.
|
||||
*
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_woopay_test_orders_count() {
|
||||
$return = function ( $count ) {
|
||||
return new \WP_REST_Response( array( 'count' => $count ) );
|
||||
};
|
||||
|
||||
$orders = wc_get_orders(
|
||||
array(
|
||||
// phpcs:ignore
|
||||
'meta_key' => '_wcpay_mode',
|
||||
// phpcs:ignore
|
||||
'meta_value' => 'test',
|
||||
'return' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
return $return( count( $orders ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete WooPayments test orders.
|
||||
*
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function delete_woopay_test_orders() {
|
||||
$return = function ( $status = 204 ) {
|
||||
return new \WP_REST_Response( null, $status );
|
||||
};
|
||||
|
||||
$orders = wc_get_orders(
|
||||
array(
|
||||
// phpcs:ignore
|
||||
'meta_key' => '_wcpay_mode',
|
||||
// phpcs:ignore
|
||||
'meta_value' => 'test',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $orders as $order ) {
|
||||
$order->delete();
|
||||
}
|
||||
|
||||
return $return();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update woocommerce_admin_launch_your_store_survey_completed to yes or no
|
||||
*
|
||||
@@ -125,4 +210,13 @@ class LaunchYourStore {
|
||||
update_option( 'woocommerce_admin_launch_your_store_survey_completed', $request->get_param( 'status' ) );
|
||||
return new \WP_REST_Response();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return woocommerce_admin_launch_your_store_survey_completed option.
|
||||
*
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function has_survey_completed() {
|
||||
return new \WP_REST_Response( get_option( 'woocommerce_admin_launch_your_store_survey_completed', 'no' ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,8 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
$core_themes = array(
|
||||
array(
|
||||
'name' => 'Twenty Twenty-Four',
|
||||
'price' => 'Free',
|
||||
'price' => __( 'Free', 'woocommerce' ),
|
||||
'is_free' => true,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Black and white',
|
||||
@@ -285,7 +286,9 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
),
|
||||
array(
|
||||
'name' => 'Highline',
|
||||
'price' => '$79/year',
|
||||
/* translators: %d: price */
|
||||
'price' => sprintf( __( '$%d/year', 'woocommerce' ), 79 ),
|
||||
'is_free' => false,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Primary',
|
||||
@@ -315,7 +318,9 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
),
|
||||
array(
|
||||
'name' => 'Luminate',
|
||||
'price' => '$79/year',
|
||||
/* translators: %d: price */
|
||||
'price' => sprintf( __( '$%d/year', 'woocommerce' ), 79 ),
|
||||
'is_free' => false,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Primary',
|
||||
@@ -345,7 +350,9 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
),
|
||||
array(
|
||||
'name' => 'Nokul',
|
||||
'price' => '$79/year',
|
||||
/* translators: %d: price */
|
||||
'price' => sprintf( __( '$%d/year', 'woocommerce' ), 79 ),
|
||||
'is_free' => false,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Foreground and background',
|
||||
@@ -380,7 +387,8 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
$default_themes = array(
|
||||
array(
|
||||
'name' => 'Tsubaki',
|
||||
'price' => 'Free',
|
||||
'price' => __( 'Free', 'woocommerce' ),
|
||||
'is_free' => true,
|
||||
'color_palettes' => array(),
|
||||
'total_palettes' => 0,
|
||||
'slug' => 'tsubaki',
|
||||
@@ -389,7 +397,8 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
),
|
||||
array(
|
||||
'name' => 'Tazza',
|
||||
'price' => 'Free',
|
||||
'price' => __( 'Free', 'woocommerce' ),
|
||||
'is_free' => true,
|
||||
'color_palettes' => array(),
|
||||
'total_palettes' => 0,
|
||||
'slug' => 'tazza',
|
||||
@@ -398,7 +407,8 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
),
|
||||
array(
|
||||
'name' => 'Amulet',
|
||||
'price' => 'Free',
|
||||
'price' => __( 'Free', 'woocommerce' ),
|
||||
'is_free' => true,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Default',
|
||||
@@ -428,7 +438,8 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
),
|
||||
array(
|
||||
'name' => 'Zaino',
|
||||
'price' => 'Free',
|
||||
'price' => __( 'Free', 'woocommerce' ),
|
||||
'is_free' => true,
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Default',
|
||||
@@ -578,6 +589,10 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
'type' => 'string',
|
||||
'description' => 'Price',
|
||||
),
|
||||
'is_free' => array(
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether theme is free',
|
||||
),
|
||||
'is_active' => array(
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether theme is active',
|
||||
|
||||
@@ -168,21 +168,17 @@ class Options extends \WC_REST_Data_Controller {
|
||||
'woocommerce_shipping_dismissed_timestamp',
|
||||
'woocommerce_allow_tracking',
|
||||
'woocommerce_task_list_keep_completed',
|
||||
'woocommerce_task_list_prompt_shown',
|
||||
'woocommerce_default_homepage_layout',
|
||||
'woocommerce_setup_jetpack_opted_in',
|
||||
'woocommerce_no_sales_tax',
|
||||
'woocommerce_calc_taxes',
|
||||
'woocommerce_bacs_settings',
|
||||
'woocommerce_bacs_accounts',
|
||||
'woocommerce_task_list_prompt_shown',
|
||||
'woocommerce_settings_shipping_recommendations_hidden',
|
||||
'woocommerce_task_list_dismissed_tasks',
|
||||
'woocommerce_setting_payments_recommendations_hidden',
|
||||
'woocommerce_navigation_favorites_tooltip_hidden',
|
||||
'woocommerce_admin_transient_notices_queue',
|
||||
'woocommerce_task_list_welcome_modal_dismissed',
|
||||
'woocommerce_welcome_from_calypso_modal_dismissed',
|
||||
'woocommerce_task_list_hidden',
|
||||
'woocommerce_task_list_complete',
|
||||
'woocommerce_extended_task_list_hidden',
|
||||
@@ -198,6 +194,7 @@ class Options extends \WC_REST_Data_Controller {
|
||||
'woocommerce_admin_reviewed_store_location_settings',
|
||||
'woocommerce_ces_product_feedback_shown',
|
||||
'woocommerce_marketing_overview_multichannel_banner_dismissed',
|
||||
'woocommerce_manage_stock',
|
||||
'woocommerce_dimension_unit',
|
||||
'woocommerce_weight_unit',
|
||||
'woocommerce_product_editor_show_feedback_bar',
|
||||
|
||||
@@ -39,7 +39,6 @@ class Features {
|
||||
*/
|
||||
protected static $beta_features = array(
|
||||
'navigation',
|
||||
'new-product-management-experience',
|
||||
'settings',
|
||||
);
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ class LaunchYourStore {
|
||||
$link
|
||||
);
|
||||
// phpcs:ignore
|
||||
echo "<div id='coming-soon-footer-banner'>$text<a class='coming-soon-footer-banner-dismiss' data-rest-url='$rest_url' data-rest-nonce='$rest_nonce'></a></div>";
|
||||
echo "<div id='coming-soon-footer-banner'><div class='coming-soon-footer-banner__content'>$text</div><a class='coming-soon-footer-banner-dismiss' data-rest-url='$rest_url' data-rest-nonce='$rest_nonce'></a></div>";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -213,17 +213,6 @@ class CoreMenu {
|
||||
);
|
||||
}
|
||||
|
||||
$add_product_mvp = array();
|
||||
if ( Features::is_enabled( 'new-product-management-experience' ) ) {
|
||||
$add_product_mvp = array(
|
||||
'id' => 'woocommerce-add-product-mbp',
|
||||
'title' => __( 'Add New (MVP)', 'woocommerce' ),
|
||||
'url' => 'admin.php?page=wc-admin&path=/add-product',
|
||||
'parent' => 'woocommerce-products',
|
||||
'order' => 50,
|
||||
);
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
array(
|
||||
$home_item,
|
||||
@@ -253,7 +242,6 @@ class CoreMenu {
|
||||
'menuId' => 'secondary',
|
||||
'order' => 10,
|
||||
),
|
||||
$add_product_mvp,
|
||||
),
|
||||
// Tools category.
|
||||
self::get_tool_items(),
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce New Product Management Experience
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\TransientNotices;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Loader;
|
||||
use WP_Block_Editor_Context;
|
||||
|
||||
/**
|
||||
* Loads assets related to the new product management experience page.
|
||||
*/
|
||||
class NewProductManagementExperience {
|
||||
|
||||
/**
|
||||
* Option name used to toggle this feature.
|
||||
*/
|
||||
const TOGGLE_OPTION_NAME = 'woocommerce_new_product_management_enabled';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->maybe_show_disabled_notice();
|
||||
if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
|
||||
add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe show disabled notice.
|
||||
*/
|
||||
public function maybe_show_disabled_notice() {
|
||||
$new_product_experience_param = 'new-product-experience-disabled';
|
||||
if ( isset( $_GET[ $new_product_experience_param ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
TransientNotices::add(
|
||||
array(
|
||||
'user_id' => get_current_user_id(),
|
||||
'id' => 'new-product-experience-disbled',
|
||||
'status' => 'success',
|
||||
'content' => __( '🌟 Thanks for the feedback. We’ll put it to good use!', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
|
||||
$url = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
$url = remove_query_arg( 'new-product-experience-disabled', $url );
|
||||
wp_safe_redirect( $url );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles needed for the rich text editor.
|
||||
*/
|
||||
public function enqueue_styles() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style( 'wp-edit-blocks' );
|
||||
wp_enqueue_style( 'wp-format-library' );
|
||||
wp_enqueue_editor();
|
||||
/**
|
||||
* Enqueue any block editor related assets.
|
||||
*
|
||||
* @since 7.1.0
|
||||
*/
|
||||
do_action( 'enqueue_block_editor_assets' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the edit product links when the new experience is enabled.
|
||||
*
|
||||
* @param string $link The edit link.
|
||||
* @param int $post_id Post ID.
|
||||
* @return string
|
||||
*/
|
||||
public function update_edit_product_link( $link, $post_id ) {
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
if ( $product->get_type() === 'simple' ) {
|
||||
return admin_url( 'admin.php?page=wc-admin&path=/product/' . $product->get_id() );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
use Automattic\WooCommerce\LayoutTemplates\LayoutTemplateRegistry;
|
||||
|
||||
/**
|
||||
* Utils for block templates.
|
||||
*/
|
||||
class BlockTemplateUtils {
|
||||
/**
|
||||
* Directory which contains all templates
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TEMPLATES_ROOT_DIR = 'templates';
|
||||
|
||||
/**
|
||||
* Directory names.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const DIRECTORY_NAMES = array(
|
||||
'TEMPLATES' => 'product-form',
|
||||
'TEMPLATE_PARTS' => 'product-form/parts',
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the directory where templates of a specific template type can be found.
|
||||
*
|
||||
* @param string $template_type wp_template or wp_template_part.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_templates_directory( $template_type = 'wp_template' ) {
|
||||
$root_path = dirname( __DIR__, 4 ) . '/' . self::TEMPLATES_ROOT_DIR . DIRECTORY_SEPARATOR;
|
||||
$templates_directory = $root_path . self::DIRECTORY_NAMES['TEMPLATES'];
|
||||
$template_parts_directory = $root_path . self::DIRECTORY_NAMES['TEMPLATE_PARTS'];
|
||||
|
||||
if ( 'wp_template_part' === $template_type ) {
|
||||
return $template_parts_directory;
|
||||
}
|
||||
|
||||
return $templates_directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to a block template file.
|
||||
* Otherwise, False.
|
||||
*
|
||||
* @param string $slug - Template slug.
|
||||
* @return string|bool Path to the template file or false.
|
||||
*/
|
||||
public static function get_block_template_path( $slug ) {
|
||||
$directory = self::get_templates_directory();
|
||||
$path = trailingslashit( $directory ) . $slug . '.php';
|
||||
|
||||
if ( ! file_exists( $path ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template data from the headers.
|
||||
*
|
||||
* @param string $file_path - File path.
|
||||
* @return array Template data.
|
||||
*/
|
||||
public static function get_template_file_data( $file_path ) {
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$file_data = get_file_data(
|
||||
$file_path,
|
||||
array(
|
||||
'title' => 'Title',
|
||||
'slug' => 'Slug',
|
||||
'description' => 'Description',
|
||||
'product_types' => 'Product Types',
|
||||
),
|
||||
);
|
||||
|
||||
$file_data['product_types'] = explode( ',', trim( $file_data['product_types'] ) );
|
||||
|
||||
return $file_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template content from the file.
|
||||
*
|
||||
* @param string $file_path - File path.
|
||||
* @return string Content.
|
||||
*/
|
||||
public static function get_template_content( $file_path ) {
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
include $file_path;
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use Automattic\WooCommerce\LayoutTemplates\LayoutTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates\SimpleProductTemplate;
|
||||
use Automattic\WooCommerce\Internal\Features\ProductBlockEditor\ProductTemplates\ProductVariationTemplate;
|
||||
|
||||
use WC_Meta_Data;
|
||||
use WP_Block_Editor_Context;
|
||||
|
||||
/**
|
||||
@@ -56,11 +56,10 @@ class Init {
|
||||
$this->redirection_controller = new RedirectionController();
|
||||
|
||||
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
||||
if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 );
|
||||
add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
|
||||
}
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 );
|
||||
add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
|
||||
|
||||
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
add_filter( 'woocommerce_register_post_type_product_variation', array( $this, 'enable_rest_api_for_product_variation' ) );
|
||||
@@ -73,6 +72,9 @@ class Init {
|
||||
add_filter( 'register_block_type_args', array( $this, 'register_metadata_attribute' ) );
|
||||
add_filter( 'woocommerce_get_block_types', array( $this, 'get_block_types' ), 999, 1 );
|
||||
|
||||
add_filter( 'woocommerce_rest_prepare_product_object', array( $this, 'possibly_add_template_id' ), 10, 2 );
|
||||
add_filter( 'woocommerce_rest_prepare_product_variation_object', array( $this, 'possibly_add_template_id' ), 10, 2 );
|
||||
|
||||
// Make sure the block registry is initialized so that core blocks are registered.
|
||||
BlockRegistry::get_instance();
|
||||
|
||||
@@ -83,6 +85,37 @@ class Init {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the product template ID to the product if it doesn't exist.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param WC_Product $product The product.
|
||||
*/
|
||||
public function possibly_add_template_id( $response, $product ) {
|
||||
if ( ! $product ) {
|
||||
return $response;
|
||||
}
|
||||
if ( ! $product->meta_exists( '_product_template_id' ) ) {
|
||||
/**
|
||||
* Experimental: Allows to determine a product template id based on the product data.
|
||||
*
|
||||
* @ignore
|
||||
* @since 9.1.0
|
||||
*/
|
||||
$product_template_id = apply_filters( 'experimental_woocommerce_product_editor_product_template_id_for_product', '', $product );
|
||||
if ( $product_template_id ) {
|
||||
$response->data['meta_data'][] = new WC_Meta_Data(
|
||||
array(
|
||||
'key' => '_product_template_id',
|
||||
'value' => $product_template_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts needed for the product form block editor.
|
||||
*/
|
||||
@@ -377,6 +410,12 @@ class Init {
|
||||
);
|
||||
|
||||
$this->redirection_controller->set_product_templates( $this->product_templates );
|
||||
|
||||
// PFT: Initialize the product form controller.
|
||||
if ( Features::is_enabled( 'product-editor-template-system' ) ) {
|
||||
$product_form_controller = new ProductFormsController();
|
||||
$product_form_controller->init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Product Forms Controller
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
/**
|
||||
* Handle retrieval of product forms.
|
||||
*/
|
||||
class ProductFormsController {
|
||||
|
||||
/**
|
||||
* Product form templates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $product_form_templates = array(
|
||||
'simple',
|
||||
);
|
||||
|
||||
/**
|
||||
* Set up the product forms controller.
|
||||
*/
|
||||
public function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingFinal, WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag -- Not an injection.
|
||||
add_action( 'upgrader_process_complete', array( $this, 'migrate_templates_when_plugin_updated' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate form templates after WooCommerce plugin update.
|
||||
*
|
||||
* @param \WP_Upgrader $upgrader The WP_Upgrader instance.
|
||||
* @param array $hook_extra Extra arguments passed to hooked filters.
|
||||
* @return void
|
||||
*/
|
||||
public function migrate_templates_when_plugin_updated( \WP_Upgrader $upgrader, array $hook_extra ) {
|
||||
// If it is not a plugin hook type, bail early.
|
||||
$type = isset( $hook_extra['type'] ) ? $hook_extra['type'] : '';
|
||||
if ( 'plugin' !== $type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it is not the WooCommerce plugin, bail early.
|
||||
$plugins = isset( $hook_extra['plugins'] ) ? $hook_extra['plugins'] : array();
|
||||
if (
|
||||
! in_array( 'woocommerce/woocommerce.php', $plugins, true )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the action is not install or update, bail early.
|
||||
$action = isset( $hook_extra['action'] ) ? $hook_extra['action'] : '';
|
||||
if ( 'install' !== $action && 'update' !== $action ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger the migration process.
|
||||
$this->migrate_product_form_posts( $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ot update a product_form post for each product form template.
|
||||
* If the post already exists, it will be updated.
|
||||
* If the post does not exist, it will be created even if the action is `update`.
|
||||
*
|
||||
* @param string $action - The action to perform. `insert` | `update`.
|
||||
* @return void
|
||||
*/
|
||||
public function migrate_product_form_posts( $action ) {
|
||||
/**
|
||||
* Allow extend the list of templates that should be auto-generated.
|
||||
*
|
||||
* @since 9.1.0
|
||||
* @param array $templates List of templates to auto-generate.
|
||||
*/
|
||||
$templates = apply_filters(
|
||||
'woocommerce_product_form_templates',
|
||||
$this->product_form_templates
|
||||
);
|
||||
|
||||
foreach ( $templates as $slug ) {
|
||||
$file_path = BlockTemplateUtils::get_block_template_path( $slug );
|
||||
|
||||
if ( ! $file_path ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_data = BlockTemplateUtils::get_template_file_data( $file_path );
|
||||
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'name' => $slug,
|
||||
'post_type' => 'product_form',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Update the the CPT post if it already exists,
|
||||
* and the action is `update`.
|
||||
*/
|
||||
if ( 'update' === $action ) {
|
||||
$post = $posts[0] ?? null;
|
||||
|
||||
if ( ! empty( $post ) ) {
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post->ID,
|
||||
'post_title' => $file_data['title'],
|
||||
'post_content' => BlockTemplateUtils::get_template_content( $file_path ),
|
||||
'post_excerpt' => $file_data['description'],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip the post creation if the post already exists.
|
||||
*/
|
||||
if ( ! empty( $posts ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$post = wp_insert_post(
|
||||
array(
|
||||
'post_title' => $file_data['title'],
|
||||
'post_name' => $slug,
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'product_form',
|
||||
'post_content' => BlockTemplateUtils::get_template_content( $file_path ),
|
||||
'post_excerpt' => $file_data['description'],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,28 +59,28 @@ class DefaultShippingPartners {
|
||||
array(
|
||||
'icon' => $check_icon,
|
||||
'description' => __(
|
||||
'Print labels from Royal Mail, Parcel Force, DPD, and many more',
|
||||
'Discounted labels from top global carriers',
|
||||
'woocommerce'
|
||||
),
|
||||
),
|
||||
array(
|
||||
'icon' => $check_icon,
|
||||
'description' => __(
|
||||
'Shop for the best rates, in real-time',
|
||||
'Sync all your selling channels in one place',
|
||||
'woocommerce'
|
||||
),
|
||||
),
|
||||
array(
|
||||
'icon' => $check_icon,
|
||||
'description' => __( 'Connect selling channels easily', 'woocommerce' ),
|
||||
'description' => __( 'Advanced automated workflows and customs', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'icon' => $check_icon,
|
||||
'description' => __( 'Advance automated workflows', 'woocommerce' ),
|
||||
'description' => __( 'Instantly send tracking to your customers', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'icon' => $check_icon,
|
||||
'description' => __( '30-days free trial', 'woocommerce' ),
|
||||
'description' => __( '30-day free trial', 'woocommerce' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -821,6 +821,35 @@ class PluginsHelper {
|
||||
|
||||
$total_expiring_subscriptions = count( $expiring_subscriptions );
|
||||
|
||||
// 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. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
|
||||
'Your WooCommerce extension subscriptions are missing a payment method for renewal. <a href="%s">Add a payment method</a> 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/',
|
||||
);
|
||||
}
|
||||
|
||||
// Payment method is available but there are expiring subscriptions.
|
||||
$notice_data = self::get_subscriptions_notice_data(
|
||||
$subscriptions,
|
||||
$expiring_subscriptions,
|
||||
|
||||
@@ -133,7 +133,7 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
'coreprofiler_store_extension_installed_and_activated',
|
||||
array(
|
||||
'success' => false,
|
||||
'extension' => $plugin_name,
|
||||
'extension' => $this->get_plugin_track_key( $plugin_name ),
|
||||
'error_message' => $error_message,
|
||||
)
|
||||
);
|
||||
@@ -173,37 +173,37 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
* @return string - Time frame.
|
||||
*/
|
||||
function get_timeframe( $timeInMs ) {
|
||||
$time_frames = [
|
||||
[
|
||||
$time_frames = array(
|
||||
array(
|
||||
'name' => '0-2s',
|
||||
'max' => 2,
|
||||
],
|
||||
[
|
||||
),
|
||||
array(
|
||||
'name' => '2-5s',
|
||||
'max' => 5,
|
||||
],
|
||||
[
|
||||
),
|
||||
array(
|
||||
'name' => '5-10s',
|
||||
'max' => 10,
|
||||
],
|
||||
[
|
||||
),
|
||||
array(
|
||||
'name' => '10-15s',
|
||||
'max' => 15,
|
||||
],
|
||||
[
|
||||
),
|
||||
array(
|
||||
'name' => '15-20s',
|
||||
'max' => 20,
|
||||
],
|
||||
[
|
||||
),
|
||||
array(
|
||||
'name' => '20-30s',
|
||||
'max' => 30,
|
||||
],
|
||||
[
|
||||
),
|
||||
array(
|
||||
'name' => '30-60s',
|
||||
'max' => 60,
|
||||
],
|
||||
[ 'name' => '>60s' ],
|
||||
];
|
||||
),
|
||||
array( 'name' => '>60s' ),
|
||||
);
|
||||
|
||||
foreach ( $time_frames as $time_frame ) {
|
||||
if ( ! isset( $time_frame['max'] ) ) {
|
||||
@@ -219,7 +219,7 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
$track_data = array(
|
||||
'success' => true,
|
||||
'installed_extensions' => array_map(
|
||||
function( $extension ) {
|
||||
function ( $extension ) {
|
||||
return $this->get_plugin_track_key( $extension );
|
||||
},
|
||||
$data['installed']
|
||||
|
||||
@@ -168,6 +168,18 @@ class WCAdminHelper {
|
||||
*/
|
||||
$store_pages = apply_filters( 'woocommerce_store_pages', $store_pages );
|
||||
|
||||
// If the shop page is not set, we will still show the product archive page.
|
||||
// Therefore, we need to check if the URL is a product archive page when the shop page is not set.
|
||||
if ( $store_pages['shop'] <= 0 ) {
|
||||
$product_post_archive_link = get_post_type_archive_link( 'product' );
|
||||
|
||||
if ( is_string( $product_post_archive_link ) &&
|
||||
0 === strpos( $normalized_path, self::get_normalized_url_path( $product_post_archive_link ) )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $store_pages as $page => $page_id ) {
|
||||
if ( 0 >= $page_id ) {
|
||||
continue;
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
namespace Automattic\WooCommerce\Blocks;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternRegistry;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Registers patterns under the `./patterns/` directory and updates their content.
|
||||
* Each pattern is defined as a PHP file and defines its metadata using plugin-style headers.
|
||||
* Registers patterns under the `./patterns/` directory and from the PTK API and updates their content.
|
||||
* Each pattern from core is defined as a PHP file and defines its metadata using plugin-style headers.
|
||||
* The minimum required definition is:
|
||||
*
|
||||
* /**
|
||||
@@ -35,82 +34,69 @@ use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
|
||||
* @internal
|
||||
*/
|
||||
class BlockPatterns {
|
||||
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
||||
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
|
||||
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';
|
||||
|
||||
/**
|
||||
* Path to the patterns directory.
|
||||
* Path to the patterns' directory.
|
||||
*
|
||||
* @var string $patterns_path
|
||||
*/
|
||||
private $patterns_path;
|
||||
private string $patterns_path;
|
||||
|
||||
/**
|
||||
* PatternRegistry instance.
|
||||
*
|
||||
* @var PatternRegistry $pattern_registry
|
||||
*/
|
||||
private PatternRegistry $pattern_registry;
|
||||
|
||||
/**
|
||||
* Patterns dictionary
|
||||
*
|
||||
* @var array|WP_Error
|
||||
*/
|
||||
private $dictionary;
|
||||
|
||||
/**
|
||||
* PTKPatternsStore instance.
|
||||
*
|
||||
* @var PTKPatternsStore $ptk_patterns_store
|
||||
*/
|
||||
private PTKPatternsStore $ptk_patterns_store;
|
||||
|
||||
/**
|
||||
* Constructor for class
|
||||
*
|
||||
* @param Package $package An instance of Package.
|
||||
* @param Package $package An instance of Package.
|
||||
* @param PatternRegistry $pattern_registry An instance of PatternRegistry.
|
||||
* @param PTKPatternsStore $ptk_patterns_store An instance of PTKPatternsStore.
|
||||
*/
|
||||
public function __construct( Package $package ) {
|
||||
$this->patterns_path = $package->get_path( 'patterns' );
|
||||
public function __construct(
|
||||
Package $package,
|
||||
PatternRegistry $pattern_registry,
|
||||
PTKPatternsStore $ptk_patterns_store
|
||||
) {
|
||||
$this->patterns_path = $package->get_path( 'patterns' );
|
||||
$this->pattern_registry = $pattern_registry;
|
||||
$this->ptk_patterns_store = $ptk_patterns_store;
|
||||
|
||||
$this->dictionary = PatternsHelper::get_patterns_dictionary();
|
||||
|
||||
add_action( 'init', array( $this, 'register_block_patterns' ) );
|
||||
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'schedule_on_option_update' ), 10, 2 );
|
||||
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'update_ai_connection_allowed_option' ), 10, 2 );
|
||||
add_action( 'upgrader_process_complete', array( $this, 'schedule_on_plugin_update' ), 10, 2 );
|
||||
add_action( 'woocommerce_update_patterns_content', array( $this, 'update_patterns_content' ) );
|
||||
|
||||
if ( Features::is_enabled( 'pattern-toolkit-full-composability' ) ) {
|
||||
add_action( 'init', array( $this, 'register_ptk_patterns' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the 'woocommerce_blocks_allow_ai_connection' option is set to true if the site is connected to AI.
|
||||
* Register block patterns from core.
|
||||
*
|
||||
* @param string $option The option name.
|
||||
* @param string $value The option value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update_ai_connection_allowed_option( $option, $value ): bool {
|
||||
$ai_connection = new Connection();
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return update_option( 'woocommerce_blocks_allow_ai_connection', false, true );
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return update_option( 'woocommerce_blocks_allow_ai_connection', false, true );
|
||||
}
|
||||
|
||||
return update_option( 'woocommerce_blocks_allow_ai_connection', true, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the block patterns and categories under `./patterns/`.
|
||||
* @return void
|
||||
*/
|
||||
public function register_block_patterns() {
|
||||
if ( ! class_exists( 'WP_Block_Patterns_Registry' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_post_type(
|
||||
self::PATTERNS_AI_DATA_POST_TYPE,
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => __( 'Patterns AI Data', 'woocommerce' ),
|
||||
'singular_name' => __( 'Patterns AI Data', 'woocommerce' ),
|
||||
),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'delete_with_user' => false,
|
||||
'can_export' => true,
|
||||
)
|
||||
);
|
||||
|
||||
$default_headers = array(
|
||||
'title' => 'Title',
|
||||
'slug' => 'Slug',
|
||||
@@ -132,279 +118,38 @@ class BlockPatterns {
|
||||
return;
|
||||
}
|
||||
|
||||
$dictionary = PatternsHelper::get_patterns_dictionary();
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
$pattern_data = get_file_data( $file, $default_headers );
|
||||
|
||||
if ( empty( $pattern_data['slug'] ) ) {
|
||||
_doing_it_wrong(
|
||||
'register_block_patterns',
|
||||
esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: file name. */
|
||||
__( 'Could not register file "%s" as a block pattern ("Slug" field missing)', 'woocommerce' ),
|
||||
$file
|
||||
)
|
||||
),
|
||||
'6.0.0'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! preg_match( self::SLUG_REGEX, $pattern_data['slug'] ) ) {
|
||||
_doing_it_wrong(
|
||||
'register_block_patterns',
|
||||
esc_html(
|
||||
sprintf(
|
||||
/* translators: %1s: file name; %2s: slug value found. */
|
||||
__( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")', 'woocommerce' ),
|
||||
$file,
|
||||
$pattern_data['slug']
|
||||
)
|
||||
),
|
||||
'6.0.0'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( \WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $pattern_data['featureFlag'] && ! Features::is_enabled( $pattern_data['featureFlag'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Title is a required property.
|
||||
if ( ! $pattern_data['title'] ) {
|
||||
_doing_it_wrong(
|
||||
'register_block_patterns',
|
||||
esc_html(
|
||||
sprintf(
|
||||
/* translators: %1s: file name; %2s: slug value found. */
|
||||
__( 'Could not register file "%s" as a block pattern ("Title" field missing)', 'woocommerce' ),
|
||||
$file
|
||||
)
|
||||
),
|
||||
'6.0.0'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For properties of type array, parse data as comma-separated.
|
||||
foreach ( array( 'categories', 'keywords', 'blockTypes' ) as $property ) {
|
||||
if ( ! empty( $pattern_data[ $property ] ) ) {
|
||||
$pattern_data[ $property ] = array_filter(
|
||||
preg_split(
|
||||
self::COMMA_SEPARATED_REGEX,
|
||||
(string) $pattern_data[ $property ]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
unset( $pattern_data[ $property ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Parse properties of type int.
|
||||
foreach ( array( 'viewportWidth' ) as $property ) {
|
||||
if ( ! empty( $pattern_data[ $property ] ) ) {
|
||||
$pattern_data[ $property ] = (int) $pattern_data[ $property ];
|
||||
} else {
|
||||
unset( $pattern_data[ $property ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Parse properties of type bool.
|
||||
foreach ( array( 'inserter' ) as $property ) {
|
||||
if ( ! empty( $pattern_data[ $property ] ) ) {
|
||||
$pattern_data[ $property ] = in_array(
|
||||
strtolower( $pattern_data[ $property ] ),
|
||||
array( 'yes', 'true' ),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
unset( $pattern_data[ $property ] );
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
|
||||
$pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', 'woocommerce' );
|
||||
if ( ! empty( $pattern_data['description'] ) ) {
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
|
||||
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woocommerce' );
|
||||
}
|
||||
|
||||
$pattern_data_from_dictionary = $this->get_pattern_from_dictionary( $dictionary, $pattern_data['slug'] );
|
||||
|
||||
// The actual pattern content is the output of the file.
|
||||
ob_start();
|
||||
|
||||
/*
|
||||
For patterns that can have AI-generated content, we need to get its content from the dictionary and pass
|
||||
it to the pattern file through the "$content" and "$images" variables.
|
||||
This is to avoid having to access the dictionary for each pattern when it's registered or inserted.
|
||||
Before the "$content" and "$images" variables were populated in each pattern. Since the pattern
|
||||
registration happens in the init hook, the dictionary was being access one for each pattern and
|
||||
for each page load. This way we only do it once on registration.
|
||||
For more context: https://github.com/woocommerce/woocommerce-blocks/pull/11733
|
||||
*/
|
||||
|
||||
$content = array();
|
||||
$images = array();
|
||||
if ( ! is_null( $pattern_data_from_dictionary ) ) {
|
||||
$content = $pattern_data_from_dictionary['content'];
|
||||
$images = $pattern_data_from_dictionary['images'] ?? array();
|
||||
}
|
||||
include $file;
|
||||
$pattern_data['content'] = ob_get_clean();
|
||||
|
||||
if ( ! $pattern_data['content'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $pattern_data['categories'] as $key => $category ) {
|
||||
$category_slug = _wp_to_kebab_case( $category );
|
||||
|
||||
$pattern_data['categories'][ $key ] = $category_slug;
|
||||
|
||||
register_block_pattern_category(
|
||||
$category_slug,
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
|
||||
array( 'label' => __( $category, 'woocommerce' ) )
|
||||
);
|
||||
}
|
||||
|
||||
register_block_pattern( $pattern_data['slug'], $pattern_data );
|
||||
$this->pattern_registry->register_block_pattern( $file, $pattern_data, $this->dictionary );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content when the store description is changed.
|
||||
* Register patterns from the Patterns Toolkit.
|
||||
*
|
||||
* @param string $option The option name.
|
||||
* @param string $value The option value.
|
||||
* @return void
|
||||
*/
|
||||
public function schedule_on_option_update( $option, $value ) {
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $value ) {
|
||||
public function register_ptk_patterns() {
|
||||
// Only if the user has allowed tracking, we register the patterns from the PTK.
|
||||
$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' );
|
||||
if ( ! $allow_tracking ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->schedule_patterns_content_update( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content when the WooCommerce Blocks plugin is updated.
|
||||
*
|
||||
* @param \WP_Upgrader $upgrader_object WP_Upgrader instance.
|
||||
* @param array $options Array of bulk item update data.
|
||||
*/
|
||||
public function schedule_on_plugin_update( $upgrader_object, $options ) {
|
||||
if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
|
||||
foreach ( $options['plugins'] as $plugin ) {
|
||||
if ( str_contains( $plugin, 'woocommerce.php' ) ) {
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
|
||||
if ( $business_description ) {
|
||||
$this->schedule_patterns_content_update( $business_description );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content when the store description is changed.
|
||||
*
|
||||
* @param string $business_description The business description.
|
||||
*/
|
||||
public function schedule_patterns_content_update( $business_description ) {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action_scheduler = WP_PLUGIN_DIR . '/woocommerce/packages/action-scheduler/action-scheduler.php';
|
||||
|
||||
if ( ! file_exists( $action_scheduler ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once $action_scheduler;
|
||||
|
||||
as_schedule_single_action( time(), 'woocommerce_update_patterns_content', array( $business_description ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content.
|
||||
*
|
||||
* @param string $value The new value saved for the add_option_woo_ai_describe_store_description option.
|
||||
*
|
||||
* @return bool|string|\WP_Error
|
||||
*/
|
||||
public function update_patterns_content( $value ) {
|
||||
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
|
||||
|
||||
if ( ! $allow_ai_connection ) {
|
||||
return new \WP_Error(
|
||||
'ai_connection_not_allowed',
|
||||
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
|
||||
$patterns = $this->ptk_patterns_store->get_patterns();
|
||||
if ( empty( $patterns ) ) {
|
||||
wc_get_logger()->warning(
|
||||
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$ai_connection = new Connection();
|
||||
foreach ( $patterns as $pattern ) {
|
||||
$pattern['slug'] = $pattern['name'];
|
||||
$pattern['content'] = $pattern['html'];
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id->get_error_message();
|
||||
$this->pattern_registry->register_block_pattern( $pattern['ID'], $pattern, $this->dictionary );
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token->get_error_message();
|
||||
}
|
||||
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
|
||||
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
|
||||
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images->get_error_message();
|
||||
}
|
||||
|
||||
$populate_patterns = ( new UpdatePatterns() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_patterns ) ) {
|
||||
return $populate_patterns->get_error_message();
|
||||
}
|
||||
|
||||
$populate_products = ( new UpdateProducts() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_products ) ) {
|
||||
return $populate_products->get_error_message();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the patterns dictionary to get the pattern data corresponding to the pattern slug.
|
||||
*
|
||||
* @param array $dictionary The patterns dictionary.
|
||||
* @param string $slug The pattern slug.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_pattern_from_dictionary( $dictionary, $slug ) {
|
||||
foreach ( $dictionary as $pattern_dictionary ) {
|
||||
if ( isset( $pattern_dictionary['slug'] ) && $pattern_dictionary['slug'] === $slug ) {
|
||||
return $pattern_dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
|
||||
use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate;
|
||||
|
||||
/**
|
||||
* BlockTypesController class.
|
||||
* BlockTemplatesController class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -29,7 +29,6 @@ class BlockTemplatesController {
|
||||
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
|
||||
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
|
||||
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
|
||||
add_filter( 'post_type_archive_title', array( $this, 'update_product_archive_title' ), 10, 2 );
|
||||
|
||||
if ( wc_current_theme_is_fse_theme() ) {
|
||||
// By default, the Template Part Block only supports template parts that are in the current theme directory.
|
||||
@@ -531,28 +530,4 @@ class BlockTemplatesController {
|
||||
$directory
|
||||
) || $this->get_block_templates( array( $template_name ), $template_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the product archive title to "Shop".
|
||||
*
|
||||
* Attention: this method is run in classic themes as well, so it
|
||||
* can't be moved to the ProductCatalogTemplate class. See:
|
||||
* https://github.com/woocommerce/woocommerce/pull/46429
|
||||
*
|
||||
* @param string $post_type_name Post type 'name' label.
|
||||
* @param string $post_type Post type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function update_product_archive_title( $post_type_name, $post_type ) {
|
||||
if (
|
||||
function_exists( 'is_shop' ) &&
|
||||
is_shop() &&
|
||||
'product' === $post_type
|
||||
) {
|
||||
return __( 'Shop', 'woocommerce' );
|
||||
}
|
||||
|
||||
return $post_type_name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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\ProductFiltersOverlayTemplate;
|
||||
|
||||
/**
|
||||
* BlockTemplatesRegistry class.
|
||||
@@ -58,8 +59,9 @@ class BlockTemplatesRegistry {
|
||||
}
|
||||
if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
|
||||
$template_parts = array(
|
||||
MiniCartTemplate::SLUG => new MiniCartTemplate(),
|
||||
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
|
||||
MiniCartTemplate::SLUG => new MiniCartTemplate(),
|
||||
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
|
||||
ProductFiltersOverlayTemplate::SLUG => new ProductFiltersOverlayTemplate(),
|
||||
);
|
||||
} else {
|
||||
$template_parts = array();
|
||||
|
||||
@@ -6,6 +6,7 @@ use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
|
||||
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* AbstractBlock class.
|
||||
@@ -239,7 +240,7 @@ abstract class AbstractBlock {
|
||||
$block_settings['style'] = null;
|
||||
add_filter(
|
||||
'render_block',
|
||||
function( $html, $block ) use ( $style_handles ) {
|
||||
function ( $html, $block ) use ( $style_handles ) {
|
||||
if ( $block['blockName'] === $this->get_block_type() ) {
|
||||
array_map( 'wp_enqueue_style', $style_handles );
|
||||
}
|
||||
@@ -434,24 +435,35 @@ abstract class AbstractBlock {
|
||||
}
|
||||
|
||||
if ( ! $this->asset_data_registry->exists( 'wcBlocksConfig' ) ) {
|
||||
$wc_blocks_config = [
|
||||
'pluginUrl' => plugins_url( '/', dirname( __DIR__, 2 ) ),
|
||||
'restApiRoutes' => [
|
||||
'/wc/store/v1' => array_keys( $this->get_routes_from_namespace( 'wc/store/v1' ) ),
|
||||
],
|
||||
'defaultAvatar' => get_avatar_url( 0, [ 'force_default' => true ] ),
|
||||
|
||||
/*
|
||||
* translators: If your word count is based on single characters (e.g. East Asian characters),
|
||||
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
|
||||
* Do not translate into your own language.
|
||||
*/
|
||||
'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ),
|
||||
];
|
||||
if ( is_admin() && ! WC()->is_rest_api_request() ) {
|
||||
$wc_blocks_config = array_merge(
|
||||
$wc_blocks_config,
|
||||
[
|
||||
// Note that while we don't have a consolidated way of doing feature-flagging
|
||||
// we are borrowing from the WC Admin Features implementation. Also note we cannot
|
||||
// use the wcAdminFeatures global because it's not always enqueued in the context of blocks.
|
||||
'experimentalBlocksEnabled' => Features::is_enabled( 'experimental-blocks' ),
|
||||
'productCount' => array_sum( (array) wp_count_posts( 'product' ) ),
|
||||
]
|
||||
);
|
||||
}
|
||||
$this->asset_data_registry->add(
|
||||
'wcBlocksConfig',
|
||||
[
|
||||
'buildPhase' => Package::feature()->get_flag(),
|
||||
'pluginUrl' => plugins_url( '/', dirname( __DIR__, 2 ) ),
|
||||
'productCount' => array_sum( (array) wp_count_posts( 'product' ) ),
|
||||
'restApiRoutes' => [
|
||||
'/wc/store/v1' => array_keys( $this->get_routes_from_namespace( 'wc/store/v1' ) ),
|
||||
],
|
||||
'defaultAvatar' => get_avatar_url( 0, [ 'force_default' => true ] ),
|
||||
|
||||
/*
|
||||
* translators: If your word count is based on single characters (e.g. East Asian characters),
|
||||
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
|
||||
* Do not translate into your own language.
|
||||
*/
|
||||
'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ),
|
||||
]
|
||||
$wc_blocks_config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ class Totals extends AbstractOrderConfirmationBlock {
|
||||
'<p class="wc-block-order-confirmation-order-note__label">' .
|
||||
esc_html__( 'Note:', 'woocommerce' ) .
|
||||
'</p>' .
|
||||
'<p>' . wp_kses_post( nl2br( wptexturize( $order->get_customer_note() ) ) ) . '</p>' .
|
||||
'<p>' . wp_kses( nl2br( wptexturize( $order->get_customer_note() ) ), [] ) . '</p>' .
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,12 +276,14 @@ class ProductCollection extends AbstractBlock {
|
||||
static $dirty_enhanced_queries = array();
|
||||
static $render_product_collection_callback = null;
|
||||
|
||||
$block_name = $parsed_block['blockName'];
|
||||
$force_page_reload_global =
|
||||
$block_name = $parsed_block['blockName'];
|
||||
$is_product_collection_block = $parsed_block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||
$force_page_reload_global =
|
||||
$parsed_block['attrs']['forcePageReload'] ?? false &&
|
||||
isset( $block['attrs']['queryId'] );
|
||||
|
||||
if (
|
||||
$is_product_collection_block &&
|
||||
'woocommerce/product-collection' === $block_name &&
|
||||
! $force_page_reload_global
|
||||
) {
|
||||
|
||||
@@ -13,7 +13,7 @@ class ProductFilters extends AbstractBlock {
|
||||
protected $block_name = 'product-filters';
|
||||
|
||||
/**
|
||||
* Register the context
|
||||
* Register the context.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* ProductFiltersOverlay class.
|
||||
*/
|
||||
class ProductFiltersOverlay extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'product-filters-overlay';
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function get_block_type_style() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include and render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes. Default empty array.
|
||||
* @param string $content Block content. Default empty string.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
ob_start();
|
||||
printf( '<div>%s</div>', esc_html__( 'Filters Overlay', 'woocommerce' ) );
|
||||
$html = ob_get_clean();
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
|
||||
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
|
||||
@@ -48,6 +48,7 @@ final class BlockTypesController {
|
||||
*/
|
||||
protected function init() {
|
||||
add_action( 'init', array( $this, 'register_blocks' ) );
|
||||
add_filter( 'block_categories_all', array( $this, 'register_block_categories' ), 10, 2 );
|
||||
add_filter( 'render_block', array( $this, 'add_data_attributes' ), 10, 2 );
|
||||
add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
|
||||
add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
|
||||
@@ -107,6 +108,29 @@ final class BlockTypesController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register block categories
|
||||
*
|
||||
* Used in combination with the `block_categories_all` filter, to append
|
||||
* WooCommerce Blocks related categories to the Gutenberg editor.
|
||||
*
|
||||
* @param array $categories The array of already registered categories.
|
||||
*/
|
||||
public function register_block_categories( $categories ) {
|
||||
$woocommerce_block_categories = array(
|
||||
array(
|
||||
'slug' => 'woocommerce',
|
||||
'title' => __( 'WooCommerce', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
'slug' => 'woocommerce-product-elements',
|
||||
'title' => __( 'WooCommerce Product Elements', 'woocommerce' ),
|
||||
),
|
||||
);
|
||||
|
||||
return array_merge( $categories, $woocommerce_block_categories );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace.
|
||||
*
|
||||
@@ -297,9 +321,12 @@ final class BlockTypesController {
|
||||
MiniCartContents::get_mini_cart_block_types()
|
||||
);
|
||||
|
||||
if ( Package::feature()->is_experimental_build() ) {
|
||||
// Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
|
||||
// when modifying this list.
|
||||
if ( Features::is_enabled( 'experimental-blocks' ) ) {
|
||||
$block_types[] = 'ProductFilter';
|
||||
$block_types[] = 'ProductFilters';
|
||||
$block_types[] = 'ProductFiltersOverlay';
|
||||
$block_types[] = 'ProductFilterStockStatus';
|
||||
$block_types[] = 'ProductFilterPrice';
|
||||
$block_types[] = 'ProductFilterAttribute';
|
||||
@@ -318,6 +345,7 @@ final class BlockTypesController {
|
||||
'AllProducts',
|
||||
'Cart',
|
||||
'Checkout',
|
||||
'ProductGallery',
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -348,6 +376,7 @@ final class BlockTypesController {
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
'OrderConfirmation\AdditionalFieldsWrapper',
|
||||
'OrderConfirmation\AdditionalFields',
|
||||
'ProductGallery',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ use Automattic\WooCommerce\Blocks\BlockPatterns;
|
||||
use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry;
|
||||
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Blocks\BlockTypesController;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\AIPatterns;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternRegistry;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PTKClient;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
|
||||
@@ -109,7 +113,7 @@ class Bootstrap {
|
||||
|
||||
add_action(
|
||||
'admin_init',
|
||||
function() {
|
||||
function () {
|
||||
// Delete this notification because the blocks are included in WC Core now. This will handle any sites
|
||||
// with lingering notices.
|
||||
InboxNotifications::delete_surface_cart_checkout_blocks_notification();
|
||||
@@ -145,6 +149,7 @@ class Bootstrap {
|
||||
if ( ! $is_store_api_request ) {
|
||||
// Template related functionality. These won't be loaded for store API requests, but may be loaded for
|
||||
// regular rest requests to maintain compatibility with the store editor.
|
||||
$this->container->get( AIPatterns::class );
|
||||
$this->container->get( BlockPatterns::class );
|
||||
$this->container->get( BlockTypesController::class );
|
||||
$this->container->get( BlockTemplatesRegistry::class )->init();
|
||||
@@ -153,6 +158,7 @@ class Bootstrap {
|
||||
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
|
||||
$this->container->get( SingleProductTemplateCompatibility::class )->init();
|
||||
$this->container->get( Notices::class )->init();
|
||||
$this->container->get( PTKPatternsStore::class );
|
||||
}
|
||||
|
||||
$this->container->get( QueryFilters::class )->init();
|
||||
@@ -178,7 +184,7 @@ class Bootstrap {
|
||||
}
|
||||
add_action(
|
||||
'admin_notices',
|
||||
function() {
|
||||
function () {
|
||||
echo '<div class="error"><p>';
|
||||
printf(
|
||||
/* translators: %1$s is the node install command, %2$s is the install command, %3$s is the build command, %4$s is the watch command. */
|
||||
@@ -218,19 +224,19 @@ class Bootstrap {
|
||||
);
|
||||
$this->container->register(
|
||||
AssetDataRegistry::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new AssetDataRegistry( $container->get( AssetApi::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
AssetsController::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new AssetsController( $container->get( AssetApi::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PaymentMethodRegistry::class,
|
||||
function() {
|
||||
function () {
|
||||
return new PaymentMethodRegistry();
|
||||
}
|
||||
);
|
||||
@@ -250,13 +256,13 @@ class Bootstrap {
|
||||
);
|
||||
$this->container->register(
|
||||
BlockTemplatesRegistry::class,
|
||||
function ( Container $container ) {
|
||||
function () {
|
||||
return new BlockTemplatesRegistry();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
BlockTemplatesController::class,
|
||||
function ( Container $container ) {
|
||||
function () {
|
||||
return new BlockTemplatesController();
|
||||
}
|
||||
);
|
||||
@@ -281,51 +287,51 @@ class Bootstrap {
|
||||
);
|
||||
$this->container->register(
|
||||
DraftOrders::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new DraftOrders( $container->get( Package::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CreateAccount::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new CreateAccount( $container->get( Package::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
GoogleAnalytics::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
return new GoogleAnalytics( $asset_api );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
Notices::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new Notices( $container->get( Package::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
Hydration::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new Hydration( $container->get( AssetDataRegistry::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFields::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
return new CheckoutFields( $container->get( AssetDataRegistry::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFieldsAdmin::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$checkout_fields_controller = $container->get( CheckoutFields::class );
|
||||
return new CheckoutFieldsAdmin( $checkout_fields_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFieldsFrontend::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$checkout_fields_controller = $container->get( CheckoutFields::class );
|
||||
return new CheckoutFieldsFrontend( $checkout_fields_controller );
|
||||
}
|
||||
@@ -347,36 +353,58 @@ class Bootstrap {
|
||||
// Maintains backwards compatibility with previous Store API namespace.
|
||||
$this->container->register(
|
||||
'Automattic\WooCommerce\Blocks\StoreApi\Formatters',
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\StoreApi\Formatters', '6.4.0', 'Automattic\WooCommerce\StoreApi\Formatters', '6.5.0' );
|
||||
return $container->get( StoreApi::class )->container()->get( \Automattic\WooCommerce\StoreApi\Formatters::class );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
'Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi',
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi', '6.4.0', 'Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema', '6.5.0' );
|
||||
return $container->get( StoreApi::class )->container()->get( \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::class );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
'Automattic\WooCommerce\Blocks\StoreApi\SchemaController',
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\StoreApi\SchemaController', '6.4.0', 'Automattic\WooCommerce\StoreApi\SchemaController', '6.5.0' );
|
||||
return $container->get( StoreApi::class )->container()->get( SchemaController::class );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
'Automattic\WooCommerce\Blocks\StoreApi\RoutesController',
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\StoreApi\RoutesController', '6.4.0', 'Automattic\WooCommerce\StoreApi\RoutesController', '6.5.0' );
|
||||
return $container->get( StoreApi::class )->container()->get( RoutesController::class );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PTKClient::class,
|
||||
function () {
|
||||
return new PTKClient();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PTKPatternsStore::class,
|
||||
function () {
|
||||
return new PTKPatternsStore( $this->container->get( PTKClient::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
BlockPatterns::class,
|
||||
function () {
|
||||
return new BlockPatterns( $this->package );
|
||||
return new BlockPatterns(
|
||||
$this->package,
|
||||
new PatternRegistry(),
|
||||
$this->container->get( PTKPatternsStore::class )
|
||||
);
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
AIPatterns::class,
|
||||
function () {
|
||||
return new AIPatterns();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
@@ -389,13 +417,13 @@ class Bootstrap {
|
||||
);
|
||||
$this->container->register(
|
||||
TasksController::class,
|
||||
function() {
|
||||
function () {
|
||||
return new TasksController();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
QueryFilters::class,
|
||||
function() {
|
||||
function () {
|
||||
return new QueryFilters();
|
||||
}
|
||||
);
|
||||
@@ -470,28 +498,28 @@ class Bootstrap {
|
||||
protected function register_payment_methods() {
|
||||
$this->container->register(
|
||||
Cheque::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
return new Cheque( $asset_api );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PayPal::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
return new PayPal( $asset_api );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
BankTransfer::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
return new BankTransfer( $asset_api );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CashOnDelivery::class,
|
||||
function( Container $container ) {
|
||||
function ( Container $container ) {
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
return new CashOnDelivery( $asset_api );
|
||||
}
|
||||
|
||||
@@ -123,22 +123,4 @@ class Package {
|
||||
public function feature() {
|
||||
return $this->feature_gating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're executing the code in an experimental build mode.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_experimental_build() {
|
||||
return $this->feature()->is_experimental_build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're executing the code in an feature plugin or experimental build mode.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_feature_plugin_build() {
|
||||
return $this->feature()->is_feature_plugin_build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1187,7 +1187,7 @@ class CheckoutFields {
|
||||
*/
|
||||
$value = apply_filters( "woocommerce_get_default_value_for_{$missing_field}", null, $group, $wc_object );
|
||||
|
||||
if ( $value ) {
|
||||
if ( isset( $value ) ) {
|
||||
$meta_data[ $missing_field ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,12 @@
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
/**
|
||||
* Service class that handles the feature flags.
|
||||
* Service class that used to handle feature flags. That functionality
|
||||
* is removed now and it is only used to determine "environment".
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FeatureGating {
|
||||
|
||||
/**
|
||||
* Current flag value.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $flag;
|
||||
|
||||
const EXPERIMENTAL_FLAG = 3;
|
||||
const FEATURE_PLUGIN_FLAG = 2;
|
||||
const CORE_FLAG = 1;
|
||||
|
||||
/**
|
||||
* Current environment
|
||||
*
|
||||
@@ -33,35 +22,16 @@ class FeatureGating {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int $flag Hardcoded flag value. Useful for tests.
|
||||
* @param string $environment Hardcoded environment value. Useful for tests.
|
||||
*/
|
||||
public function __construct( $flag = 0, $environment = 'unset' ) {
|
||||
$this->flag = $flag;
|
||||
public function __construct( $environment = 'unset' ) {
|
||||
$this->environment = $environment;
|
||||
$this->load_flag();
|
||||
$this->load_environment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set correct flag.
|
||||
* Set correct environment.
|
||||
*/
|
||||
public function load_flag() {
|
||||
if ( 0 === $this->flag ) {
|
||||
$default_flag = defined( 'WC_BLOCKS_IS_FEATURE_PLUGIN' ) ? self::FEATURE_PLUGIN_FLAG : self::CORE_FLAG;
|
||||
if ( file_exists( __DIR__ . '/../../../../blocks.ini' ) ) {
|
||||
$allowed_flags = [ self::EXPERIMENTAL_FLAG, self::FEATURE_PLUGIN_FLAG, self::CORE_FLAG ];
|
||||
$woo_options = parse_ini_file( __DIR__ . '/../../../../blocks.ini' );
|
||||
$this->flag = is_array( $woo_options ) && in_array( intval( $woo_options['woocommerce_blocks_phase'] ), $allowed_flags, true ) ? $woo_options['woocommerce_blocks_phase'] : $default_flag;
|
||||
} else {
|
||||
$this->flag = $default_flag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set correct environment.
|
||||
*/
|
||||
public function load_environment() {
|
||||
if ( 'unset' === $this->environment ) {
|
||||
if ( file_exists( __DIR__ . '/../../../../blocks.ini' ) ) {
|
||||
@@ -74,33 +44,6 @@ class FeatureGating {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current flag value.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_flag() {
|
||||
return $this->flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're executing the code in an experimental build mode.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_experimental_build() {
|
||||
return $this->flag >= self::EXPERIMENTAL_FLAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're executing the code in an feature plugin or experimental build mode.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_feature_plugin_build() {
|
||||
return $this->flag >= self::FEATURE_PLUGIN_FLAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current environment value.
|
||||
*
|
||||
@@ -136,46 +79,4 @@ class FeatureGating {
|
||||
public function is_test_environment() {
|
||||
return self::TEST_ENVIRONMENT === $this->environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns core flag value.
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
public static function get_core_flag() {
|
||||
return self::CORE_FLAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feature plugin flag value.
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
public static function get_feature_plugin_flag() {
|
||||
return self::FEATURE_PLUGIN_FLAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns experimental flag value.
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
public static function get_experimental_flag() {
|
||||
return self::EXPERIMENTAL_FLAG;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the block templates controller refactor should be used to display blocks.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_block_templates_controller_refactor_enabled() {
|
||||
if ( file_exists( __DIR__ . '/../../../../blocks.ini' ) ) {
|
||||
$conf = parse_ini_file( __DIR__ . '/../../../../blocks.ini' );
|
||||
return $this->is_development_environment() && isset( $conf['use_block_templates_controller_refactor'] ) && true === (bool) $conf['use_block_templates_controller_refactor'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class Notices {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all notices with the new block based notices.
|
||||
* Replaces all notices with the new block-based notices.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,7 @@ class Installer {
|
||||
|
||||
/**
|
||||
* Modifies default page content replacing it with classic shortcode block.
|
||||
* We check for shortcode as default because after WooCommerce 8.3, block based checkout is used by default.
|
||||
* We check for shortcode as default because after WooCommerce 8.3, block-based checkout is used by default.
|
||||
* This only runs on Tools > Create Pages as the filter is not applied on WooCommerce plugin activation.
|
||||
*
|
||||
* @param array $pages Default pages.
|
||||
|
||||
@@ -71,25 +71,6 @@ class Package {
|
||||
return self::get_package()->feature();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're executing the code in an experimental build mode.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_experimental_build() {
|
||||
return self::get_package()->is_experimental_build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we're executing the code in a feature plugin or experimental build mode.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_feature_plugin_build() {
|
||||
return self::get_package()->is_feature_plugin_build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the dependency injection container for woocommerce blocks.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
|
||||
/**
|
||||
* AIPatterns class.
|
||||
*/
|
||||
class AIPatterns {
|
||||
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';
|
||||
|
||||
/**
|
||||
* Constructor for the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register_patterns_ai_data_post_type' ) );
|
||||
|
||||
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'schedule_on_option_update' ), 10, 2 );
|
||||
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'update_ai_connection_allowed_option' ), 10, 2 );
|
||||
add_action( 'upgrader_process_complete', array( $this, 'schedule_on_plugin_update' ), 10, 2 );
|
||||
add_action( 'woocommerce_update_patterns_content', array( $this, 'update_patterns_content' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Patterns AI Data post type to store patterns with the AI-generated content.
|
||||
*/
|
||||
public function register_patterns_ai_data_post_type() {
|
||||
register_post_type(
|
||||
self::PATTERNS_AI_DATA_POST_TYPE,
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => __( 'Patterns AI Data', 'woocommerce' ),
|
||||
'singular_name' => __( 'Patterns AI Data', 'woocommerce' ),
|
||||
),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'delete_with_user' => false,
|
||||
'can_export' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the 'woocommerce_blocks_allow_ai_connection' option is set to true if the site is connected to AI.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update_ai_connection_allowed_option(): bool {
|
||||
$ai_connection = new Connection();
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return update_option( 'woocommerce_blocks_allow_ai_connection', false, true );
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return update_option( 'woocommerce_blocks_allow_ai_connection', false, true );
|
||||
}
|
||||
|
||||
return update_option( 'woocommerce_blocks_allow_ai_connection', true, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content when the store description is changed.
|
||||
*
|
||||
* @param string $option The option name.
|
||||
* @param string $value The option value.
|
||||
*/
|
||||
public function schedule_on_option_update( $option, $value ) {
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->schedule_patterns_content_update( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content when the WooCommerce Blocks plugin is updated.
|
||||
*
|
||||
* @param \WP_Upgrader $upgrader_object WP_Upgrader instance.
|
||||
* @param array $options Array of bulk item update data.
|
||||
*/
|
||||
public function schedule_on_plugin_update( $upgrader_object, $options ) {
|
||||
if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
|
||||
foreach ( $options['plugins'] as $plugin ) {
|
||||
if ( str_contains( $plugin, 'woocommerce.php' ) ) {
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
|
||||
if ( $business_description ) {
|
||||
$this->schedule_patterns_content_update( $business_description );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content when the store description is changed.
|
||||
*
|
||||
* @param string $business_description The business description.
|
||||
*/
|
||||
public function schedule_patterns_content_update( $business_description ) {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action_scheduler = WP_PLUGIN_DIR . '/woocommerce/packages/action-scheduler/action-scheduler.php';
|
||||
|
||||
if ( ! file_exists( $action_scheduler ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once $action_scheduler;
|
||||
|
||||
as_schedule_single_action( time(), 'woocommerce_update_patterns_content', array( $business_description ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the patterns content.
|
||||
*
|
||||
* @return bool|string|\WP_Error
|
||||
*/
|
||||
public function update_patterns_content() {
|
||||
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
|
||||
|
||||
if ( ! $allow_ai_connection ) {
|
||||
return new \WP_Error(
|
||||
'ai_connection_not_allowed',
|
||||
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$ai_connection = new Connection();
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $site_id->get_error_message();
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token->get_error_message();
|
||||
}
|
||||
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
|
||||
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
|
||||
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images->get_error_message();
|
||||
}
|
||||
|
||||
$populate_patterns = ( new UpdatePatterns() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_patterns ) ) {
|
||||
return $populate_patterns->get_error_message();
|
||||
}
|
||||
|
||||
$populate_products = ( new UpdateProducts() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_products ) ) {
|
||||
return $populate_products->get_error_message();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* PatternsToolkit class.
|
||||
*/
|
||||
class PTKClient {
|
||||
/**
|
||||
* The Patterns Toolkit API URL
|
||||
*/
|
||||
const PATTERNS_TOOLKIT_URL = 'https://public-api.wordpress.com/rest/v1/ptk/patterns/';
|
||||
|
||||
/**
|
||||
* Fetch the WooCommerce patterns from the Patterns Toolkit (PTK) API.
|
||||
*
|
||||
* @param array $options Options for fetching patterns.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function fetch_patterns( array $options = array() ) {
|
||||
$locale = get_user_locale();
|
||||
$lang = preg_replace( '/(_.*)$/', '', $locale );
|
||||
|
||||
$ptk_url = self::PATTERNS_TOOLKIT_URL . $lang;
|
||||
|
||||
if ( isset( $options['site'] ) ) {
|
||||
$ptk_url = add_query_arg( 'site', $options['site'], $ptk_url );
|
||||
}
|
||||
|
||||
if ( isset( $options['categories'] ) ) {
|
||||
$ptk_url = add_query_arg( 'categories', implode( ',', $options['categories'] ), $ptk_url );
|
||||
}
|
||||
|
||||
if ( isset( $options['per_page'] ) ) {
|
||||
$ptk_url = add_query_arg( 'per_page', $options['per_page'], $ptk_url );
|
||||
}
|
||||
|
||||
$patterns = wp_safe_remote_get( $ptk_url );
|
||||
if ( is_wp_error( $patterns ) || 200 !== wp_remote_retrieve_response_code( $patterns ) ) {
|
||||
return new WP_Error(
|
||||
'patterns_toolkit_api_error',
|
||||
__( 'Failed to connect with the Patterns Toolkit API: try again later.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $patterns );
|
||||
|
||||
if ( empty( $body ) ) {
|
||||
return new WP_Error(
|
||||
'patterns_toolkit_api_error',
|
||||
__( 'Empty response received from the Patterns Toolkit API.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
$decoded_body = json_decode( $body, true );
|
||||
|
||||
if ( ! is_array( $decoded_body ) ) {
|
||||
return new WP_Error(
|
||||
'patterns_toolkit_api_error',
|
||||
__( 'Wrong response received from the Patterns Toolkit API: try again later.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
return $decoded_body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use WP_Upgrader;
|
||||
|
||||
/**
|
||||
* PTKPatterns class.
|
||||
*/
|
||||
class PTKPatternsStore {
|
||||
const TRANSIENT_NAME = 'ptk_patterns';
|
||||
|
||||
// Some patterns need to be excluded because they have dependencies which
|
||||
// are not installed by default (like Jetpack). Otherwise, the user
|
||||
// would see an error when trying to insert them in the editor.
|
||||
const EXCLUDED_PATTERNS = array( '13923', '14781', '14779', '13666', '13664', '13660', '13588', '14922', '14880', '13596', '13967', '13958', '15050', '15027' );
|
||||
|
||||
/**
|
||||
* PatternsToolkit instance.
|
||||
*
|
||||
* @var PTKClient $ptk_client
|
||||
*/
|
||||
private PTKClient $ptk_client;
|
||||
|
||||
/**
|
||||
* Constructor for the class.
|
||||
*
|
||||
* @param PTKClient $ptk_client An instance of PatternsToolkit.
|
||||
*/
|
||||
public function __construct( PTKClient $ptk_client ) {
|
||||
$this->ptk_client = $ptk_client;
|
||||
|
||||
if ( Features::is_enabled( 'pattern-toolkit-full-composability' ) ) {
|
||||
// We want to flush the cached patterns when:
|
||||
// - The WooCommerce plugin is deactivated.
|
||||
// - The `woocommerce_allow_tracking` option is disabled.
|
||||
//
|
||||
// We also want to re-fetch the patterns and update the cache when:
|
||||
// - The `woocommerce_allow_tracking` option changes to enabled.
|
||||
// - The WooCommerce plugin is activated (if `woocommerce_allow_tracking` is enabled).
|
||||
// - The WooCommerce plugin is updated.
|
||||
|
||||
add_action( 'woocommerce_activated_plugin', array( $this, 'flush_or_fetch_patterns' ), 10, 2 );
|
||||
add_action( 'update_option_woocommerce_allow_tracking', array( $this, 'flush_or_fetch_patterns' ), 10, 2 );
|
||||
add_action( 'deactivated_plugin', array( $this, 'flush_cached_patterns' ), 10, 2 );
|
||||
add_action( 'upgrader_process_complete', array( $this, 'fetch_patterns_on_plugin_update' ), 10, 2 );
|
||||
|
||||
// This is the scheduled action that takes care of flushing and re-fetching the patterns from the PTK API.
|
||||
add_action( 'fetch_patterns', array( $this, 'fetch_patterns' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cached patterns when the `woocommerce_allow_tracking` option is disabled.
|
||||
* Resets and fetch the patterns from the PTK when it is enabled (if the scheduler
|
||||
* is initialized, it's done asynchronously via a scheduled action).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush_or_fetch_patterns() {
|
||||
if ( $this->allowed_tracking_is_enabled() ) {
|
||||
$this->schedule_fetch_patterns();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->flush_cached_patterns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an async action to fetch the PTK patterns when the scheduler is initialized.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_fetch_patterns() {
|
||||
if ( did_action( 'action_scheduler_init' ) ) {
|
||||
$this->schedule_action_if_not_pending( 'fetch_patterns' );
|
||||
} else {
|
||||
add_action(
|
||||
'action_scheduler_init',
|
||||
function () {
|
||||
$this->schedule_action_if_not_pending( 'fetch_patterns' );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an action if it's not already pending.
|
||||
*
|
||||
* @param string $action The action name to schedule.
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_action_if_not_pending( $action ) {
|
||||
if ( as_has_scheduled_action( $action ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
as_schedule_single_action( time(), $action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the patterns from the Patterns Toolkit cache.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_patterns() {
|
||||
$patterns = get_transient( self::TRANSIENT_NAME );
|
||||
|
||||
// Only if the transient is not set, we schedule fetching the patterns from the PTK.
|
||||
if ( false === $patterns ) {
|
||||
$this->schedule_fetch_patterns();
|
||||
return array();
|
||||
}
|
||||
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter patterns to exclude those with the given IDs.
|
||||
*
|
||||
* @param array $patterns The patterns to filter.
|
||||
* @param array $pattern_ids The pattern IDs to exclude.
|
||||
* @return array
|
||||
*/
|
||||
private function filter_patterns( array $patterns, array $pattern_ids ) {
|
||||
return array_filter(
|
||||
$patterns,
|
||||
function ( $pattern ) use ( $pattern_ids ) {
|
||||
if ( ! isset( $pattern['ID'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( isset( $pattern['post_type'] ) && 'wp_block' !== $pattern['post_type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! in_array( (string) $pattern['ID'], $pattern_ids, true );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-fetch the patterns when the WooCommerce plugin is updated.
|
||||
*
|
||||
* @param WP_Upgrader $upgrader_object WP_Upgrader instance.
|
||||
* @param array $options Array of bulk item update data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function fetch_patterns_on_plugin_update( $upgrader_object, $options ) {
|
||||
if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
|
||||
foreach ( $options['plugins'] as $plugin ) {
|
||||
if ( str_contains( $plugin, 'woocommerce.php' ) ) {
|
||||
$this->schedule_fetch_patterns();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cached patterns to fetch them again from the PTK.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush_cached_patterns() {
|
||||
delete_transient( self::TRANSIENT_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cached patterns and fetch them again from the PTK API.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function fetch_patterns() {
|
||||
if ( ! $this->allowed_tracking_is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->flush_cached_patterns();
|
||||
|
||||
$patterns = $this->ptk_client->fetch_patterns(
|
||||
array(
|
||||
'categories' => array( 'intro', 'about', 'services', 'testimonials' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $patterns ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf(
|
||||
// translators: %s is a generated error message.
|
||||
__( 'Failed to get the patterns from the PTK: "%s"', 'woocommerce' ),
|
||||
$patterns->get_error_message()
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$patterns = $this->filter_patterns( $patterns, self::EXCLUDED_PATTERNS );
|
||||
|
||||
set_transient( self::TRANSIENT_NAME, $patterns );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user allowed tracking.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function allowed_tracking_is_enabled(): bool {
|
||||
return 'yes' === get_option( 'woocommerce_allow_tracking' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* PatternRegistry class.
|
||||
*/
|
||||
class PatternRegistry {
|
||||
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
||||
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
|
||||
|
||||
/**
|
||||
* Register a block pattern.
|
||||
*
|
||||
* @param string $source The pattern source.
|
||||
* @param array $pattern_data The pattern data.
|
||||
* @param array $dictionary The patterns' dictionary.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_block_pattern( $source, $pattern_data, $dictionary ) {
|
||||
if ( empty( $pattern_data['slug'] ) ) {
|
||||
_doing_it_wrong(
|
||||
'register_block_patterns',
|
||||
esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: file name. */
|
||||
__( 'Could not register pattern "%s" as a block pattern ("Slug" field missing)', 'woocommerce' ),
|
||||
$source
|
||||
)
|
||||
),
|
||||
'6.0.0'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! preg_match( self::SLUG_REGEX, $pattern_data['slug'] ) ) {
|
||||
_doing_it_wrong(
|
||||
'register_block_patterns',
|
||||
esc_html(
|
||||
sprintf(
|
||||
/* translators: %1s: file name; %2s: slug value found. */
|
||||
__( 'Could not register pattern "%1$s" as a block pattern (invalid slug "%2$s")', 'woocommerce' ),
|
||||
$source,
|
||||
$pattern_data['slug']
|
||||
)
|
||||
),
|
||||
'6.0.0'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( \WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $pattern_data['featureFlag'] ) && '' !== $pattern_data['featureFlag'] && ! Features::is_enabled( $pattern_data['featureFlag'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Title is a required property.
|
||||
if ( ! isset( $pattern_data['title'] ) || ! $pattern_data['title'] ) {
|
||||
_doing_it_wrong(
|
||||
'register_block_patterns',
|
||||
esc_html(
|
||||
sprintf(
|
||||
/* translators: %1s: file name; %2s: slug value found. */
|
||||
__( 'Could not register pattern "%s" as a block pattern ("Title" field missing)', 'woocommerce' ),
|
||||
$source
|
||||
)
|
||||
),
|
||||
'6.0.0'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// For properties of type array, parse data as comma-separated.
|
||||
foreach ( array( 'categories', 'keywords', 'blockTypes' ) as $property ) {
|
||||
if ( ! empty( $pattern_data[ $property ] ) ) {
|
||||
if ( is_array( $pattern_data[ $property ] ) ) {
|
||||
$pattern_data[ $property ] = array_values(
|
||||
array_map(
|
||||
function ( $property ) {
|
||||
return $property['title'];
|
||||
},
|
||||
$pattern_data[ $property ]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$pattern_data[ $property ] = array_filter(
|
||||
preg_split(
|
||||
self::COMMA_SEPARATED_REGEX,
|
||||
(string) $pattern_data[ $property ]
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unset( $pattern_data[ $property ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Parse properties of type int.
|
||||
foreach ( array( 'viewportWidth' ) as $property ) {
|
||||
if ( ! empty( $pattern_data[ $property ] ) ) {
|
||||
$pattern_data[ $property ] = (int) $pattern_data[ $property ];
|
||||
} else {
|
||||
unset( $pattern_data[ $property ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Parse properties of type bool.
|
||||
foreach ( array( 'inserter' ) as $property ) {
|
||||
if ( ! empty( $pattern_data[ $property ] ) ) {
|
||||
$pattern_data[ $property ] = in_array(
|
||||
strtolower( $pattern_data[ $property ] ),
|
||||
array( 'yes', 'true' ),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
unset( $pattern_data[ $property ] );
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
|
||||
$pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', 'woocommerce' );
|
||||
if ( ! empty( $pattern_data['description'] ) ) {
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
|
||||
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woocommerce' );
|
||||
}
|
||||
|
||||
$pattern_data_from_dictionary = $this->get_pattern_from_dictionary( $dictionary, $pattern_data['slug'] );
|
||||
|
||||
if ( file_exists( $source ) ) {
|
||||
// The actual pattern content is the output of the file.
|
||||
ob_start();
|
||||
|
||||
/*
|
||||
For patterns that can have AI-generated content, we need to get its content from the dictionary and pass
|
||||
it to the pattern file through the "$content" and "$images" variables.
|
||||
This is to avoid having to access the dictionary for each pattern when it's registered or inserted.
|
||||
Before the "$content" and "$images" variables were populated in each pattern. Since the pattern
|
||||
registration happens in the init hook, the dictionary was being access one for each pattern and
|
||||
for each page load. This way we only do it once on registration.
|
||||
For more context: https://github.com/woocommerce/woocommerce-blocks/pull/11733
|
||||
*/
|
||||
|
||||
$content = array();
|
||||
$images = array();
|
||||
if ( ! is_null( $pattern_data_from_dictionary ) ) {
|
||||
$content = $pattern_data_from_dictionary['content'];
|
||||
$images = $pattern_data_from_dictionary['images'] ?? array();
|
||||
}
|
||||
|
||||
include $source;
|
||||
$pattern_data['content'] = ob_get_clean();
|
||||
|
||||
if ( ! $pattern_data['content'] ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $pattern_data['categories'] ) ) {
|
||||
foreach ( $pattern_data['categories'] as $key => $category ) {
|
||||
$category_slug = _wp_to_kebab_case( $category );
|
||||
|
||||
$pattern_data['categories'][ $key ] = $category_slug;
|
||||
|
||||
register_block_pattern_category(
|
||||
$category_slug,
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
|
||||
array( 'label' => __( $category, 'woocommerce' ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
register_block_pattern( $pattern_data['slug'], $pattern_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the patterns dictionary to get the pattern data corresponding to the pattern slug.
|
||||
*
|
||||
* @param array $dictionary The patterns' dictionary.
|
||||
* @param string $slug The pattern slug.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_pattern_from_dictionary( $dictionary, $slug ) {
|
||||
foreach ( $dictionary as $pattern_dictionary ) {
|
||||
if ( isset( $pattern_dictionary['slug'] ) && $pattern_dictionary['slug'] === $slug ) {
|
||||
return $pattern_dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Templates;
|
||||
|
||||
/**
|
||||
* ProductFiltersOverlayTemplate class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
|
||||
|
||||
/**
|
||||
* The slug of the template.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SLUG = 'product-filters-overlay';
|
||||
|
||||
/**
|
||||
* The template part area where the template part belongs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $template_area = 'product-filters-overlay';
|
||||
|
||||
/**
|
||||
* Initialization method.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'default_wp_template_part_areas', array( $this, 'register_product_filters_overlay_template_part_area' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title of the template.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_template_title() {
|
||||
return _x( 'Filters Overlay', 'Template name', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the template.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
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 ) );
|
||||
}
|
||||
}
|
||||
@@ -316,6 +316,7 @@ class BlockTemplateUtils {
|
||||
$wp_template_part_filenames = array(
|
||||
'checkout-header.html',
|
||||
'mini-cart.html',
|
||||
'product-filters-overlay.html',
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
@@ -44,6 +44,32 @@ class CartCheckoutUtils {
|
||||
return $checkout_page_id && has_block( 'woocommerce/checkout', $checkout_page_id );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the template overriding the page loads the page content or not.
|
||||
* Templates by default load the page content, but if that block is deleted the content can get out of sync with the one presented in the page editor.
|
||||
*
|
||||
* @param string $block The block to check.
|
||||
*
|
||||
* @return bool true if the template has out of sync content.
|
||||
*/
|
||||
public static function is_overriden_by_custom_template_content( string $block ): bool {
|
||||
|
||||
$block = str_replace( 'woocommerce/', '', $block );
|
||||
|
||||
if ( wc_current_theme_is_fse_theme() ) {
|
||||
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( array( 'page-' . $block ) );
|
||||
foreach ( $templates_from_db as $template ) {
|
||||
if ( ! has_block( 'woocommerce/page-content-wrapper', $template->content ) ) {
|
||||
// Return true if the template does not load the page content via the woocommerce/page-content-wrapper block.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets country codes, names, states, and locale information.
|
||||
*
|
||||
|
||||
@@ -76,6 +76,8 @@ class CLIRunner {
|
||||
WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
|
||||
WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
|
||||
WP_CLI::add_command( 'wc hpos backfill', array( $this, 'backfill' ) );
|
||||
WP_CLI::add_command( 'wc hpos compatibility-mode enable', array( $this, 'enable_compat_mode' ) );
|
||||
WP_CLI::add_command( 'wc hpos compatibility-mode disable', array( $this, 'disable_compat_mode' ) );
|
||||
|
||||
WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) ); // Fully deprecated. No longer works.
|
||||
}
|
||||
@@ -803,12 +805,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
}
|
||||
|
||||
if ( $assoc_args['with-sync'] && $table_exists ) {
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'Sync is already enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, true );
|
||||
WP_CLI::success( __( 'Sync enabled.', 'woocommerce' ) );
|
||||
}
|
||||
$this->toggle_compat_mode( true );
|
||||
}
|
||||
|
||||
if ( ! $enable_hpos ) {
|
||||
@@ -889,15 +886,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
}
|
||||
|
||||
if ( $assoc_args['with-sync'] ) {
|
||||
if ( ! $data_synchronizer->data_sync_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'Sync is already disabled.', 'woocommerce' ) );
|
||||
}
|
||||
$feature_controller->change_feature_enable( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, false );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'Sync could not be disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::success( __( 'Sync disabled.', 'woocommerce' ) );
|
||||
}
|
||||
$this->toggle_compat_mode( false );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1223,4 +1212,55 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables compatibility mode, which keeps the HPOS and posts datastore in sync.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*/
|
||||
public function enable_compat_mode(): void {
|
||||
$this->toggle_compat_mode( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables compatibility mode, which keeps the HPOS and posts datastore in sync.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*/
|
||||
public function disable_compat_mode(): void {
|
||||
$this->toggle_compat_mode( false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles compatibility mode on or off.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*
|
||||
* @param bool $enabled TRUE to enable compatibility mode, FALSE to disable.
|
||||
*/
|
||||
private function toggle_compat_mode( bool $enabled ): void {
|
||||
if ( ! $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::error( __( 'HPOS tables do not exist.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$currently_enabled = $this->synchronizer->data_sync_is_enabled();
|
||||
|
||||
if ( $currently_enabled === $enabled ) {
|
||||
if ( $enabled ) {
|
||||
WP_CLI::warning( __( 'Compatibility mode is already enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::warning( __( 'Compatibility mode is already disabled.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
update_option( $this->synchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, wc_bool_to_string( $enabled ) );
|
||||
|
||||
if ( $enabled ) {
|
||||
WP_CLI::success( __( 'Compatibility mode enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::success( __( 'Compatibility mode disabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
unset( $context_for_entry['source'] );
|
||||
|
||||
if ( ! empty( $context_for_entry ) ) {
|
||||
$formatted_context = wp_json_encode( $context_for_entry );
|
||||
$message .= " CONTEXT: $formatted_context";
|
||||
$formatted_context = wp_json_encode( $context_for_entry, JSON_UNESCAPED_UNICODE );
|
||||
$message .= stripslashes( " CONTEXT: $formatted_context" );
|
||||
}
|
||||
|
||||
$entry = "$time_string $level_string $message";
|
||||
|
||||
@@ -715,13 +715,18 @@ class PageController {
|
||||
$message_chunks = explode( 'CONTEXT:', $segments[2], 2 );
|
||||
if ( isset( $message_chunks[1] ) ) {
|
||||
try {
|
||||
$maybe_json = stripslashes( html_entity_decode( trim( $message_chunks[1] ) ) );
|
||||
$context = json_decode( $maybe_json, false, 512, JSON_THROW_ON_ERROR );
|
||||
$maybe_json = html_entity_decode( addslashes( trim( $message_chunks[1] ) ) );
|
||||
|
||||
// Decode for validation.
|
||||
$context = json_decode( $maybe_json, false, 512, JSON_THROW_ON_ERROR );
|
||||
|
||||
// Re-encode to make it pretty.
|
||||
$context = wp_json_encode( $context, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
|
||||
|
||||
$message_chunks[1] = sprintf(
|
||||
'<details><summary>%1$s</summary>%2$s</details>',
|
||||
esc_html__( 'Additional context', 'woocommerce' ),
|
||||
wp_json_encode( $context, JSON_PRETTY_PRINT )
|
||||
stripslashes( $context )
|
||||
);
|
||||
|
||||
$segments[2] = implode( ' ', $message_chunks );
|
||||
|
||||
@@ -474,7 +474,12 @@ class Edit {
|
||||
wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false );
|
||||
?>
|
||||
<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/>
|
||||
<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/>
|
||||
|
||||
<?php
|
||||
$order_status = $this->order->get_status( 'edit' );
|
||||
?>
|
||||
<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $order_status ); ?>"/>
|
||||
<input type="hidden" id="original_post_status" name="original_post_status" value="<?php echo esc_attr( wc_is_order_status( 'wc-' . $order_status ) ? 'wc-' . $order_status : $order_status ); ?>"/>
|
||||
<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/>
|
||||
<input type="hidden" id="post_ID" name="post_ID" value="<?php echo esc_attr( $this->order->get_id() ); ?>"/>
|
||||
<div id="poststuff">
|
||||
|
||||
@@ -1030,33 +1030,8 @@ class ListTable extends WP_List_Table {
|
||||
* @return void
|
||||
*/
|
||||
public function render_order_status_column( WC_Order $order ): void {
|
||||
$tooltip = '';
|
||||
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
|
||||
$comment_count = get_comment_count( $order->get_id() );
|
||||
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
|
||||
$approved_comments_count = absint( $comment_count['approved'] );
|
||||
|
||||
if ( $approved_comments_count ) {
|
||||
$latest_notes = wc_get_order_notes(
|
||||
array(
|
||||
'order_id' => $order->get_id(),
|
||||
'limit' => 1,
|
||||
'orderby' => 'date_created_gmt',
|
||||
)
|
||||
);
|
||||
|
||||
$latest_note = current( $latest_notes );
|
||||
|
||||
if ( isset( $latest_note->content ) && 1 === $approved_comments_count ) {
|
||||
$tooltip = wc_sanitize_tooltip( $latest_note->content );
|
||||
} elseif ( isset( $latest_note->content ) ) {
|
||||
/* translators: %d: notes count */
|
||||
$tooltip = wc_sanitize_tooltip( $latest_note->content . '<br/><small style="display:block">' . sprintf( _n( 'Plus %d other note', 'Plus %d other notes', ( $approved_comments_count - 1 ), 'woocommerce' ), $approved_comments_count - 1 ) . '</small>' );
|
||||
} else {
|
||||
/* translators: %d: notes count */
|
||||
$tooltip = wc_sanitize_tooltip( sprintf( _n( '%d note', '%d notes', $approved_comments_count, 'woocommerce' ), $approved_comments_count ) );
|
||||
}
|
||||
}
|
||||
/* translators: %s: order status label */
|
||||
$tooltip = wc_sanitize_tooltip( $this->get_order_status_label( $order ) );
|
||||
|
||||
// Gracefully handle legacy statuses.
|
||||
if ( in_array( $order->get_status(), array( 'trash', 'draft', 'auto-draft' ), true ) ) {
|
||||
@@ -1072,6 +1047,39 @@ class ListTable extends WP_List_Table {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order status label for an order.
|
||||
*
|
||||
* @param WC_Order $order The order object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
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' ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Provides an opportunity to modify and extend the order status labels.
|
||||
*
|
||||
* @param array $action Order actions.
|
||||
* @param WC_Order $order Current order object.
|
||||
* @since 9.1.0
|
||||
*/
|
||||
$status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names );
|
||||
|
||||
$status_name = wc_get_order_status_name( $order->get_status() );
|
||||
|
||||
return isset( $status_names[ $status_name ] ) ? $status_names[ $status_name ] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders order billing information.
|
||||
*
|
||||
|
||||
@@ -143,7 +143,7 @@ class PageController {
|
||||
}
|
||||
|
||||
// Not on an Orders page.
|
||||
if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
|
||||
if ( empty( $plugin_page ) || 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ class Reviews {
|
||||
self::add_action( 'wp_ajax_replyto-comment', [ $this, 'handle_reply_to_review' ], -1 );
|
||||
|
||||
self::add_filter( 'parent_file', [ $this, 'edit_review_parent_file' ] );
|
||||
self::add_filter( 'gettext', [ $this, 'edit_comments_screen_text' ], 10, 2 );
|
||||
self::add_action( 'admin_notices', [ $this, 'display_notices' ] );
|
||||
}
|
||||
|
||||
@@ -522,45 +521,6 @@ class Reviews {
|
||||
return $parent_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces Edit/Moderate Comment title/headline with Edit Review, when editing/moderating a review.
|
||||
*
|
||||
* @param string|mixed $translation Translated text.
|
||||
* @param string|mixed $text Text to translate.
|
||||
* @return string|mixed Translated text.
|
||||
*/
|
||||
protected function edit_comments_screen_text( $translation, $text ) {
|
||||
global $comment;
|
||||
|
||||
// Bail out if not a text we should replace.
|
||||
if ( ! in_array( $text, [ 'Edit Comment', 'Moderate Comment' ], true ) ) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
// Try to get comment from query params when not in context already.
|
||||
if ( ! $comment && isset( $_GET['action'], $_GET['c'] ) && $_GET['action'] === 'editcomment' ) {
|
||||
$comment_id = absint( $_GET['c'] );
|
||||
$comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
|
||||
$is_reply = isset( $comment->comment_parent ) && $comment->comment_parent > 0;
|
||||
|
||||
// Only replace the translated text if we are editing a comment left on a product (ie. a review).
|
||||
if ( isset( $comment->comment_post_ID ) && get_post_type( $comment->comment_post_ID ) === 'product' ) {
|
||||
if ( $text === 'Edit Comment' ) {
|
||||
$translation = $is_reply
|
||||
? __( 'Edit Review Reply', 'woocommerce' )
|
||||
: __( 'Edit Review', 'woocommerce' );
|
||||
} elseif ( $text === 'Moderate Comment' ) {
|
||||
$translation = $is_reply
|
||||
? __( 'Moderate Review Reply', 'woocommerce' )
|
||||
: __( 'Moderate Review', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of `ReviewsListTable`, with the screen argument specified.
|
||||
*
|
||||
|
||||
@@ -52,11 +52,13 @@ class WcPayWelcomePage {
|
||||
/**
|
||||
* Whether the WooPayments welcome page should be visible.
|
||||
*
|
||||
* @param bool $skip_wcpay_active Whether to skip the check for the WooPayments plugin being active.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function must_be_visible(): bool {
|
||||
public function must_be_visible( $skip_wcpay_active = false ): bool {
|
||||
// The WooPayments plugin must not be active.
|
||||
if ( $this->is_wcpay_active() ) {
|
||||
if ( ! $skip_wcpay_active && $this->is_wcpay_active() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -174,18 +176,20 @@ class WcPayWelcomePage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds allowed promo notes from the WooPayments incentive.
|
||||
* Adds allowed promo notes for the WooPayments incentives.
|
||||
*
|
||||
* @param array $promo_notes Allowed promo notes.
|
||||
* @return array
|
||||
*/
|
||||
public function allowed_promo_notes( $promo_notes = [] ): array {
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
// Note: We need to disregard if WooPayments is active when adding the promo note to the list of
|
||||
// allowed promo notes. The AJAX call that adds the promo note happens after WooPayments is installed and activated.
|
||||
// Return early if the incentive page must not be visible, without checking if WooPayments is active.
|
||||
if ( ! $this->must_be_visible( true ) ) {
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
// Add our incentive ID to the promo notes.
|
||||
// Add our incentive ID to the allowed promo notes so it can be added to the store.
|
||||
$promo_notes[] = $this->get_incentive()['id'];
|
||||
|
||||
return $promo_notes;
|
||||
|
||||
@@ -46,6 +46,11 @@ class BatchProcessingController {
|
||||
const ENQUEUED_PROCESSORS_OPTION_NAME = 'wc_pending_batch_processes';
|
||||
const ACTION_GROUP = 'wc_batch_processes';
|
||||
|
||||
/**
|
||||
* Maximum number of failures per processor before it gets dequeued.
|
||||
*/
|
||||
const FAILING_PROCESS_MAX_ATTEMPTS_DEFAULT = 5;
|
||||
|
||||
/**
|
||||
* Instance of WC_Logger class.
|
||||
*
|
||||
@@ -75,6 +80,13 @@ class BatchProcessingController {
|
||||
2
|
||||
);
|
||||
|
||||
add_action(
|
||||
'shutdown',
|
||||
function () {
|
||||
$this->remove_or_retry_failed_processors();
|
||||
}
|
||||
);
|
||||
|
||||
$this->logger = wc_get_logger();
|
||||
}
|
||||
|
||||
@@ -156,9 +168,14 @@ class BatchProcessingController {
|
||||
$still_pending = count( $batch_processor->get_next_batch_to_process( 1 ) ) > 0;
|
||||
if ( ( $error instanceof \Exception ) ) {
|
||||
// The batch processing failed and no items were processed:
|
||||
// reschedule the processing with a delay, and also throw the error
|
||||
// so Action Scheduler will ignore the rescheduling if this happens repeatedly.
|
||||
$this->schedule_batch_processing( $processor_class_name, true );
|
||||
// reschedule the processing with a delay, unless this is a repeatead failure.
|
||||
if ( $this->is_consistently_failing( $batch_processor ) ) {
|
||||
$this->log_consistent_failure( $batch_processor, $this->get_process_details( $batch_processor ) );
|
||||
$this->remove_processor( $processor_class_name );
|
||||
} else {
|
||||
$this->schedule_batch_processing( $processor_class_name, true );
|
||||
}
|
||||
|
||||
throw $error;
|
||||
}
|
||||
if ( $still_pending ) {
|
||||
@@ -206,9 +223,12 @@ class BatchProcessingController {
|
||||
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,
|
||||
'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,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -216,12 +236,12 @@ class BatchProcessingController {
|
||||
/**
|
||||
* Get the name of the option where we will be saving state for a given processor.
|
||||
*
|
||||
* @param BatchProcessorInterface $batch_processor Batch processor instance.
|
||||
* @param BatchProcessorInterface|string $batch_processor Batch processor instance or class name.
|
||||
*
|
||||
* @return string Option name.
|
||||
*/
|
||||
private function get_processor_state_option_name( BatchProcessorInterface $batch_processor ): string {
|
||||
$class_name = get_class( $batch_processor );
|
||||
private function get_processor_state_option_name( $batch_processor ): string {
|
||||
$class_name = is_a( $batch_processor, BatchProcessorInterface::class ) ? get_class( $batch_processor ) : $batch_processor;
|
||||
$class_md5 = md5( $class_name );
|
||||
// truncate the class name so we know that it will fit in the option name column along with md5 hash and prefix.
|
||||
$class_name = substr( $class_name, 0, 140 );
|
||||
@@ -239,16 +259,41 @@ class BatchProcessingController {
|
||||
$current_status = $this->get_process_details( $batch_processor );
|
||||
$current_status['total_time_spent'] += $time_taken;
|
||||
$current_status['last_error'] = null !== $last_error ? $last_error->getMessage() : null;
|
||||
|
||||
if ( null !== $last_error ) {
|
||||
$current_status['recent_failures'] = ( $current_status['recent_failures'] ?? 0 ) + 1;
|
||||
$current_status['batch_last_failure'] = current_time( 'mysql' );
|
||||
|
||||
if ( is_null( $current_status['batch_first_failure'] ) ) {
|
||||
$current_status['batch_first_failure'] = $current_status['batch_last_failure'];
|
||||
}
|
||||
} else {
|
||||
$current_status['recent_failures'] = 0;
|
||||
$current_status['batch_first_failure'] = null;
|
||||
$current_status['batch_last_failure'] = null;
|
||||
}
|
||||
|
||||
update_option( $this->get_processor_state_option_name( $batch_processor ), $current_status, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the option where we store state for a given processor.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
*/
|
||||
private function clear_processor_state( string $processor_class_name ): void {
|
||||
delete_option( $this->get_processor_state_option_name( $processor_class_name ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a processing action for a single processor.
|
||||
*
|
||||
* @param string $processor_class_name Fully qualified class name of the processor.
|
||||
* @param bool $with_delay Whether to schedule the action for immediate execution or for later.
|
||||
*/
|
||||
private function schedule_batch_processing( string $processor_class_name, bool $with_delay = false ) : void {
|
||||
private function schedule_batch_processing( string $processor_class_name, bool $with_delay = false ): void {
|
||||
$time = $with_delay ? time() + MINUTE_IN_SECONDS : time();
|
||||
as_schedule_single_action( $time, self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
||||
}
|
||||
@@ -273,7 +318,7 @@ class BatchProcessingController {
|
||||
* @return BatchProcessorInterface Instance of batch processor for the given class.
|
||||
* @throws \Exception If it's not possible to get an instance of the class.
|
||||
*/
|
||||
private function get_processor_instance( string $processor_class_name ) : BatchProcessorInterface {
|
||||
private function get_processor_instance( string $processor_class_name ): BatchProcessorInterface {
|
||||
|
||||
$container = wc_get_container();
|
||||
$processor = $container->has( $processor_class_name ) ? $container->get( $processor_class_name ) : null;
|
||||
@@ -303,7 +348,7 @@ class BatchProcessingController {
|
||||
*
|
||||
* @return array List (of string) of the class names of the enqueued processors.
|
||||
*/
|
||||
public function get_enqueued_processors() : array {
|
||||
public function get_enqueued_processors(): array {
|
||||
return get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
|
||||
}
|
||||
|
||||
@@ -315,6 +360,7 @@ class BatchProcessingController {
|
||||
private function dequeue_processor( string $processor_class_name ): void {
|
||||
$pending_processes = $this->get_enqueued_processors();
|
||||
if ( in_array( $processor_class_name, $pending_processes, true ) ) {
|
||||
$this->clear_processor_state( $processor_class_name );
|
||||
$pending_processes = array_diff( $pending_processes, array( $processor_class_name ) );
|
||||
$this->set_enqueued_processors( $pending_processes );
|
||||
}
|
||||
@@ -336,7 +382,7 @@ class BatchProcessingController {
|
||||
*
|
||||
* @return bool True if the processor is enqueued.
|
||||
*/
|
||||
public function is_enqueued( string $processor_class_name ) : bool {
|
||||
public function is_enqueued( string $processor_class_name ): bool {
|
||||
return in_array( $processor_class_name, $this->get_enqueued_processors(), true );
|
||||
}
|
||||
|
||||
@@ -358,6 +404,7 @@ class BatchProcessingController {
|
||||
} else {
|
||||
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, $enqueued_processors, false );
|
||||
as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME, array( $processor_class_name ) );
|
||||
$this->clear_processor_state( $processor_class_name );
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -369,6 +416,11 @@ class BatchProcessingController {
|
||||
public function force_clear_all_processes(): void {
|
||||
as_unschedule_all_actions( self::PROCESS_SINGLE_BATCH_ACTION_NAME );
|
||||
as_unschedule_all_actions( self::WATCHDOG_ACTION_NAME );
|
||||
|
||||
foreach ( $this->get_enqueued_processors() as $processor ) {
|
||||
$this->clear_processor_state( $processor );
|
||||
}
|
||||
|
||||
update_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array(), false );
|
||||
}
|
||||
|
||||
@@ -379,19 +431,23 @@ class BatchProcessingController {
|
||||
* @param BatchProcessorInterface $batch_processor Batch processor instance.
|
||||
* @param array $batch Batch that was being processed.
|
||||
*/
|
||||
protected function log_error( \Exception $error, BatchProcessorInterface $batch_processor, array $batch ) : void {
|
||||
$batch_detail_string = '';
|
||||
protected function log_error( \Exception $error, BatchProcessorInterface $batch_processor, array $batch ): void {
|
||||
$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}";
|
||||
$error_context = array(
|
||||
'exception' => $error,
|
||||
'source' => 'batch-processing',
|
||||
);
|
||||
|
||||
// Log only first and last, as the entire batch may be too big.
|
||||
if ( count( $batch ) > 0 ) {
|
||||
$batch_detail_string = "\n" . wp_json_encode(
|
||||
$error_context = array_merge(
|
||||
$error_context,
|
||||
array(
|
||||
'batch_start' => $batch[0],
|
||||
'batch_end' => end( $batch ),
|
||||
),
|
||||
JSON_PRETTY_PRINT
|
||||
)
|
||||
);
|
||||
}
|
||||
$error_message = "Error processing batch for {$batch_processor->get_name()}: {$error->getMessage()}" . $batch_detail_string;
|
||||
|
||||
/**
|
||||
* Filters the error message for a batch processing.
|
||||
@@ -400,12 +456,115 @@ class BatchProcessingController {
|
||||
* @param \Exception $error The exception that was thrown by the processor.
|
||||
* @param BatchProcessorInterface $batch_processor The processor that threw the exception.
|
||||
* @param array $batch The batch that was being processed.
|
||||
* @param array $error_context Context to be passed to the logging function.
|
||||
* @return string The actual error message that will be logged.
|
||||
*
|
||||
* @since 6.8.0
|
||||
*/
|
||||
$error_message = apply_filters( 'wc_batch_processing_log_message', $error_message, $error, $batch_processor, $batch );
|
||||
$error_message = apply_filters( 'wc_batch_processing_log_message', $error_message, $error, $batch_processor, $batch, $error_context );
|
||||
|
||||
$this->logger->error( $error_message, array( 'exception' => $error ) );
|
||||
$this->logger->error( $error_message, $error_context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given processor is consistently failing based on how many recent consecutive failures it has had.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*
|
||||
* @param BatchProcessorInterface $batch_processor The processor that we want to check.
|
||||
* @return boolean TRUE if processor is consistently failing. FALSE otherwise.
|
||||
*/
|
||||
private function is_consistently_failing( BatchProcessorInterface $batch_processor ): bool {
|
||||
$process_details = $this->get_process_details( $batch_processor );
|
||||
$max_attempts = absint(
|
||||
/**
|
||||
* Controls the failure threshold for batch processors. That is, the number of times we'll attempt to
|
||||
* process a batch that has resulted in a failure. Once above this threshold, the processor won't be
|
||||
* re-scheduled and will be removed from the queue.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*
|
||||
* @param int $failure_threshold Maximum number of times for the processor to try processing a given batch.
|
||||
* @param BatchProcessorInterface $batch_processor The processor instance.
|
||||
* @param array $process_details Array with batch processor state.
|
||||
*/
|
||||
apply_filters(
|
||||
'wc_batch_processing_max_attempts',
|
||||
self::FAILING_PROCESS_MAX_ATTEMPTS_DEFAULT,
|
||||
$batch_processor,
|
||||
$process_details
|
||||
)
|
||||
);
|
||||
|
||||
return absint( $process_details['recent_failures'] ?? 0 ) >= max( $max_attempts, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates log entry with details about a batch processor that is consistently failing.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*
|
||||
* @param BatchProcessorInterface $batch_processor The batch processor instance.
|
||||
* @param array $process_details Failing process details.
|
||||
*/
|
||||
private function log_consistent_failure( BatchProcessorInterface $batch_processor, array $process_details ): void {
|
||||
$this->logger->error(
|
||||
"Batch processor {$batch_processor->get_name()} appears to be failing consistently: {$process_details['recent_failures']} unsuccessful attempt(s). No further attempts will be made.",
|
||||
array(
|
||||
'source' => 'batch-processing',
|
||||
'failures' => $process_details['recent_failures'],
|
||||
'first_failure' => $process_details['batch_first_failure'],
|
||||
'last_failure' => $process_details['batch_last_failure'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'shutdown'. This cleanup routine checks enqueued processors and whether they are scheduled or not to
|
||||
* either re-eschedule them or remove them from the queue.
|
||||
* This prevents stale states where Action Scheduler won't schedule any more attempts but we still report the
|
||||
* processor as enqueued.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*/
|
||||
private function remove_or_retry_failed_processors(): void {
|
||||
if ( ! did_action( 'wp_loaded' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$last_error = error_get_last();
|
||||
if ( ! is_null( $last_error ) && in_array( $last_error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
|
||||
// cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
|
||||
$has_scheduled_action = function_exists( 'as_has_scheduled_action') ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
|
||||
|
||||
if ( call_user_func( $has_scheduled_action, self::WATCHDOG_ACTION_NAME ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enqueued_processors = $this->get_enqueued_processors();
|
||||
$unscheduled_processors = array_diff( $enqueued_processors, array_filter( $enqueued_processors, array( $this, 'is_scheduled' ) ) );
|
||||
|
||||
foreach ( $unscheduled_processors as $processor ) {
|
||||
try {
|
||||
$instance = $this->get_processor_instance( $processor );
|
||||
} catch ( \Exception $e ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exception = new \Exception( 'Processor is enqueued but not scheduled. Background job was probably killed or marked as failed. Reattempting execution.' );
|
||||
$this->update_processor_state( $instance, 0, $exception );
|
||||
$this->log_error( $exception, $instance, array() );
|
||||
|
||||
if ( $this->is_consistently_failing( $instance ) ) {
|
||||
$this->log_consistent_failure( $instance, $this->get_process_details( $instance ) );
|
||||
$this->remove_processor( $processor );
|
||||
} else {
|
||||
$this->schedule_batch_processing( $processor, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +39,10 @@ class ComingSoonCacheInvalidator {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate the SiteGround Speed Optimizer cache.
|
||||
if ( function_exists( '\sg_cachepress_purge_cache' ) ) {
|
||||
\sg_cachepress_purge_cache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,27 @@ class ComingSoonRequestHandler {
|
||||
|
||||
$coming_soon_template = get_query_template( 'coming-soon' );
|
||||
|
||||
if ( ! wc_current_theme_is_fse_theme() && $this->coming_soon_helper->is_store_coming_soon() ) {
|
||||
$is_fse_theme = wc_current_theme_is_fse_theme();
|
||||
$is_store_coming_soon = $this->coming_soon_helper->is_store_coming_soon();
|
||||
|
||||
if ( ! $is_fse_theme && $is_store_coming_soon ) {
|
||||
get_header();
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_head',
|
||||
function () {
|
||||
echo "<meta name='woo-coming-soon-page' content='yes'>";
|
||||
}
|
||||
);
|
||||
|
||||
include $coming_soon_template;
|
||||
|
||||
if ( ! wc_current_theme_is_fse_theme() && $this->coming_soon_helper->is_store_coming_soon() ) {
|
||||
if ( ! $is_fse_theme && $is_store_coming_soon ) {
|
||||
get_footer();
|
||||
}
|
||||
|
||||
if ( wc_current_theme_is_fse_theme() ) {
|
||||
if ( $is_fse_theme ) {
|
||||
// Since we've already rendered a template, return null to ensure no other template is rendered.
|
||||
return null;
|
||||
} else {
|
||||
@@ -102,6 +112,17 @@ class ComingSoonRequestHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an exclusion.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*
|
||||
* @param bool $is_excluded If the request should be excluded from Coming soon mode. Defaults to false.
|
||||
*/
|
||||
if ( apply_filters( 'woocommerce_coming_soon_exclude', false ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the private link option is enabled.
|
||||
if ( get_option( 'woocommerce_private_link' ) === 'yes' ) {
|
||||
// Exclude users with a private link.
|
||||
|
||||
@@ -20,6 +20,8 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
|
||||
/**
|
||||
* Option name for this feature.
|
||||
*
|
||||
* @deprecated 9.1.0
|
||||
*/
|
||||
public const OPTION_NAME = 'woocommerce_hpos_legacy_data_cleanup_in_progress';
|
||||
|
||||
@@ -56,17 +58,6 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
*/
|
||||
private $error_logger;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_filter( 'pre_update_option_' . self::OPTION_NAME, array( $this, 'pre_update_option' ), 999, 2 );
|
||||
self::add_action( 'add_option_' . self::OPTION_NAME, array( $this, 'process_added_option' ), 999, 2 );
|
||||
self::add_action( 'update_option_' . self::OPTION_NAME, array( $this, 'process_updated_option' ), 999, 2 );
|
||||
self::add_action( 'delete_option_' . self::OPTION_NAME, array( $this, 'process_deleted_option' ), 999 );
|
||||
self::add_action( 'shutdown', array( $this, 'maybe_reset_state' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class initialization, invoked by the DI container.
|
||||
*
|
||||
@@ -106,7 +97,7 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
* @return int Number of pending records.
|
||||
*/
|
||||
public function get_total_pending_count(): int {
|
||||
return $this->should_run() ? $this->legacy_handler->count_orders_for_cleanup() : 0;
|
||||
return $this->can_run() ? $this->legacy_handler->count_orders_for_cleanup() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,7 +107,7 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
* @return array Batch of records.
|
||||
*/
|
||||
public function get_next_batch_to_process( int $size ): array {
|
||||
return $this->should_run()
|
||||
return $this->can_run()
|
||||
? array_map( 'absint', $this->legacy_handler->get_orders_for_cleanup( array(), $size ) )
|
||||
: array();
|
||||
}
|
||||
@@ -128,7 +119,7 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
*/
|
||||
public function process_batch( array $batch ): void {
|
||||
// This is a destructive operation, so check if we need to bail out just in case.
|
||||
if ( ! $this->should_run() ) {
|
||||
if ( ! $this->can_run() ) {
|
||||
$this->toggle_flag( false );
|
||||
return;
|
||||
}
|
||||
@@ -179,34 +170,28 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
return $this->data_synchronizer->custom_orders_table_is_authoritative() && ! $this->data_synchronizer->data_sync_is_enabled() && ! $this->batch_processing->is_enqueued( get_class( $this->data_synchronizer ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cleanup process should run. That is, it must be activated and {@see can_run()} must return TRUE.
|
||||
*
|
||||
* @return boolean TRUE if the cleanup process should be run, FALSE otherwise.
|
||||
*/
|
||||
public function should_run() {
|
||||
return $this->can_run() && $this->is_flag_set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user has initiated the cleanup process.
|
||||
*
|
||||
* @return boolean TRUE if the user has initiated the cleanup process, FALSE otherwise.
|
||||
*/
|
||||
public function is_flag_set() {
|
||||
return 'yes' === get_option( self::OPTION_NAME, 'no' );
|
||||
return $this->batch_processing->is_enqueued( self::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag that indicates that the cleanup process should be initiated.
|
||||
*
|
||||
* @param boolean $enabled TRUE if the process should be initiated, FALSE if it should be canceled.
|
||||
* @return boolean Whether the legacy data cleanup was initiated or not.
|
||||
*/
|
||||
public function toggle_flag( bool $enabled ) {
|
||||
if ( $enabled ) {
|
||||
update_option( self::OPTION_NAME, wc_bool_to_string( $enabled ) );
|
||||
public function toggle_flag( bool $enabled ): bool {
|
||||
if ( $enabled && $this->can_run() ) {
|
||||
$this->batch_processing->enqueue_processor( self::class );
|
||||
return true;
|
||||
} else {
|
||||
delete_option( self::OPTION_NAME );
|
||||
$this->batch_processing->remove_processor( self::class );
|
||||
return $enabled ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,49 +243,6 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
return array( $entry_id => $entry );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'add_option' to enqueue the batch processor (if needed).
|
||||
*
|
||||
* @param string $option Name of the option to add.
|
||||
* @param mixed $value Value of the option.
|
||||
*/
|
||||
private function process_added_option( string $option, $value ) {
|
||||
$this->process_updated_option( false, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'delete_option' to remove the batch processor.
|
||||
*/
|
||||
private function process_deleted_option() {
|
||||
$this->process_updated_option( false, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'update_option' to enqueue the batch processor as needed.
|
||||
*
|
||||
* @param mixed $old_value Previous option value.
|
||||
* @param mixed $new_value New option value.
|
||||
*/
|
||||
private function process_updated_option( $old_value, $new_value ) {
|
||||
$enable = wc_string_to_bool( $new_value );
|
||||
|
||||
if ( $enable ) {
|
||||
$this->batch_processing->enqueue_processor( self::class );
|
||||
} else {
|
||||
$this->batch_processing->remove_processor( self::class );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'pre_update_option' to prevent enabling of the cleanup process when conditions aren't met.
|
||||
*
|
||||
* @param mixed $new_value New option value.
|
||||
* @param mixed $old_value Previous option value.
|
||||
*/
|
||||
private function pre_update_option( $new_value, $old_value ) {
|
||||
return $this->can_run() ? $new_value : 'no';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there are any orders in need of cleanup and cleanup can run.
|
||||
*
|
||||
@@ -310,17 +252,4 @@ class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
return ! empty( $this->get_next_batch_to_process( 1 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'shutdown' to clean up or set things straight in case of failures (timeouts, etc).
|
||||
*/
|
||||
private function maybe_reset_state() {
|
||||
$is_enqueued = $this->batch_processing->is_enqueued( self::class );
|
||||
$is_flag_set = $this->is_flag_set();
|
||||
|
||||
if ( $is_enqueued xor $is_flag_set ) {
|
||||
$this->toggle_flag( false );
|
||||
$this->batch_processing->remove_processor( self::class );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\CLIRunner;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
@@ -24,6 +25,7 @@ class ProductAttributesLookupServiceProvider extends AbstractServiceProvider {
|
||||
DataRegenerator::class,
|
||||
Filterer::class,
|
||||
LookupDataStore::class,
|
||||
CLIRunner::class,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -33,5 +35,6 @@ class ProductAttributesLookupServiceProvider extends AbstractServiceProvider {
|
||||
$this->share( DataRegenerator::class )->addArgument( LookupDataStore::class );
|
||||
$this->share( Filterer::class )->addArgument( LookupDataStore::class );
|
||||
$this->share( LookupDataStore::class );
|
||||
$this->share( CLIRunner::class )->addArguments( array( DataRegenerator::class, LookupDataStore::class ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
||||
public function __construct() {
|
||||
$this->add_group_blocks();
|
||||
$this->add_general_group_blocks();
|
||||
$this->add_pricing_group_blocks();
|
||||
$this->add_inventory_group_blocks();
|
||||
$this->add_shipping_group_blocks();
|
||||
}
|
||||
@@ -74,15 +73,6 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
||||
),
|
||||
)
|
||||
);
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['PRICING'],
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
$this->add_group(
|
||||
array(
|
||||
'id' => $this::GROUP_IDS['INVENTORY'],
|
||||
@@ -107,6 +97,8 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
||||
* Adds the general group blocks to the template.
|
||||
*/
|
||||
protected function add_general_group_blocks() {
|
||||
$is_calc_taxes_enabled = wc_tax_enabled();
|
||||
|
||||
$general_group = $this->get_group_by_id( $this::GROUP_IDS['GENERAL'] );
|
||||
$general_group->add_block(
|
||||
array(
|
||||
@@ -132,6 +124,92 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Product Pricing columns.
|
||||
$pricing_columns = $basic_details->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-group-pricing-columns',
|
||||
'blockName' => 'core/columns',
|
||||
'order' => 10,
|
||||
)
|
||||
);
|
||||
$pricing_column_1 = $pricing_columns->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-group-pricing-column-1',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_column_1->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-regular-price',
|
||||
'blockName' => 'woocommerce/product-regular-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'Regular price', 'woocommerce' ),
|
||||
'isRequired' => true,
|
||||
'help' => $is_calc_taxes_enabled ? null : sprintf(
|
||||
/* translators: %1$s: store settings link opening tag. %2$s: store settings link closing tag.*/
|
||||
__( 'Per your %1$sstore settings%2$s, taxes are not enabled.', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=general' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_column_2 = $pricing_columns->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-group-pricing-column-2',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_column_2->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-sale-price',
|
||||
'blockName' => 'woocommerce/product-sale-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
$basic_details->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-schedule-sale-fields',
|
||||
'blockName' => 'woocommerce/product-schedule-sale-fields',
|
||||
'order' => 20,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $is_calc_taxes_enabled ) {
|
||||
$basic_details->add_block(
|
||||
array(
|
||||
'id' => 'product-tax-class',
|
||||
'blockName' => 'woocommerce/product-select-field',
|
||||
'order' => 40,
|
||||
'attributes' => array(
|
||||
'label' => __( 'Tax class', 'woocommerce' ),
|
||||
'help' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'property' => 'tax_class',
|
||||
'options' => SimpleProductTemplate::get_tax_classes( 'product_variation' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$basic_details->add_block(
|
||||
array(
|
||||
'id' => 'product-variation-note',
|
||||
@@ -190,128 +268,6 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
||||
$this->add_downloadable_product_blocks( $general_group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the pricing group blocks to the template.
|
||||
*/
|
||||
protected function add_pricing_group_blocks() {
|
||||
$is_calc_taxes_enabled = wc_tax_enabled();
|
||||
|
||||
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
|
||||
$pricing_group->add_block(
|
||||
array(
|
||||
'id' => 'pricing-single-variation-notice',
|
||||
'blockName' => 'woocommerce/product-single-variation-notice',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'content' => __( '<strong>You’re editing details specific to this variation.</strong> Some information, like description and images, will be inherited from the main product, <noticeLink><parentProductName/></noticeLink>.', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'isDismissible' => true,
|
||||
'name' => $this::SINGLE_VARIATION_NOTICE_DISMISSED_OPTION,
|
||||
),
|
||||
)
|
||||
);
|
||||
// Product Pricing Section.
|
||||
$product_pricing_section = $pricing_group->add_section(
|
||||
array(
|
||||
'id' => 'product-pricing-section',
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
|
||||
__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_columns = $product_pricing_section->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-group-pricing-columns',
|
||||
'blockName' => 'core/columns',
|
||||
'order' => 10,
|
||||
)
|
||||
);
|
||||
$pricing_column_1 = $pricing_columns->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-group-pricing-column-1',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_column_1->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-regular-price',
|
||||
'blockName' => 'woocommerce/product-regular-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'Regular price', 'woocommerce' ),
|
||||
'isRequired' => true,
|
||||
'help' => $is_calc_taxes_enabled ? null : sprintf(
|
||||
/* translators: %1$s: store settings link opening tag. %2$s: store settings link closing tag.*/
|
||||
__( 'Per your %1$sstore settings%2$s, taxes are not enabled.', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=general' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_column_2 = $pricing_columns->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-group-pricing-column-2',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 20,
|
||||
'attributes' => array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
)
|
||||
);
|
||||
$pricing_column_2->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-sale-price',
|
||||
'blockName' => 'woocommerce/product-sale-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => array(
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
$product_pricing_section->add_block(
|
||||
array(
|
||||
'id' => 'product-pricing-schedule-sale-fields',
|
||||
'blockName' => 'woocommerce/product-schedule-sale-fields',
|
||||
'order' => 20,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $is_calc_taxes_enabled ) {
|
||||
$product_pricing_section->add_block(
|
||||
array(
|
||||
'id' => 'product-tax-class',
|
||||
'blockName' => 'woocommerce/product-select-field',
|
||||
'order' => 40,
|
||||
'attributes' => array(
|
||||
'label' => __( 'Tax class', 'woocommerce' ),
|
||||
'help' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'property' => 'tax_class',
|
||||
'options' => SimpleProductTemplate::get_tax_classes( 'product_variation' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the inventory group blocks to the template.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WP_CLI;
|
||||
|
||||
/**
|
||||
* Command line tools to handle the regeneration of the product aatributes lookup table.
|
||||
*/
|
||||
class CLIRunner {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* The instance of DataRegenerator to use.
|
||||
*
|
||||
* @var DataRegenerator
|
||||
*/
|
||||
private DataRegenerator $data_regenerator;
|
||||
|
||||
/**
|
||||
* The instance of DataRegenerator to use.
|
||||
*
|
||||
* @var LookupDataStore
|
||||
*/
|
||||
private LookupDataStore $lookup_data_store;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::mark_method_as_accessible( 'init' );
|
||||
}
|
||||
|
||||
// phpcs:disable WooCommerce.Functions.InternalInjectionMethod
|
||||
|
||||
/**
|
||||
* Class initialization, invoked by the DI container.
|
||||
*
|
||||
* This method is normally defined as public, we define it as private here
|
||||
* (and "publicize" it in the constructor) to prevent WP_CLI from
|
||||
* creating a dummy command for it.
|
||||
*
|
||||
* @param DataRegenerator $data_regenerator The instance of DataRegenerator to use.
|
||||
* @param LookupDataStore $lookup_data_store The instance of DataRegenerator to use.
|
||||
*/
|
||||
private function init( DataRegenerator $data_regenerator, LookupDataStore $lookup_data_store ) {
|
||||
$this->data_regenerator = $data_regenerator;
|
||||
$this->lookup_data_store = $lookup_data_store;
|
||||
}
|
||||
|
||||
// phpcs:enable WooCommerce.Functions.InternalInjectionMethod
|
||||
|
||||
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
|
||||
/**
|
||||
* Enable the usage of the product attributes lookup table.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function enable( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'enable_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "enable" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function enable_core( array $args, array $assoc_args ) {
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
if ( 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' ) ) {
|
||||
$this->warning( "The usage of the of the %W{$table_name}%n table is already enabled." );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( 'force', $assoc_args ) ) {
|
||||
$must_confirm = true;
|
||||
if ( $this->lookup_data_store->regeneration_is_in_progress() ) {
|
||||
$this->warning( "The regeneration of the %W{$table_name}%n table is currently in process." );
|
||||
} elseif ( $this->lookup_data_store->regeneration_was_aborted() ) {
|
||||
$this->warning( "The regeneration of the %W{$table_name}%n table was aborted." );
|
||||
} elseif ( 0 === $this->get_lookup_table_info()['total_rows'] ) {
|
||||
$this->warning( "The %W{$table_name}%n table is empty." );
|
||||
} else {
|
||||
$must_confirm = false;
|
||||
}
|
||||
|
||||
if ( $must_confirm ) {
|
||||
WP_CLI::confirm( 'Are you sure that you want to enable the table usage?' );
|
||||
}
|
||||
}
|
||||
|
||||
update_option( 'woocommerce_attribute_lookup_enabled', 'yes' );
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->success( "The usage of the %W{$table_name}%n table for product attribute lookup has been enabled." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the usage of the product attributes lookup table.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function disable( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'disable_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "disable" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function disable_core( array $args, array $assoc_args ) {
|
||||
if ( 'yes' !== get_option( 'woocommerce_attribute_lookup_enabled' ) ) {
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->warning( "The usage of the of the %W{$table_name}%n table is already disabled." );
|
||||
return;
|
||||
}
|
||||
update_option( 'woocommerce_attribute_lookup_enabled', 'no' );
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->success( "The usage of the %W{$table_name}%n table for product attribute lookup has been disabled." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the product attributes lookup table data for one single product.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <product-id>
|
||||
* : The id of the product for which the data will be regenerated.
|
||||
*
|
||||
* [--disable-db-optimization]
|
||||
* : Don't use optimized database access even if products are stored as custom post types.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc palt regenerate_for_product 34 --disable-db-optimization
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function regenerate_for_product( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'regenerate_for_product_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "regenerate_for_product" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function regenerate_for_product_core( array $args = array(), array $assoc_args = array() ) {
|
||||
$product_id = current( $args );
|
||||
$this->data_regenerator->check_can_do_lookup_table_regeneration( $product_id );
|
||||
$use_db_optimization = ! array_key_exists( 'disable-db-optimization', $assoc_args );
|
||||
$this->check_can_use_db_optimization( $use_db_optimization );
|
||||
$start_time = microtime( true );
|
||||
$this->lookup_data_store->create_data_for_product( $product_id, $use_db_optimization );
|
||||
|
||||
if ( $this->lookup_data_store->get_last_create_operation_failed() ) {
|
||||
$this->error( "Lookup data regeneration failed.\nSee the WooCommerce logs (source is %9palt-updates%n) for details." );
|
||||
} else {
|
||||
$total_time = microtime( true ) - $start_time;
|
||||
WP_CLI::success( sprintf( 'Attributes lookup data for product %d regenerated in %f seconds.', $product_id, $total_time ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If database access optimization is requested but can't be used, show a warning.
|
||||
*
|
||||
* @param bool $use_db_optimization True if database access optimization is requested.
|
||||
*/
|
||||
private function check_can_use_db_optimization( bool $use_db_optimization ) {
|
||||
if ( $use_db_optimization && ! $this->lookup_data_store->can_use_optimized_db_access() ) {
|
||||
$this->warning( "Optimized database access can't be used (products aren't stored as custom post types)." );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain information about the product attributes lookup table.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function info( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'info_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "info" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function info_core( array $args, array $assoc_args ) {
|
||||
global $wpdb;
|
||||
|
||||
$enabled = 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' );
|
||||
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$info = $this->get_lookup_table_info();
|
||||
|
||||
$this->log( "Table name: %W{$table_name}%n" );
|
||||
$this->log( 'Table usage is ' . ( $enabled ? '%Genabled%n' : '%Ydisabled%n' ) );
|
||||
$this->log( "The table contains %C{$info['total_rows']}%n rows corresponding to %G{$info['products_count']}%n products." );
|
||||
|
||||
if ( $info['total_rows'] > 0 ) {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$highest_product_id_in_table = $wpdb->get_var( 'select max(product_or_parent_id) from ' . $table_name );
|
||||
$this->log( "The highest product id in the table is %B{$highest_product_id_in_table}%n." );
|
||||
}
|
||||
|
||||
if ( $this->lookup_data_store->regeneration_is_in_progress() ) {
|
||||
$max_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process', '???' );
|
||||
WP_CLI::log( '' );
|
||||
$this->warning( 'Full regeneration of the table is currently %Gin progress.%n' );
|
||||
if ( ! $this->data_regenerator->has_scheduled_action_for_regeneration_step() ) {
|
||||
$this->log( 'However, there are %9NO%n actions scheduled to run the regeneration steps (a %9wp cli palt regenerate%n command was aborted?).' );
|
||||
}
|
||||
$this->log( "The last product id that will be processed is %Y{$max_product_id_to_process}%n." );
|
||||
$this->log( "\nRun %9wp cli palt abort_regeneration%n to abort the regeneration process," );
|
||||
$this->log( "then you'll be able to run %9wp cli palt resume_regeneration%n to resume the regeneration process," );
|
||||
} elseif ( $this->lookup_data_store->regeneration_was_aborted() ) {
|
||||
$max_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process', '???' );
|
||||
WP_CLI::log( '' );
|
||||
$this->warning( "Full regeneration of the table has been %Raborted.%n\nThe last product id that will be processed is %Y{$max_product_id_to_process}%n." );
|
||||
$this->log( "\nRun %9wp cli palt resume_regeneration%n to resume the regeneration process." );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the background regeneration of the product attributes lookup table that is happening in the background.
|
||||
*
|
||||
* [--cleanup]
|
||||
* : Also cleanup temporary data (so regeneration can't be resumed, but it can be restarted).
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc palt abort_regeneration --cleanup
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function abort_regeneration( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'abort_regeneration_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "abort_regeneration" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function abort_regeneration_core( array $args, array $assoc_args ) {
|
||||
$this->data_regenerator->abort_regeneration( false );
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->success( "The regeneration of the data in the %W{$table_name}%n table has been aborted." );
|
||||
if ( array_key_exists( 'cleanup', $assoc_args ) ) {
|
||||
$this->cleanup_regeneration_progress( array(), array() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the background regeneration of the product attributes lookup table after it has been aborted.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function resume_regeneration( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'resume_regeneration_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "resume_regeneration" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function resume_regeneration_core( array $args, array $assoc_args ) {
|
||||
$this->data_regenerator->resume_regeneration( false );
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->success( "The regeneration of the data in the %W{$table_name}%n table has been resumed." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the temporary data used during the regeneration of the product attributes lookup table. This data is normally deleted automatically after the regeneration process finishes.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function cleanup_regeneration_progress( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'cleanup_regeneration_progress_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "cleanup_regeneration_progress" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function cleanup_regeneration_progress_core( array $args, array $assoc_args ) {
|
||||
$this->data_regenerator->finalize_regeneration( false );
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->success( "The temporary data used for regeneration of the data in the %W{$table_name}%n table has been deleted." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the background regeneration of the product attributes lookup table. The regeneration will happen in the background, using scheduled actions.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--force]
|
||||
* : Don't prompt for confirmation if the product attributes lookup table isn't empty.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc palt initiate_regeneration --force
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function initiate_regeneration( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'initiate_regeneration_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "initiate_regeneration" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
private function initiate_regeneration_core( array $args, array $assoc_args ) {
|
||||
$this->data_regenerator->check_can_do_lookup_table_regeneration();
|
||||
$info = $this->get_lookup_table_info();
|
||||
if ( $info['total_rows'] > 0 && ! array_key_exists( 'force', $assoc_args ) ) {
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->warning( "The %W{$table_name}%n table contains %C{$info['total_rows']}%n rows corresponding to %G{$info['products_count']}%n products." );
|
||||
WP_CLI::confirm( 'Initiating the regeneration will first delete the data. Are you sure?' );
|
||||
}
|
||||
|
||||
$this->data_regenerator->initiate_regeneration();
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
$this->log( "%GSuccess:%n The regeneration of the data in the %W{$table_name}%n table has been initiated." );
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the product attributes lookup table immediately, without using scheduled tasks.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--force]
|
||||
* : Don't prompt for confirmation if the product attributes lookup table isn't empty.
|
||||
*
|
||||
* [--from-scratch]
|
||||
* : Start table regeneration from scratch even if a regeneration is already in progress.
|
||||
*
|
||||
* [--disable-db-optimization]
|
||||
* : Don't use optimized database access even if products are stored as custom post types.
|
||||
*
|
||||
* [--batch-size=<size>]
|
||||
* : How many products to process in each iteration of the loop.
|
||||
* ---
|
||||
* default: 10
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp wc palt regenerate --force --from-scratch --batch-size=20
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function regenerate( array $args = array(), array $assoc_args = array() ) {
|
||||
return $this->invoke( 'regenerate_core', $args, $assoc_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Core method for the "regenerate" command.
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
* @throws \Exception Invalid batch size argument.
|
||||
*/
|
||||
private function regenerate_core( array $args = array(), array $assoc_args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
|
||||
$batch_size = $assoc_args['batch-size'] ?? DataRegenerator::PRODUCTS_PER_GENERATION_STEP;
|
||||
if ( ! is_numeric( $batch_size ) || $batch_size < 1 ) {
|
||||
throw new \Exception( 'batch_size must be a number bigger than 0' );
|
||||
}
|
||||
|
||||
$was_enabled = 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' );
|
||||
|
||||
// phpcs:ignore Generic.Commenting.Todo.TaskFound
|
||||
// TODO: adjust for non-CPT datastores (this is only used for the progress bar, though).
|
||||
$products_count = wp_count_posts( 'product' );
|
||||
$products_count = intval( $products_count->publish ) + intval( $products_count->pending ) + intval( $products_count->draft );
|
||||
|
||||
if ( ! $this->lookup_data_store->regeneration_is_in_progress() || array_key_exists( 'from-scratch', $assoc_args ) ) {
|
||||
$info = $this->get_lookup_table_info();
|
||||
if ( $info['total_rows'] > 0 && ! array_key_exists( 'force', $assoc_args ) ) {
|
||||
$this->warning( "The %W{$table_name}%n table contains %C{$info['total_rows']}%n rows corresponding to %G{$info['products_count']}%n products." );
|
||||
WP_CLI::confirm( 'Triggering the regeneration will first delete the data. Are you sure?' );
|
||||
}
|
||||
|
||||
$this->data_regenerator->finalize_regeneration( false );
|
||||
$last_product_id = $this->data_regenerator->initiate_regeneration( false );
|
||||
if ( 0 === $last_product_id ) {
|
||||
$this->data_regenerator->finalize_regeneration( $was_enabled );
|
||||
WP_CLI::log( 'No products exist in the database, the table is left empty.' );
|
||||
return;
|
||||
}
|
||||
$processed_count = 0;
|
||||
} else {
|
||||
$last_product_id = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
|
||||
if ( false === $last_product_id ) {
|
||||
WP_CLI::error( 'Regeneration seems to be already in progress, but the woocommerce_attribute_lookup_last_product_id_to_process option isn\'t there. Try %9wp cli palt cleanup_regeneration_progress%n first." );' );
|
||||
return 1;
|
||||
}
|
||||
$processed_count = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
|
||||
$this->log( "Resuming regeneration, %C{$processed_count}%n products have been processed already" );
|
||||
$this->lookup_data_store->set_regeneration_in_progress_flag();
|
||||
}
|
||||
|
||||
$this->data_regenerator->cancel_regeneration_scheduled_action();
|
||||
|
||||
$use_db_optimization = ! array_key_exists( 'disable-db-optimization', $assoc_args );
|
||||
$this->check_can_use_db_optimization( $use_db_optimization );
|
||||
$progress = WP_CLI\Utils\make_progress_bar( '', $products_count );
|
||||
$this->log( "Regenerating %W{$table_name}%n..." );
|
||||
$progress->tick( $processed_count );
|
||||
|
||||
$regeneration_step_failed = false;
|
||||
while ( $this->data_regenerator->do_regeneration_step( $batch_size, $use_db_optimization ) ) {
|
||||
$progress->tick( $batch_size );
|
||||
$regeneration_step_failed = $regeneration_step_failed || $this->data_regenerator->get_last_regeneration_step_failed();
|
||||
}
|
||||
|
||||
$this->data_regenerator->finalize_regeneration( $was_enabled );
|
||||
$time = $progress->formatTime( $progress->elapsed() );
|
||||
$progress->finish();
|
||||
|
||||
if ( $regeneration_step_failed ) {
|
||||
$this->warning( "Lookup data regeneration failed for at least one product.\nSee the WooCommerce logs (source is %9palt-updates%n) for details.\n" );
|
||||
$this->log( "Table %W{$table_name}%n regenerated in {$time}." );
|
||||
} else {
|
||||
$this->log( "%GSuccess:%n Table %W{$table_name}%n regenerated in {$time}." );
|
||||
}
|
||||
|
||||
$info = $this->get_lookup_table_info();
|
||||
$this->log( "The table contains now %C{$info['total_rows']}%n rows corresponding to %G{$info['products_count']}%n products." );
|
||||
}
|
||||
|
||||
// phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
|
||||
/**
|
||||
* Get information about the product attributes lookup table.
|
||||
*
|
||||
* @return array Array containing the 'total_rows' and 'products_count' keys.
|
||||
*/
|
||||
private function get_lookup_table_info(): array {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $this->lookup_data_store->get_lookup_table_name();
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$info = $wpdb->get_row( 'select count(1), count(distinct(product_or_parent_id)) from ' . $table_name, ARRAY_N );
|
||||
return array(
|
||||
'total_rows' => absint( $info[0] ),
|
||||
'products_count' => absint( $info[1] ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a method from the class, and if an exception is thrown, show it using WP_CLI::error.
|
||||
*
|
||||
* @param string $method_name Name of the method to invoke.
|
||||
* @param array $args Positional arguments to pass to the method.
|
||||
* @param array $assoc_args Associative arguments to pass to the method.
|
||||
* @return mixed Result from the method, or 1 if an exception is thrown.
|
||||
*/
|
||||
private function invoke( string $method_name, array $args, array $assoc_args ) {
|
||||
try {
|
||||
return call_user_func( array( $this, $method_name ), $args, $assoc_args );
|
||||
} catch ( \Exception $e ) {
|
||||
WP_CLI::error( $e->getMessage() );
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a log message using the WP_CLI text colorization feature.
|
||||
*
|
||||
* @param string $text Text to show.
|
||||
*/
|
||||
private function log( string $text ) {
|
||||
WP_CLI::log( WP_CLI::colorize( $text ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning message using the WP_CLI text colorization feature.
|
||||
*
|
||||
* @param string $text Text to show.
|
||||
*/
|
||||
private function warning( string $text ) {
|
||||
WP_CLI::warning( WP_CLI::colorize( $text ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success message using the WP_CLI text colorization feature.
|
||||
*
|
||||
* @param string $text Text to show.
|
||||
*/
|
||||
private function success( string $text ) {
|
||||
WP_CLI::success( WP_CLI::colorize( $text ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error message using the WP_CLI text colorization feature.
|
||||
*
|
||||
* @param string $text Text to show.
|
||||
*/
|
||||
private function error( string $text ) {
|
||||
WP_CLI::error( WP_CLI::colorize( $text ) );
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class DataRegenerator {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
public const PRODUCTS_PER_GENERATION_STEP = 10;
|
||||
public const PRODUCTS_PER_GENERATION_STEP = 100;
|
||||
|
||||
/**
|
||||
* The data store to use.
|
||||
@@ -48,6 +48,13 @@ class DataRegenerator {
|
||||
*/
|
||||
private $lookup_table_name;
|
||||
|
||||
/**
|
||||
* Flag indicating if the last regeneration step failed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $last_regeneration_step_failed;
|
||||
|
||||
/**
|
||||
* DataRegenerator constructor.
|
||||
*/
|
||||
@@ -71,64 +78,96 @@ class DataRegenerator {
|
||||
$this->data_store = $data_store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last regeneration step failed.
|
||||
*
|
||||
* @return bool True if the last regeneration step failed.
|
||||
*/
|
||||
public function get_last_regeneration_step_failed() {
|
||||
return $this->last_regeneration_step_failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the regeneration procedure:
|
||||
* deletes the lookup table and related options if they exist,
|
||||
* then it creates the table and runs the first step of the regeneration process.
|
||||
*
|
||||
* This method is intended ONLY to be used as a callback for a db update in wc-update-functions,
|
||||
* regeneration triggered from the tools page will use initiate_regeneration_from_tools_page instead.
|
||||
* If $in_background is true, regeneration will continue in the background using scheduled actions.
|
||||
* If $in_background is false, do_regeneration_step and finalize_regeneration must be invoked explicitly.
|
||||
*
|
||||
* This method is intended to be used as a callback for a db update in wc-update-functions
|
||||
* and in the CLI commands, regeneration triggered from the tools page will use
|
||||
* initiate_regeneration_from_tools_page instead.
|
||||
*
|
||||
* @param bool $in_background True if regeneration will continue in the background using scheduled actions.
|
||||
* @return int Highest product id that will be processed.
|
||||
*/
|
||||
public function initiate_regeneration() {
|
||||
$this->data_store->unset_regeneration_aborted_flag();
|
||||
public function initiate_regeneration( bool $in_background = true ): int {
|
||||
$this->check_can_do_lookup_table_regeneration();
|
||||
|
||||
$this->enable_or_disable_lookup_table_usage( false );
|
||||
|
||||
$this->delete_all_attributes_lookup_data();
|
||||
$products_exist = $this->initialize_table_and_data();
|
||||
if ( $products_exist ) {
|
||||
$this->enqueue_regeneration_step_run();
|
||||
$this->delete_all_attributes_lookup_data( true );
|
||||
$last_product_id = $this->initialize_table_and_data();
|
||||
if ( $last_product_id > 0 ) {
|
||||
$this->data_store->set_regeneration_in_progress_flag();
|
||||
if ( $in_background ) {
|
||||
$this->enqueue_regeneration_step_run();
|
||||
}
|
||||
} else {
|
||||
$this->finalize_regeneration( true );
|
||||
}
|
||||
return $last_product_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the existing data related to the lookup table, including the table itself.
|
||||
* Delete all the existing data related to the lookup table, optionally including the table itself.
|
||||
*
|
||||
* @param bool $truncate_table True to truncate the lookup table too.
|
||||
*/
|
||||
private function delete_all_attributes_lookup_data() {
|
||||
private function delete_all_attributes_lookup_data( bool $truncate_table ) {
|
||||
global $wpdb;
|
||||
|
||||
delete_option( 'woocommerce_attribute_lookup_enabled' );
|
||||
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
|
||||
delete_option( 'woocommerce_attribute_lookup_processed_count' );
|
||||
$this->data_store->unset_regeneration_in_progress_flag();
|
||||
$this->data_store->unset_regeneration_aborted_flag();
|
||||
|
||||
if ( $this->data_store->check_lookup_table_exists() ) {
|
||||
$wpdb->query( "TRUNCATE TABLE {$this->lookup_table_name}" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
if ( $truncate_table && $this->data_store->check_lookup_table_exists() ) {
|
||||
$this->truncate_lookup_table();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the data from the lookup table.
|
||||
*/
|
||||
public function truncate_lookup_table() {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query( "TRUNCATE TABLE {$this->lookup_table_name}" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the lookup table and initialize the options that will be temporarily used
|
||||
* while the regeneration is in progress.
|
||||
*
|
||||
* @return bool True if there's any product at all in the database, false otherwise.
|
||||
* @return int Id of the last product id that will be processed.
|
||||
*/
|
||||
private function initialize_table_and_data() {
|
||||
private function initialize_table_and_data(): int {
|
||||
$database_util = wc_get_container()->get( DatabaseUtil::class );
|
||||
$database_util->dbdelta( $this->get_table_creation_sql() );
|
||||
|
||||
$last_existing_product_id = $this->get_last_existing_product_id();
|
||||
if ( ! $last_existing_product_id ) {
|
||||
// No products exist, nothing to (re)generate.
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->data_store->set_regeneration_in_progress_flag();
|
||||
update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', $last_existing_product_id );
|
||||
update_option( 'woocommerce_attribute_lookup_processed_count', 0 );
|
||||
|
||||
return true;
|
||||
return $last_existing_product_id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +204,7 @@ class DataRegenerator {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->do_regeneration_step();
|
||||
$result = $this->do_regeneration_step( null, $this->data_store->optimized_data_access_is_enabled() );
|
||||
if ( $result ) {
|
||||
$this->enqueue_regeneration_step_run();
|
||||
} else {
|
||||
@@ -190,16 +229,18 @@ class DataRegenerator {
|
||||
* Perform one regeneration step: grabs a chunk of products and creates
|
||||
* the appropriate entries for them in the lookup table.
|
||||
*
|
||||
* @param int|null $step_size How many products to process, by default PRODUCTS_PER_GENERATION_STEP will be used.
|
||||
* @param bool $use_optimized_db_access Use direct database access for data retrieval if possible.
|
||||
* @return bool True if more steps need to be run, false otherwise.
|
||||
*/
|
||||
private function do_regeneration_step() {
|
||||
public function do_regeneration_step( ?int $step_size = null, bool $use_optimized_db_access = false ) {
|
||||
/**
|
||||
* Filter to alter the count of products that will be processed in each step of the product attributes lookup table regeneration process.
|
||||
*
|
||||
* @since 6.3
|
||||
* @param int $count Default processing step size.
|
||||
*/
|
||||
$products_per_generation_step = apply_filters( 'woocommerce_attribute_lookup_regeneration_step_size', self::PRODUCTS_PER_GENERATION_STEP );
|
||||
$products_per_generation_step = apply_filters( 'woocommerce_attribute_lookup_regeneration_step_size', $step_size ?? self::PRODUCTS_PER_GENERATION_STEP );
|
||||
|
||||
$products_already_processed = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
|
||||
|
||||
@@ -219,8 +260,10 @@ class DataRegenerator {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->last_regeneration_step_failed = false;
|
||||
foreach ( $product_ids as $id ) {
|
||||
$this->data_store->create_data_for_product( $id );
|
||||
$this->data_store->create_data_for_product( $id, $use_optimized_db_access );
|
||||
$this->last_regeneration_step_failed = $this->last_regeneration_step_failed || $this->data_store->get_last_create_operation_failed();
|
||||
}
|
||||
|
||||
$products_already_processed += count( $product_ids );
|
||||
@@ -235,11 +278,10 @@ class DataRegenerator {
|
||||
*
|
||||
* @param bool $enable_usage Whether the table usage should be enabled or not.
|
||||
*/
|
||||
private function finalize_regeneration( bool $enable_usage ) {
|
||||
delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
|
||||
delete_option( 'woocommerce_attribute_lookup_processed_count' );
|
||||
public function finalize_regeneration( bool $enable_usage ) {
|
||||
$this->cancel_regeneration_scheduled_action();
|
||||
$this->delete_all_attributes_lookup_data( false );
|
||||
update_option( 'woocommerce_attribute_lookup_enabled', $enable_usage ? 'yes' : 'no' );
|
||||
$this->data_store->unset_regeneration_in_progress_flag();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,10 +302,9 @@ class DataRegenerator {
|
||||
'name' => __( 'Regenerate the product attributes lookup table', 'woocommerce' ),
|
||||
'desc' => __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function() {
|
||||
'callback' => function () {
|
||||
$this->initiate_regeneration_from_tools_page();
|
||||
return __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
|
||||
|
||||
},
|
||||
'selector' => array(
|
||||
'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ),
|
||||
@@ -292,8 +333,8 @@ class DataRegenerator {
|
||||
'name' => __( 'Abort the product attributes lookup table regeneration', 'woocommerce' ),
|
||||
'desc' => __( 'This tool will abort the regenerate product attributes lookup table regeneration. After this is done the process can be either started over, or resumed to continue where it stopped.', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function() {
|
||||
$this->abort_regeneration_from_tools_page();
|
||||
'callback' => function () {
|
||||
$this->abort_regeneration( true );
|
||||
return __( 'Product attributes lookup table regeneration process has been aborted.', 'woocommerce' );
|
||||
},
|
||||
'button' => __( 'Abort', 'woocommerce' ),
|
||||
@@ -310,8 +351,8 @@ class DataRegenerator {
|
||||
$processed_count
|
||||
),
|
||||
'requires_refresh' => true,
|
||||
'callback' => function() {
|
||||
$this->resume_regeneration_from_tools_page();
|
||||
'callback' => function () {
|
||||
$this->resume_regeneration( true );
|
||||
return __( 'Product attributes lookup table regeneration process has been resumed.', 'woocommerce' );
|
||||
},
|
||||
'button' => __( 'Resume', 'woocommerce' ),
|
||||
@@ -334,9 +375,8 @@ class DataRegenerator {
|
||||
if ( isset( $_REQUEST['regenerate_product_attribute_lookup_data_product_id'] ) ) {
|
||||
$product_id = (int) $_REQUEST['regenerate_product_attribute_lookup_data_product_id'];
|
||||
$this->check_can_do_lookup_table_regeneration( $product_id );
|
||||
$this->data_store->create_data_for_product( $product_id );
|
||||
$this->data_store->create_data_for_product( $product_id, $this->data_store->optimized_data_access_is_enabled() );
|
||||
} else {
|
||||
$this->check_can_do_lookup_table_regeneration();
|
||||
$this->initiate_regeneration();
|
||||
}
|
||||
//phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
@@ -363,7 +403,7 @@ class DataRegenerator {
|
||||
* @param mixed $product_id The product id to check the regeneration viability for, or null to check if a complete regeneration is possible.
|
||||
* @throws \Exception Something prevents the regeneration from starting.
|
||||
*/
|
||||
private function check_can_do_lookup_table_regeneration( $product_id = null ) {
|
||||
public function check_can_do_lookup_table_regeneration( $product_id = null ) {
|
||||
if ( $product_id && ! $this->data_store->check_lookup_table_exists() ) {
|
||||
throw new \Exception( "Can't do product attribute lookup data regeneration: lookup table doesn't exist" );
|
||||
}
|
||||
@@ -376,12 +416,15 @@ class DataRegenerator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to abort the regeneration process from the Status - Tools page.
|
||||
* Callback to abort the regeneration process from the Status - Tools page or from CLI.
|
||||
*
|
||||
* @param bool $verify_nonce True to perform nonce verification (needed when running the tool from the tools page).
|
||||
* @throws \Exception The lookup table doesn't exist, or there's no regeneration process in progress to abort.
|
||||
*/
|
||||
private function abort_regeneration_from_tools_page() {
|
||||
$this->verify_tool_execution_nonce();
|
||||
public function abort_regeneration( bool $verify_nonce ) {
|
||||
if ( $verify_nonce ) {
|
||||
$this->verify_tool_execution_nonce();
|
||||
}
|
||||
|
||||
if ( ! $this->data_store->check_lookup_table_exists() ) {
|
||||
throw new \Exception( "Can't abort the product attribute lookup data regeneration process: lookup table doesn't exist" );
|
||||
@@ -390,8 +433,7 @@ class DataRegenerator {
|
||||
throw new \Exception( "Can't abort the product attribute lookup data regeneration process since it's not currently in progress" );
|
||||
}
|
||||
|
||||
$queue = WC()->get_instance_of( \WC_Queue::class );
|
||||
$queue->cancel_all( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
|
||||
$this->cancel_regeneration_scheduled_action();
|
||||
$this->data_store->unset_regeneration_in_progress_flag();
|
||||
$this->data_store->set_regeneration_aborted_flag();
|
||||
$this->enable_or_disable_lookup_table_usage( false );
|
||||
@@ -401,12 +443,40 @@ class DataRegenerator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to resume the regeneration process from the Status - Tools page.
|
||||
*
|
||||
* @throws \Exception The lookup table doesn't exist, or a regeneration process is already in place.
|
||||
* Cancel any existing regeneration step scheduled action.
|
||||
*/
|
||||
private function resume_regeneration_from_tools_page() {
|
||||
$this->verify_tool_execution_nonce();
|
||||
public function cancel_regeneration_scheduled_action() {
|
||||
$queue = WC()->get_instance_of( \WC_Queue::class );
|
||||
$queue->cancel_all( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any pending regeneration step scheduled action exists.
|
||||
*
|
||||
* @return bool True if any pending regeneration step scheduled action exists.
|
||||
*/
|
||||
public function has_scheduled_action_for_regeneration_step(): bool {
|
||||
$queue = WC()->get_instance_of( \WC_Queue::class );
|
||||
$actions = $queue->search(
|
||||
array(
|
||||
'hook' => 'woocommerce_run_product_attribute_lookup_regeneration_callback',
|
||||
'status' => \ActionScheduler_Store::STATUS_PENDING,
|
||||
),
|
||||
'ids'
|
||||
);
|
||||
return ! empty( $actions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to resume the regeneration process from the Status - Tools page or from CLI.
|
||||
*
|
||||
* @param bool $verify_nonce True to perform nonce verification (needed when running the tool from the tools page).
|
||||
* @throws \Exception The lookup table doesn't exist, or a regeneration process is already in place or hasn't been aborted.
|
||||
*/
|
||||
public function resume_regeneration( bool $verify_nonce ) {
|
||||
if ( $verify_nonce ) {
|
||||
$this->verify_tool_execution_nonce();
|
||||
}
|
||||
|
||||
if ( ! $this->data_store->check_lookup_table_exists() ) {
|
||||
throw new \Exception( "Can't resume the product attribute lookup data regeneration process: lookup table doesn't exist" );
|
||||
@@ -414,6 +484,9 @@ class DataRegenerator {
|
||||
if ( $this->data_store->regeneration_is_in_progress() ) {
|
||||
throw new \Exception( "Can't resume the product attribute lookup data regeneration process: regeneration is already in progress" );
|
||||
}
|
||||
if ( ! $this->data_store->regeneration_was_aborted() ) {
|
||||
throw new \Exception( "Can't resume the product attribute lookup data regeneration process: no aborted regeneration process exists" );
|
||||
}
|
||||
|
||||
$this->data_store->unset_regeneration_aborted_flag();
|
||||
$this->data_store->set_regeneration_in_progress_flag();
|
||||
@@ -501,7 +574,8 @@ class DataRegenerator {
|
||||
// Otherwise (lookup table is empty but products exist) we need to initiate a regeneration if one isn't already in progress.
|
||||
if ( $this->data_store->lookup_table_has_data() || ! $this->get_last_existing_product_id() ) {
|
||||
$must_enable = get_option( 'woocommerce_attribute_lookup_enabled' ) !== 'no';
|
||||
$this->finalize_regeneration( $must_enable );
|
||||
$this->delete_all_attributes_lookup_data( false );
|
||||
update_option( 'woocommerce_attribute_lookup_enabled', $must_enable ? 'yes' : 'no' );
|
||||
} else {
|
||||
$this->initiate_regeneration();
|
||||
}
|
||||
|
||||
@@ -35,12 +35,29 @@ class LookupDataStore {
|
||||
private $lookup_table_name;
|
||||
|
||||
/**
|
||||
* LookupDataStore constructor. Makes the feature hidden by default.
|
||||
* True if the optimized database access setting is enabled AND products are stored as custom post types.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $optimized_db_access_is_enabled;
|
||||
|
||||
/**
|
||||
* Flag indicating if the last lookup table creation operation failed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $last_create_operation_failed = false;
|
||||
|
||||
/**
|
||||
* LookupDataStore constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
|
||||
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
|
||||
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
|
||||
$this->optimized_db_access_is_enabled =
|
||||
$this->can_use_optimized_db_access() &&
|
||||
'yes' === get_option( 'woocommerce_attribute_lookup_optimized_updates' );
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
@@ -55,6 +72,19 @@ class LookupDataStore {
|
||||
self::add_filter( 'woocommerce_get_settings_products', array( $this, 'add_product_attributes_lookup_table_settings' ), 100, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if optimized database access can be used when creating lookup table entries.
|
||||
*
|
||||
* @return bool True if optimized database access can be used.
|
||||
*/
|
||||
public function can_use_optimized_db_access() {
|
||||
try {
|
||||
return is_a( \WC_Data_Store::load( 'product' )->get_current_class_name(), 'WC_Product_Data_Store_CPT', true );
|
||||
} catch ( \Exception $ex ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the lookup table exists in the database.
|
||||
*
|
||||
@@ -78,6 +108,15 @@ class LookupDataStore {
|
||||
return $this->lookup_table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last lookup data creation operation failed.
|
||||
*
|
||||
* @return bool True if the last lookup data creation operation failed.
|
||||
*/
|
||||
public function get_last_create_operation_failed() {
|
||||
return $this->last_create_operation_failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert/update the appropriate lookup table entries for a new or modified product or variation.
|
||||
* This must be invoked after a product or a variation is created (including untrashing and duplication)
|
||||
@@ -96,7 +135,7 @@ class LookupDataStore {
|
||||
}
|
||||
|
||||
$action = $this->get_update_action( $changeset );
|
||||
if ( $action !== self::ACTION_NONE ) {
|
||||
if ( self::ACTION_NONE !== $action ) {
|
||||
$this->maybe_schedule_update( $product->get_id(), $action );
|
||||
}
|
||||
}
|
||||
@@ -158,7 +197,11 @@ class LookupDataStore {
|
||||
switch ( $action ) {
|
||||
case self::ACTION_INSERT:
|
||||
$this->delete_data_for( $product_id );
|
||||
$this->create_data_for( $product );
|
||||
if ( $this->optimized_db_access_is_enabled ) {
|
||||
$this->create_data_for_product_cpt( $product_id );
|
||||
} else {
|
||||
$this->create_data_for( $product );
|
||||
}
|
||||
break;
|
||||
case self::ACTION_UPDATE_STOCK:
|
||||
$this->update_stock_status_for( $product );
|
||||
@@ -192,7 +235,7 @@ class LookupDataStore {
|
||||
|
||||
if ( in_array( 'catalog_visibility', $keys, true ) ) {
|
||||
$new_visibility = $changeset['catalog_visibility'];
|
||||
if ( $new_visibility === 'visible' || $new_visibility === 'catalog' ) {
|
||||
if ( 'visible' === $new_visibility || 'catalog' === $new_visibility ) {
|
||||
return self::ACTION_INSERT;
|
||||
} else {
|
||||
return self::ACTION_DELETE;
|
||||
@@ -258,19 +301,20 @@ class LookupDataStore {
|
||||
* This method is intended to be called from the data regenerator.
|
||||
*
|
||||
* @param int|WC_Product $product Product object or id.
|
||||
* @throws \Exception A variation object is passed.
|
||||
* @param bool $use_optimized_db_access Use direct database access for data retrieval if possible.
|
||||
*/
|
||||
public function create_data_for_product( $product ) {
|
||||
if ( ! is_a( $product, \WC_Product::class ) ) {
|
||||
$product = WC()->call_function( 'wc_get_product', $product );
|
||||
}
|
||||
public function create_data_for_product( $product, $use_optimized_db_access = false ) {
|
||||
if ( $use_optimized_db_access ) {
|
||||
$product_id = intval( ( $product instanceof \WC_Product ) ? $product->get_id() : $product );
|
||||
$this->create_data_for_product_cpt( $product_id );
|
||||
} else {
|
||||
if ( ! is_a( $product, \WC_Product::class ) ) {
|
||||
$product = WC()->call_function( 'wc_get_product', $product );
|
||||
}
|
||||
|
||||
if ( $this->is_variation( $product ) ) {
|
||||
throw new \Exception( "LookupDataStore::create_data_for_product can't be called for variations." );
|
||||
$this->delete_data_for( $product->get_id() );
|
||||
$this->create_data_for( $product );
|
||||
}
|
||||
|
||||
$this->delete_data_for( $product->get_id() );
|
||||
$this->create_data_for( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,12 +323,28 @@ class LookupDataStore {
|
||||
* @param \WC_Product $product The product to create the data for.
|
||||
*/
|
||||
private function create_data_for( \WC_Product $product ) {
|
||||
if ( $this->is_variation( $product ) ) {
|
||||
$this->create_data_for_variation( $product );
|
||||
} elseif ( $this->is_variable_product( $product ) ) {
|
||||
$this->create_data_for_variable_product( $product );
|
||||
} else {
|
||||
$this->create_data_for_simple_product( $product );
|
||||
$this->last_create_operation_failed = false;
|
||||
|
||||
try {
|
||||
if ( $this->is_variation( $product ) ) {
|
||||
$this->create_data_for_variation( $product );
|
||||
} elseif ( $this->is_variable_product( $product ) ) {
|
||||
$this->create_data_for_variable_product( $product );
|
||||
} else {
|
||||
$this->create_data_for_simple_product( $product );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
$product_id = $product->get_id();
|
||||
WC()->call_function( 'wc_get_logger' )->error(
|
||||
"Lookup data creation (not optimized) failed for product $product_id: " . $e->getMessage(),
|
||||
array(
|
||||
'source' => 'palt-updates',
|
||||
'exception' => $e,
|
||||
'product_id' => $product_id,
|
||||
)
|
||||
);
|
||||
|
||||
$this->last_create_operation_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,13 +396,13 @@ class LookupDataStore {
|
||||
$product_attributes_data = $this->get_attribute_taxonomies( $product );
|
||||
$variation_attributes_data = array_filter(
|
||||
$product_attributes_data,
|
||||
function( $item ) {
|
||||
function ( $item ) {
|
||||
return $item['used_for_variations'];
|
||||
}
|
||||
);
|
||||
$non_variation_attributes_data = array_filter(
|
||||
$product_attributes_data,
|
||||
function( $item ) {
|
||||
function ( $item ) {
|
||||
return ! $item['used_for_variations'];
|
||||
}
|
||||
);
|
||||
@@ -378,7 +438,7 @@ class LookupDataStore {
|
||||
$product_attributes_data = $this->get_attribute_taxonomies( $main_product );
|
||||
$variation_attributes_data = array_filter(
|
||||
$product_attributes_data,
|
||||
function( $item ) {
|
||||
function ( $item ) {
|
||||
return $item['used_for_variations'];
|
||||
}
|
||||
);
|
||||
@@ -471,7 +531,7 @@ class LookupDataStore {
|
||||
private function get_variations_of( \WC_Product_Variable $product ) {
|
||||
$variation_ids = $product->get_children();
|
||||
return array_map(
|
||||
function( $id ) {
|
||||
function ( $id ) {
|
||||
return WC()->call_function( 'wc_get_product', $id );
|
||||
},
|
||||
$variation_ids
|
||||
@@ -662,7 +722,7 @@ class LookupDataStore {
|
||||
* @return array New settings configuration array.
|
||||
*/
|
||||
private function add_product_attributes_lookup_table_settings( array $settings, string $section_id ): array {
|
||||
if ( $section_id === 'advanced' && $this->check_lookup_table_exists() ) {
|
||||
if ( 'advanced' === $section_id && $this->check_lookup_table_exists() ) {
|
||||
$title_item = array(
|
||||
'title' => __( 'Product attributes lookup table', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
@@ -703,6 +763,16 @@ class LookupDataStore {
|
||||
'type' => 'checkbox',
|
||||
'checkboxgroup' => 'start',
|
||||
);
|
||||
|
||||
$settings[] = array(
|
||||
'title' => __( 'Optimized updates', 'woocommerce' ),
|
||||
'desc' => __( 'Uses much more performant queries to update the lookup table, but may not be compatible with some extensions.', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This setting only works when product data is stored in the posts table.', 'woocommerce' ),
|
||||
'id' => 'woocommerce_attribute_lookup_optimized_updates',
|
||||
'default' => 'no',
|
||||
'type' => 'checkbox',
|
||||
'checkboxgroup' => 'start',
|
||||
);
|
||||
}
|
||||
|
||||
$settings[] = array( 'type' => 'sectionend' );
|
||||
@@ -710,4 +780,292 @@ class LookupDataStore {
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the optimized database access setting is enabled.
|
||||
*
|
||||
* @return bool True if the optimized database access setting is enabled.
|
||||
*/
|
||||
public function optimized_data_access_is_enabled() {
|
||||
return 'yes' === get_option( 'woocommerce_attribute_lookup_optimized_updates' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the lookup table data for a product or variation using optimized database access.
|
||||
* For variable products entries are created for the main product and for all the variations.
|
||||
*
|
||||
* @param int $product_id Product or variation id.
|
||||
*/
|
||||
private function create_data_for_product_cpt( int $product_id ) {
|
||||
$this->last_create_operation_failed = false;
|
||||
|
||||
try {
|
||||
$this->create_data_for_product_cpt_core( $product_id );
|
||||
} catch ( \Exception $e ) {
|
||||
$data = array(
|
||||
'source' => 'palt-updates',
|
||||
'product_id' => $product_id,
|
||||
);
|
||||
|
||||
if ( $e instanceof \WC_Data_Exception ) {
|
||||
$data = array_merge( $data, $e->getErrorData() );
|
||||
} else {
|
||||
$data['exception'] = $e;
|
||||
}
|
||||
|
||||
WC()->call_function( 'wc_get_logger' )
|
||||
->error( "Lookup data creation (optimized) failed for product $product_id: " . $e->getMessage(), $data );
|
||||
|
||||
$this->last_create_operation_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core version of create_data_for_product_cpt (doesn't catch exceptions).
|
||||
*
|
||||
* @param int $product_id Product or variation id.
|
||||
* @return void
|
||||
* @throws \WC_Data_Exception Wrongly serialized attribute data found, or INSERT statement failed.
|
||||
*/
|
||||
private function create_data_for_product_cpt_core( int $product_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL
|
||||
$sql = $wpdb->prepare(
|
||||
"delete from {$this->lookup_table_name} where product_or_parent_id=%d",
|
||||
$product_id
|
||||
);
|
||||
$wpdb->query( $sql );
|
||||
// phpcs:enable WordPress.DB.PreparedSQL
|
||||
|
||||
// * Obtain list of product variations, together with stock statuses; also get the product type.
|
||||
// For a variation this will return just one entry, with type 'variation'.
|
||||
// Output: $product_ids_with_stock_status = associative array where 'id' is the key and values are the stock status (1 for "in stock", 0 otherwise).
|
||||
// $variation_ids = raw list of variation ids.
|
||||
// $is_variable_product = true or false.
|
||||
// $is_variation = true or false.
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"(select p.ID as id, null parent, m.meta_value as stock_status, t.name as product_type from {$wpdb->posts} p
|
||||
left join {$wpdb->postmeta} m on p.id=m.post_id and m.meta_key='_stock_status'
|
||||
left join {$wpdb->term_relationships} tr on tr.object_id=p.id
|
||||
left join {$wpdb->term_taxonomy} tt on tt.term_taxonomy_id=tr.term_taxonomy_id
|
||||
left join {$wpdb->terms} t on t.term_id=tt.term_id
|
||||
where p.post_type = 'product'
|
||||
and p.post_status in ('publish', 'draft', 'pending', 'private')
|
||||
and tt.taxonomy='product_type'
|
||||
and t.name != 'exclude-from-search'
|
||||
and p.id=%d
|
||||
limit 1)
|
||||
union
|
||||
(select p.ID as id, p.post_parent as parent, m.meta_value as stock_status, 'variation' as product_type from {$wpdb->posts} p
|
||||
left join {$wpdb->postmeta} m on p.id=m.post_id and m.meta_key='_stock_status'
|
||||
where p.post_type = 'product_variation'
|
||||
and p.post_status in ('publish', 'draft', 'pending', 'private')
|
||||
and (p.ID=%d or p.post_parent=%d));
|
||||
",
|
||||
$product_id,
|
||||
$product_id,
|
||||
$product_id
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$product_ids_with_stock_status = $wpdb->get_results( $sql, ARRAY_A );
|
||||
|
||||
$main_product_row = array_filter( $product_ids_with_stock_status, fn( $item ) => 'variation' !== $item['product_type'] );
|
||||
$is_variation = empty( $main_product_row );
|
||||
|
||||
$main_product_id =
|
||||
$is_variation ?
|
||||
current( $product_ids_with_stock_status )['parent'] :
|
||||
$product_id;
|
||||
|
||||
$is_variable_product = ! $is_variation && ( 'variable' === current( $main_product_row )['product_type'] );
|
||||
|
||||
$product_ids_with_stock_status = ArrayUtil::group_by_column( $product_ids_with_stock_status, 'id', true );
|
||||
$variation_ids = $is_variation ? array( $product_id ) : array_keys( array_diff_key( $product_ids_with_stock_status, array( $product_id => null ) ) );
|
||||
$product_ids_with_stock_status = ArrayUtil::select( $product_ids_with_stock_status, 'stock_status' );
|
||||
|
||||
$product_ids_with_stock_status = array_map( fn( $item ) => 'instock' === $item ? 1 : 0, $product_ids_with_stock_status );
|
||||
|
||||
// * Obtain the list of attributes used for variations and not.
|
||||
// Output: two lists of attribute slugs, all starting with 'pa_'.
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"select meta_value from {$wpdb->postmeta} where post_id=%d and meta_key=%s",
|
||||
$main_product_id,
|
||||
'_product_attributes'
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$temp = $wpdb->get_var( $sql );
|
||||
|
||||
if ( is_null( $temp ) ) {
|
||||
// The product has no attributes, thus there's no attributes lookup data to generate.
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
|
||||
$temp = unserialize( $temp );
|
||||
if ( false === $temp ) {
|
||||
throw new \WC_Data_Exception( 0, 'The product attributes metadata row is not properly serialized' );
|
||||
}
|
||||
|
||||
$temp = array_filter( $temp, fn( $item, $slug ) => StringUtil::starts_with( $slug, 'pa_' ) && '' === $item['value'], ARRAY_FILTER_USE_BOTH );
|
||||
|
||||
$attributes_not_for_variations =
|
||||
$is_variation || $is_variable_product ?
|
||||
array_keys( array_filter( $temp, fn( $item ) => 0 === $item['is_variation'] ) ) :
|
||||
array_keys( $temp );
|
||||
|
||||
// * Obtain the terms used for each attribute.
|
||||
// Output: $terms_used_per_attribute =
|
||||
// [
|
||||
// 'pa_...' => [
|
||||
// [
|
||||
// 'term_id' => <term id>,
|
||||
// 'attribute' => 'pa_...'
|
||||
// 'slug' => <term slug>
|
||||
// ],...
|
||||
// ],...
|
||||
// ]
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"select tt.term_id, tt.taxonomy as attribute, t.slug from {$wpdb->prefix}term_relationships tr
|
||||
join {$wpdb->term_taxonomy} tt on tt.term_taxonomy_id = tr.term_taxonomy_id
|
||||
join {$wpdb->terms} t on t.term_id=tt.term_id
|
||||
where tr.object_id=%d and taxonomy like %s;",
|
||||
$main_product_id,
|
||||
'pa_%'
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$terms_used_per_attribute = $wpdb->get_results( $sql, ARRAY_A );
|
||||
foreach ( $terms_used_per_attribute as &$term ) {
|
||||
$term['attribute'] = strtolower( rawurlencode( $term['attribute'] ) );
|
||||
}
|
||||
$terms_used_per_attribute = ArrayUtil::group_by_column( $terms_used_per_attribute, 'attribute' );
|
||||
|
||||
// * Obtain the actual variations defined (only if variations exist).
|
||||
// Output: $variations_defined =
|
||||
// [
|
||||
// <variation id> => [
|
||||
// [
|
||||
// 'variation_id' => <variation id>,
|
||||
// 'attribute' => 'pa_...'
|
||||
// 'slug' => <term slug>
|
||||
// ],...
|
||||
// ],...
|
||||
// ]
|
||||
//
|
||||
// Note that this does NOT include "any..." attributes!
|
||||
|
||||
if ( ! $is_variation && ( ! $is_variable_product || empty( $variation_ids ) ) ) {
|
||||
$variations_defined = array();
|
||||
} else {
|
||||
$sql = $wpdb->prepare(
|
||||
"select post_id as variation_id, substr(meta_key,11) as attribute, meta_value as slug from {$wpdb->postmeta}
|
||||
where post_id in (select ID from {$wpdb->posts} where (id=%d or post_parent=%d) and post_type = 'product_variation')
|
||||
and meta_key like %s
|
||||
and meta_value != ''",
|
||||
$product_id,
|
||||
$product_id,
|
||||
'attribute_pa_%'
|
||||
);
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$variations_defined = $wpdb->get_results( $sql, ARRAY_A );
|
||||
$variations_defined = ArrayUtil::group_by_column( $variations_defined, 'variation_id' );
|
||||
}
|
||||
|
||||
// Now we'll fill an array with all the data rows to be inserted in the lookup table.
|
||||
|
||||
$insert_data = array();
|
||||
|
||||
// * Insert data for the main product
|
||||
|
||||
if ( ! $is_variation ) {
|
||||
foreach ( $attributes_not_for_variations as $attribute_name ) {
|
||||
foreach ( ( $terms_used_per_attribute[ $attribute_name ] ?? array() ) as $attribute_data ) {
|
||||
$insert_data[] = array( $product_id, $main_product_id, $attribute_name, $attribute_data['term_id'], 0, $product_ids_with_stock_status[ $product_id ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Insert data for the variations defined
|
||||
|
||||
// Remove the non-variation attributes data first.
|
||||
$terms_used_per_attribute = array_diff_key( $terms_used_per_attribute, array_flip( $attributes_not_for_variations ) );
|
||||
|
||||
$used_attributes_per_variation = array();
|
||||
foreach ( $variations_defined as $variation_id => $variation_data ) {
|
||||
$used_attributes_per_variation[ $variation_id ] = array();
|
||||
foreach ( $variation_data as $variation_attribute_data ) {
|
||||
$attribute_name = $variation_attribute_data['attribute'];
|
||||
$used_attributes_per_variation[ $variation_id ][] = $attribute_name;
|
||||
$term_id = current( array_filter( ( $terms_used_per_attribute[ $attribute_name ] ?? array() ), fn( $item ) => $item['slug'] === $variation_attribute_data['slug'] ) )['term_id'] ?? null;
|
||||
if ( is_null( $term_id ) ) {
|
||||
continue;
|
||||
}
|
||||
$insert_data[] = array( $variation_id, $main_product_id, $attribute_name, $term_id, 1, $product_ids_with_stock_status[ $variation_id ] ?? false );
|
||||
}
|
||||
}
|
||||
|
||||
// * Insert data for variations that have "any..." attributes and at least one defined attribute
|
||||
|
||||
foreach ( $used_attributes_per_variation as $variation_id => $attributes_list ) {
|
||||
$any_attributes = array_diff_key( $terms_used_per_attribute, array_flip( $attributes_list ) );
|
||||
foreach ( $any_attributes as $attributes_data ) {
|
||||
foreach ( $attributes_data as $attribute_data ) {
|
||||
$insert_data[] = array( $variation_id, $main_product_id, $attribute_data['attribute'], $attribute_data['term_id'], 1, $product_ids_with_stock_status[ $variation_id ] ?? false );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Insert data for variations that have all their attributes defined as "any..."
|
||||
|
||||
$variations_with_all_any = array_keys( array_diff_key( array_flip( $variation_ids ), $used_attributes_per_variation ) );
|
||||
foreach ( $variations_with_all_any as $variation_id ) {
|
||||
foreach ( $terms_used_per_attribute as $attribute_name => $attribute_terms ) {
|
||||
foreach ( $attribute_terms as $attribute_term ) {
|
||||
$insert_data[] = array( $variation_id, $main_product_id, $attribute_name, $attribute_term['term_id'], 1, $product_ids_with_stock_status[ $variation_id ] ?? false );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * We have all the data to insert, let's go and insert it.
|
||||
|
||||
$insert_data_chunks = array_chunk( $insert_data, 100 );
|
||||
foreach ( $insert_data_chunks as $insert_data_chunk ) {
|
||||
$sql = 'INSERT INTO ' . $this->lookup_table_name . ' (
|
||||
product_id,
|
||||
product_or_parent_id,
|
||||
taxonomy,
|
||||
term_id,
|
||||
is_variation_attribute,
|
||||
in_stock)
|
||||
VALUES (';
|
||||
|
||||
$values_strings = array();
|
||||
foreach ( $insert_data_chunk as $dataset ) {
|
||||
$attribute_name = esc_sql( $dataset[2] );
|
||||
$values_strings[] = "{$dataset[0]},{$dataset[1]},'{$attribute_name}',{$dataset[3]},{$dataset[4]},{$dataset[5]}";
|
||||
}
|
||||
|
||||
$sql .= implode( '),(', $values_strings ) . ')';
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$result = $wpdb->query( $sql );
|
||||
if ( false === $result ) {
|
||||
throw new \WC_Data_Exception(
|
||||
0,
|
||||
'INSERT statement failed',
|
||||
0,
|
||||
array(
|
||||
'db_error' => esc_html( $wpdb->last_error ),
|
||||
'db_query' => esc_html( $wpdb->last_query ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ class LegacyRestApiStub {
|
||||
private static function parse_legacy_rest_api_request() {
|
||||
global $wp;
|
||||
|
||||
// The WC_API class existing means that the Legacy REST API extension is installed and active.
|
||||
if ( class_exists( 'WC_API' ) ) {
|
||||
// The WC_Legacy_REST_API_Plugin class existence means that the Legacy REST API extension is installed and active.
|
||||
if ( class_exists( 'WC_Legacy_REST_API_Plugin' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PTKClient;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
|
||||
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
|
||||
/**
|
||||
* Patterns class.
|
||||
*/
|
||||
class Patterns extends AbstractRoute {
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'patterns';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'patterns';
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return self::get_path_regex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of this rest route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_path_regex() {
|
||||
return '/patterns';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
],
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single pattern from the PTK to ensure the API is available.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_Error|\WP_HTTP_Response|WP_REST_Response
|
||||
* @throws RouteException If the patterns cannot be fetched.
|
||||
*/
|
||||
protected function get_route_response( WP_REST_Request $request ) {
|
||||
$ptk_client = Package::container()->get( PTKClient::class );
|
||||
|
||||
$response = $ptk_client->fetch_patterns(
|
||||
array(
|
||||
'per_page' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new RouteException(
|
||||
wp_kses( $response->get_error_message(), array() ),
|
||||
wp_kses( $response->get_error_code(), array() )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the patterns from the PTK and update the transient.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
* @throws Exception If the patterns cannot be fetched.
|
||||
*/
|
||||
protected function get_route_post_response( WP_REST_Request $request ) {
|
||||
$ptk_patterns_store = Package::container()->get( PTKPatternsStore::class );
|
||||
|
||||
$ptk_patterns_store->fetch_patterns();
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ class RoutesController {
|
||||
Routes\V1\AI\Products::IDENTIFIER => Routes\V1\AI\Products::class,
|
||||
Routes\V1\AI\BusinessDescription::IDENTIFIER => Routes\V1\AI\BusinessDescription::class,
|
||||
Routes\V1\AI\StoreInfo::IDENTIFIER => Routes\V1\AI\StoreInfo::class,
|
||||
Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class SchemaController {
|
||||
Schemas\V1\AI\ProductsSchema::IDENTIFIER => Schemas\V1\AI\ProductsSchema::class,
|
||||
Schemas\V1\AI\BusinessDescriptionSchema::IDENTIFIER => Schemas\V1\AI\BusinessDescriptionSchema::class,
|
||||
Schemas\V1\AI\StoreInfoSchema::IDENTIFIER => Schemas\V1\AI\StoreInfoSchema::class,
|
||||
Schemas\V1\PatternsSchema::IDENTIFIER => Schemas\V1\PatternsSchema::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
|
||||
|
||||
/**
|
||||
* OrderSchema class.
|
||||
*/
|
||||
class PatternsSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'patterns';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'patterns';
|
||||
|
||||
/**
|
||||
* Patterns schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Patterns response.
|
||||
*
|
||||
* @param array $item Item to get response for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
return [
|
||||
'success' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Utilities;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
|
||||
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
|
||||
@@ -85,7 +86,21 @@ trait CheckoutTrait {
|
||||
throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woocommerce' ), 500 );
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', esc_html( $e->getMessage() ), 400 );
|
||||
$additional_data = [];
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/**
|
||||
* Allows to check if WP_DEBUG mode is enabled before returning previous Exception.
|
||||
*
|
||||
* @param bool The WP_DEBUG mode.
|
||||
*/
|
||||
if ( apply_filters( 'woocommerce_return_previous_exceptions', Constants::is_true( 'WP_DEBUG' ) ) && $e->getPrevious() ) {
|
||||
$additional_data = [
|
||||
'previous' => get_class( $e->getPrevious() ),
|
||||
];
|
||||
}
|
||||
|
||||
throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', esc_html( $e->getMessage() ), 400, array_map( 'esc_attr', $additional_data ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,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 );
|
||||
|
||||
@@ -35,32 +35,32 @@ class ArrayUtil {
|
||||
*
|
||||
* E.g. for [ 'foo' => [ 'bar' => [ 'fizz' => 'buzz' ] ] ] the value for key 'foo::bar::fizz' would be 'buzz'.
|
||||
*
|
||||
* @param array $array The array to get the value from.
|
||||
* @param array $items The array to get the value from.
|
||||
* @param string $key The complete key hierarchy, using '::' as separator.
|
||||
* @param mixed $default The value to return if the key doesn't exist in the array.
|
||||
* @param mixed $default_value The value to return if the key doesn't exist in the array.
|
||||
*
|
||||
* @return mixed The retrieved value, or the supplied default value.
|
||||
* @throws \Exception $array is not an array.
|
||||
*/
|
||||
public static function get_nested_value( array $array, string $key, $default = null ) {
|
||||
public static function get_nested_value( array $items, string $key, $default_value = null ) {
|
||||
$key_stack = explode( '::', $key );
|
||||
$subkey = array_shift( $key_stack );
|
||||
|
||||
if ( isset( $array[ $subkey ] ) ) {
|
||||
$value = $array[ $subkey ];
|
||||
if ( isset( $items[ $subkey ] ) ) {
|
||||
$value = $items[ $subkey ];
|
||||
|
||||
if ( count( $key_stack ) ) {
|
||||
foreach ( $key_stack as $subkey ) {
|
||||
if ( is_array( $value ) && isset( $value[ $subkey ] ) ) {
|
||||
$value = $value[ $subkey ];
|
||||
} else {
|
||||
$value = $default;
|
||||
$value = $default_value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$value = $default;
|
||||
$value = $default_value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
@@ -69,12 +69,12 @@ class ArrayUtil {
|
||||
/**
|
||||
* Checks if a given key exists in an array and its value can be evaluated as 'true'.
|
||||
*
|
||||
* @param array $array The array to check.
|
||||
* @param array $items The array to check.
|
||||
* @param string $key The key for the value to check.
|
||||
* @return bool True if the key exists in the array and the value can be evaluated as 'true'.
|
||||
*/
|
||||
public static function is_truthy( array $array, string $key ) {
|
||||
return isset( $array[ $key ] ) && $array[ $key ];
|
||||
public static function is_truthy( array $items, string $key ) {
|
||||
return isset( $items[ $key ] ) && $items[ $key ];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,13 +87,13 @@ class ArrayUtil {
|
||||
* $array['key'] ?? 'default' => 'default'
|
||||
* ArrayUtil::get_value_or_default($array, 'key', 'default') => null
|
||||
*
|
||||
* @param array $array The array to get the value from.
|
||||
* @param array $items The array to get the value from.
|
||||
* @param string $key The key to use to retrieve the value.
|
||||
* @param null $default The default value to return if the key doesn't exist in the array.
|
||||
* @param null $default_value The default value to return if the key doesn't exist in the array.
|
||||
* @return mixed|null The value for the key, or the default value passed.
|
||||
*/
|
||||
public static function get_value_or_default( array $array, string $key, $default = null ) {
|
||||
return array_key_exists( $key, $array ) ? $array[ $key ] : $default;
|
||||
public static function get_value_or_default( array $items, string $key, $default_value = null ) {
|
||||
return array_key_exists( $key, $items ) ? $items[ $key ] : $default_value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,19 +149,19 @@ class ArrayUtil {
|
||||
*/
|
||||
private static function get_selector_callback( string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): \Closure {
|
||||
if ( self::SELECT_BY_OBJECT_METHOD === $selector_type ) {
|
||||
$callback = function( $item ) use ( $selector_name ) {
|
||||
$callback = function ( $item ) use ( $selector_name ) {
|
||||
return $item->$selector_name();
|
||||
};
|
||||
} elseif ( self::SELECT_BY_OBJECT_PROPERTY === $selector_type ) {
|
||||
$callback = function( $item ) use ( $selector_name ) {
|
||||
$callback = function ( $item ) use ( $selector_name ) {
|
||||
return $item->$selector_name;
|
||||
};
|
||||
} elseif ( self::SELECT_BY_ARRAY_KEY === $selector_type ) {
|
||||
$callback = function( $item ) use ( $selector_name ) {
|
||||
$callback = function ( $item ) use ( $selector_name ) {
|
||||
return $item[ $selector_name ];
|
||||
};
|
||||
} else {
|
||||
$callback = function( $item ) use ( $selector_name ) {
|
||||
$callback = function ( $item ) use ( $selector_name ) {
|
||||
if ( is_array( $item ) ) {
|
||||
return $item[ $selector_name ];
|
||||
} elseif ( method_exists( $item, $selector_name ) ) {
|
||||
@@ -269,14 +269,12 @@ class ArrayUtil {
|
||||
}
|
||||
$diff[ $key ] = $value;
|
||||
}
|
||||
} else {
|
||||
// phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- Intentional when $strict is false.
|
||||
if ( ! array_key_exists( $key, $array2 ) || $value != $array2[ $key ] ) {
|
||||
if ( $compare ) {
|
||||
return true;
|
||||
}
|
||||
$diff[ $key ] = $value;
|
||||
// phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Intentional when $strict is false.
|
||||
} elseif ( ! array_key_exists( $key, $array2 ) || $value != $array2[ $key ] ) {
|
||||
if ( $compare ) {
|
||||
return true;
|
||||
}
|
||||
$diff[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,40 +284,66 @@ class ArrayUtil {
|
||||
/**
|
||||
* Push a value to an array, but only if the value isn't in the array already.
|
||||
*
|
||||
* @param array $array The array.
|
||||
* @param array $items The array.
|
||||
* @param mixed $value The value to maybe push.
|
||||
* @return bool True if the value has been added to the array, false if the value was already in the array.
|
||||
*/
|
||||
public static function push_once( array &$array, $value ) : bool {
|
||||
if ( in_array( $value, $array, true ) ) {
|
||||
public static function push_once( array &$items, $value ): bool {
|
||||
if ( in_array( $value, $items, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$array[] = $value;
|
||||
$items[] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an associative array has a given key, and if not, set the key to an empty array.
|
||||
*
|
||||
* @param array $array The array to check.
|
||||
* @param array $items The array to check.
|
||||
* @param string $key The key to check.
|
||||
* @param bool $throw_if_existing_is_not_array If true, an exception will be thrown if the key already exists in the array but the value is not an array.
|
||||
* @return bool True if the key has been added to the array, false if not (the key already existed).
|
||||
* @throws \Exception The key already exists in the array but the value is not an array.
|
||||
*/
|
||||
public static function ensure_key_is_array( array &$array, string $key, bool $throw_if_existing_is_not_array = false ): bool {
|
||||
if ( ! isset( $array[ $key ] ) ) {
|
||||
$array[ $key ] = array();
|
||||
public static function ensure_key_is_array( array &$items, string $key, bool $throw_if_existing_is_not_array = false ): bool {
|
||||
if ( ! isset( $items[ $key ] ) ) {
|
||||
$items[ $key ] = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $throw_if_existing_is_not_array && ! is_array( $array[ $key ] ) ) {
|
||||
$type = is_object( $array[ $key ] ) ? get_class( $array[ $key ] ) : gettype( $array[ $key ] );
|
||||
if ( $throw_if_existing_is_not_array && ! is_array( $items[ $key ] ) ) {
|
||||
$type = is_object( $items[ $key ] ) ? get_class( $items[ $key ] ) : gettype( $items[ $key ] );
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw new \Exception( "Array key exists but it's not an array, it's a {$type}" );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of associative arrays, all having a shared key name ("column"), generates a new array in which
|
||||
* keys are the distinct column values found, and values are arrays with all the matches found
|
||||
* (or only the last matching array found, if $single_values is true).
|
||||
* See ArrayUtilTest for examples.
|
||||
*
|
||||
* @param array $items The array to process.
|
||||
* @param string $column The name of the key to group by.
|
||||
* @param bool $single_values True to only return the last suitable array found for each column value.
|
||||
* @return array The grouped array.
|
||||
*/
|
||||
public static function group_by_column( array $items, string $column, bool $single_values = false ): array {
|
||||
if ( $single_values ) {
|
||||
return array_combine( array_column( $items, $column ), array_values( $items ) );
|
||||
}
|
||||
|
||||
$distinct_column_values = array_unique( array_column( $items, $column ), SORT_REGULAR );
|
||||
$result = array_fill_keys( $distinct_column_values, array() );
|
||||
|
||||
foreach ( $items as $value ) {
|
||||
$result[ $value[ $column ] ][] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user