plugin updates

This commit is contained in:
Tony Volpe
2024-07-16 13:57:46 +00:00
parent 41f50eacc4
commit 8f93917880
1529 changed files with 259452 additions and 25451 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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
);
}
}

View File

@@ -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>';
}
}

View File

@@ -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
) {

View File

@@ -13,7 +13,7 @@ class ProductFilters extends AbstractBlock {
protected $block_name = 'product-filters';
/**
* Register the context
* Register the context.
*
* @return string[]
*/

View File

@@ -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;
}
}

View File

@@ -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',
)
);
}

View File

@@ -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 );
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
*/

View File

@@ -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.

View File

@@ -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.
*

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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' );
}
}

View File

@@ -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;
}
}

View File

@@ -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 ) );
}
}

View File

@@ -316,6 +316,7 @@ class BlockTemplateUtils {
$wp_template_part_filenames = array(
'checkout-header.html',
'mini-cart.html',
'product-filters-overlay.html',
);
/*

View File

@@ -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.
*