PluginProbe
WooCommerce / 11.0.1
WooCommerce v11.0.1
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / src / Blocks / BlockTypesController.php
BlockTypesController.php
797 lines 25.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare(strict_types=1);
3
4 namespace Automattic\WooCommerce\Blocks;
5
6 use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
7 use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
8 use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
9 use Automattic\WooCommerce\Blocks\BlockTypes\Cart;
10 use Automattic\WooCommerce\Blocks\BlockTypes\Checkout;
11 use Automattic\WooCommerce\Blocks\BlockTypes\MiniCartContents;
12 use Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController;
13
14 /**
15 * BlockTypesController class.
16 *
17 * @since 5.0.0
18 * @internal
19 */
20 final class BlockTypesController {
21
22 /**
23 * Instance of the asset API.
24 *
25 * @var AssetApi
26 */
27 protected $asset_api;
28
29 /**
30 * Instance of the asset data registry.
31 *
32 * @var AssetDataRegistry
33 */
34 protected $asset_data_registry;
35
36 /**
37 * Holds the registered blocks that have WooCommerce blocks as their parents.
38 *
39 * @var array List of registered blocks.
40 */
41 private $registered_blocks_with_woocommerce_parents;
42
43 /**
44 * Constructor.
45 *
46 * @param AssetApi $asset_api Instance of the asset API.
47 * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
48 */
49 public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
50 $this->asset_api = $asset_api;
51 $this->asset_data_registry = $asset_data_registry;
52 $this->init();
53 }
54
55 /**
56 * Initialize class features.
57 */
58 protected function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingPublic
59 add_action( 'init', array( $this, 'register_blocks' ) );
60 add_action( 'wp_loaded', array( $this, 'register_block_patterns' ) );
61 add_filter( 'block_categories_all', array( $this, 'register_block_categories' ), 10, 2 );
62 add_filter( 'render_block', array( $this, 'add_data_attributes' ), 10, 2 );
63 add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
64 add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
65 add_filter( 'register_block_type_args', array( $this, 'enqueue_block_style_for_classic_themes' ), 10, 2 );
66 add_filter( 'block_core_breadcrumbs_post_type_settings', array( $this, 'set_product_breadcrumbs_preferred_taxonomy' ), 10, 3 );
67 add_filter( 'block_core_breadcrumbs_items', array( $this, 'apply_woocommerce_breadcrumb_filters' ), 10, 1 );
68 }
69
70 /**
71 * Get registered blocks that have WooCommerce blocks as their parents. Adds the value to the
72 * `registered_blocks_with_woocommerce_parents` cache if `init` has been fired.
73 *
74 * @return array Registered blocks with WooCommerce blocks as parents.
75 */
76 public function get_registered_blocks_with_woocommerce_parent() {
77 // If init has run and the cache is already set, return it.
78 if ( did_action( 'init' ) && ! empty( $this->registered_blocks_with_woocommerce_parents ) ) {
79 return $this->registered_blocks_with_woocommerce_parents;
80 }
81
82 $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
83
84 if ( ! is_array( $registered_blocks ) ) {
85 return array();
86 }
87
88 $this->registered_blocks_with_woocommerce_parents = array_filter(
89 $registered_blocks,
90 function ( $block ) {
91 if ( empty( $block->parent ) ) {
92 return false;
93 }
94 if ( ! is_array( $block->parent ) ) {
95 $block->parent = array( $block->parent );
96 }
97 $woocommerce_blocks = array_filter(
98 $block->parent,
99 function ( $parent_block_name ) {
100 return 'woocommerce' === strtok( $parent_block_name, '/' );
101 }
102 );
103 return ! empty( $woocommerce_blocks );
104 }
105 );
106 return $this->registered_blocks_with_woocommerce_parents;
107 }
108
109 /**
110 * Register blocks, hooking up assets and render functions as needed.
111 */
112 public function register_blocks() {
113 $this->register_block_metadata();
114 $block_types = $this->get_block_types();
115
116 foreach ( $block_types as $block_type ) {
117 $block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
118
119 new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
120 }
121 }
122
123 /**
124 * Register block metadata collections for WooCommerce blocks.
125 *
126 * This method handles the registration of block metadata by using WordPress's block metadata
127 * collection registration system. It includes a temporary workaround for WordPress 6.7's
128 * strict path validation that might fail for sites using symlinked plugins.
129 *
130 * If the registration fails due to path validation, blocks will fall back to regular
131 * registration without affecting functionality.
132 */
133 public function register_block_metadata() {
134 $meta_file_path = WC_ABSPATH . 'assets/client/blocks/blocks-json.php';
135 if ( function_exists( 'wp_register_block_metadata_collection' ) && file_exists( $meta_file_path ) ) {
136 add_filter( 'doing_it_wrong_trigger_error', array( __CLASS__, 'bypass_block_metadata_doing_it_wrong' ), 10, 4 );
137 wp_register_block_metadata_collection(
138 WC_ABSPATH . 'assets/client/blocks/',
139 $meta_file_path
140 );
141 remove_filter( 'doing_it_wrong_trigger_error', array( __CLASS__, 'bypass_block_metadata_doing_it_wrong' ), 10 );
142 }
143 }
144
145 /**
146 * Temporarily bypasses _doing_it_wrong() notices for block metadata collection registration.
147 *
148 * WordPress 6.7 introduced block metadata collections (with strict path validation).
149 * Any sites using symlinks for plugins will fail the validation which causes the metadata
150 * collection to not be registered. However, the blocks will still fall back to the regular
151 * registration and no functionality is affected.
152 * While this validation is being discussed in WordPress Core (#62140),
153 * this method allows registration to proceed by temporarily disabling
154 * the relevant notice.
155 *
156 * @param bool $trigger Whether to trigger the error.
157 * @param string $function The function that was called.
158 * @param string $message A message explaining what was done incorrectly.
159 * @param string $version The version of WordPress where the message was added.
160 * @return bool Whether to trigger the error.
161 */
162 public static function bypass_block_metadata_doing_it_wrong( $trigger, $function, $message, $version ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable,Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed,Universal.NamingConventions.NoReservedKeywordParameterNames.functionFound
163 if ( 'WP_Block_Metadata_Registry::register_collection' === $function ) {
164 return false;
165 }
166 return $trigger;
167 }
168
169 /**
170 * Register block patterns
171 */
172 public function register_block_patterns() {
173 register_block_pattern(
174 'woocommerce/order-confirmation-totals-heading',
175 array(
176 'title' => '',
177 'inserter' => false,
178 'content' => '<!-- wp:heading {"level":2,"style":{"typography":{"fontSize":"24px"}}} --><h2 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Order details', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
179 )
180 );
181 register_block_pattern(
182 'woocommerce/order-confirmation-downloads-heading',
183 array(
184 'title' => '',
185 'inserter' => false,
186 'content' => '<!-- wp:heading {"level":2,"style":{"typography":{"fontSize":"24px"}}} --><h2 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Downloads', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
187 )
188 );
189 register_block_pattern(
190 'woocommerce/order-confirmation-shipping-heading',
191 array(
192 'title' => '',
193 'inserter' => false,
194 'content' => '<!-- wp:heading {"level":2,"style":{"typography":{"fontSize":"24px"}}} --><h2 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Shipping address', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
195 )
196 );
197 register_block_pattern(
198 'woocommerce/order-confirmation-billing-heading',
199 array(
200 'title' => '',
201 'inserter' => false,
202 'content' => '<!-- wp:heading {"level":2,"style":{"typography":{"fontSize":"24px"}}} --><h2 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Billing address', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
203 )
204 );
205 register_block_pattern(
206 'woocommerce/order-confirmation-additional-fields-heading',
207 array(
208 'title' => '',
209 'inserter' => false,
210 'content' => '<!-- wp:heading {"level":2,"style":{"typography":{"fontSize":"24px"}}} --><h2 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Additional information', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
211 )
212 );
213 }
214
215 /**
216 * Register block categories
217 *
218 * Used in combination with the `block_categories_all` filter, to append
219 * WooCommerce Blocks related categories to the Gutenberg editor.
220 *
221 * @param array $categories The array of already registered categories.
222 */
223 public function register_block_categories( $categories ) {
224 $woocommerce_block_categories = array(
225 array(
226 'slug' => 'woocommerce',
227 'title' => __( 'WooCommerce', 'woocommerce' ),
228 ),
229 array(
230 'slug' => 'woocommerce-product-elements',
231 'title' => __( 'WooCommerce Product Elements', 'woocommerce' ),
232 ),
233 );
234
235 return array_merge( $categories, $woocommerce_block_categories );
236 }
237
238 /**
239 * Check if a block should have data attributes appended on render. If it's in an allowed namespace, or the block
240 * has explicitly been added to the allowed block list, or if one of the block's parents is in the WooCommerce
241 * namespace it can have data attributes.
242 *
243 * @param string $block_name Name of the block to check.
244 *
245 * @return boolean
246 */
247 public function block_should_have_data_attributes( $block_name ) {
248 $block_namespace = strtok( $block_name ?? '', '/' );
249
250 /**
251 * Filters the list of allowed block namespaces.
252 *
253 * This hook defines which block namespaces should have block name and attribute `data-` attributes appended on render.
254 *
255 * @since 5.9.0
256 *
257 * @param array $allowed_namespaces List of namespaces.
258 */
259 $allowed_namespaces = array_merge( array( 'woocommerce', 'woocommerce-checkout' ), (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', array() ) );
260
261 /**
262 * Filters the list of allowed Block Names
263 *
264 * This hook defines which block names should have block name and attribute data- attributes appended on render.
265 *
266 * @since 5.9.0
267 *
268 * @param array $allowed_namespaces List of namespaces.
269 */
270 $allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', array() );
271
272 $blocks_with_woo_parents = $this->get_registered_blocks_with_woocommerce_parent();
273 $block_has_woo_parent = in_array( $block_name, array_keys( $blocks_with_woo_parents ), true );
274 $in_allowed_namespace_list = in_array( $block_namespace, $allowed_namespaces, true );
275 $in_allowed_block_list = in_array( $block_name, $allowed_blocks, true );
276
277 return $block_has_woo_parent || $in_allowed_block_list || $in_allowed_namespace_list;
278 }
279
280 /**
281 * Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace.
282 *
283 * @param string $content Block content.
284 * @param array $block Parsed block data.
285 * @return string
286 */
287 public function add_data_attributes( $content, $block ) {
288
289 if ( ! is_string( $content ) || ! $this->block_should_have_data_attributes( $block['blockName'] ) ) {
290 return $content;
291 }
292
293 $attributes = (array) $block['attrs'];
294 $exclude_attributes = array( 'className', 'align' );
295
296 $processor = new \WP_HTML_Tag_Processor( $content );
297
298 if (
299 false === $processor->next_tag() || $processor->is_tag_closer()
300 ) {
301
302 return $content;
303 }
304
305 foreach ( $attributes as $key => $value ) {
306 if ( ! is_string( $key ) || in_array( $key, $exclude_attributes, true ) ) {
307 continue;
308 }
309 if ( is_bool( $value ) ) {
310 $value = $value ? 'true' : 'false';
311 }
312 if ( ! is_scalar( $value ) ) {
313 $value = wp_json_encode( $value );
314 }
315
316 // For output consistency, we convert camelCase to kebab-case and output in lowercase.
317 $key = strtolower( preg_replace( '/(?<!^|\ )[A-Z]/', '-$0', $key ) );
318
319 $processor->set_attribute( "data-{$key}", $value );
320 }
321
322 // Set this last to prevent user-input from overriding it.
323 $processor->set_attribute( 'data-block-name', $block['blockName'] );
324 return $processor->get_updated_html();
325 }
326
327 /**
328 * Adds a redirect field to the login form so blocks can redirect users after login.
329 */
330 public function redirect_to_field() {
331 // phpcs:ignore WordPress.Security.NonceVerification
332 if ( empty( $_GET['redirect_to'] ) ) {
333 return;
334 }
335 echo '<input type="hidden" name="redirect" value="' . esc_attr( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) . '" />'; // phpcs:ignore WordPress.Security.NonceVerification
336 }
337
338 /**
339 * Hide legacy widgets with a feature complete block equivalent in the inserter
340 * and prevent them from showing as an option in the Legacy Widget block.
341 *
342 * @param array $widget_types An array of widgets hidden in core.
343 * @return array $widget_types An array including the WooCommerce widgets to hide.
344 */
345 public function hide_legacy_widgets_with_block_equivalent( $widget_types ) {
346 array_push(
347 $widget_types,
348 'woocommerce_product_search',
349 'woocommerce_product_categories',
350 'woocommerce_recent_reviews',
351 'woocommerce_product_tag_cloud',
352 'woocommerce_price_filter',
353 'woocommerce_layered_nav',
354 'woocommerce_layered_nav_filters',
355 'woocommerce_rating_filter'
356 );
357
358 return $widget_types;
359 }
360
361 /**
362 * Delete product transients when a product is deleted.
363 *
364 * @deprecated since 10.6.0
365 * @return void
366 */
367 public function delete_product_transients() {
368 wc_deprecated_function( __METHOD__, '10.6.0' );
369 }
370
371 /**
372 * Get list of block types allowed in Widget Areas. New blocks won't be
373 * exposed in the Widget Area unless specifically added here.
374 *
375 * @return array Array of block types.
376 */
377 protected function get_widget_area_block_types() {
378 return array(
379 'AllReviews',
380 'Breadcrumbs',
381 'CartLink',
382 'CatalogSorting',
383 'ClassicShortcode',
384 'CustomerAccount',
385 'Dropdown',
386 'FeaturedCategory',
387 'FeaturedProduct',
388 'MiniCart',
389 'ProductCategories',
390 'ProductResultsCount',
391 'ProductSearch',
392 'ReviewsByCategory',
393 'ReviewsByProduct',
394 'ProductFilters',
395 'ProductFilterStatus',
396 'ProductFilterPrice',
397 'ProductFilterPriceSlider',
398 'ProductFilterAttribute',
399 'ProductFilterRating',
400 'ProductFilterActive',
401 'ProductFilterRemovableChips',
402 'ProductFilterClearButton',
403 'ProductFilterCheckboxList',
404 'ProductFilterChips',
405 'ProductFilterTaxonomy',
406
407 // Keep hidden legacy filter blocks for backward compatibility.
408 'ActiveFilters',
409 'AttributeFilter',
410 'FilterWrapper',
411 'PriceFilter',
412 'RatingFilter',
413 'StockFilter',
414 // End: legacy filter blocks.
415
416 // Below product grids are hidden from inserter however they could have been used in widgets.
417 // Keep them for backward compatibility.
418 'HandpickedProducts',
419 'ProductBestSellers',
420 'ProductNew',
421 'ProductOnSale',
422 'ProductTopRated',
423 'ProductsByAttribute',
424 'ProductCategory',
425 'ProductTag',
426 // End: legacy product grids blocks.
427 );
428 }
429
430 /**
431 * Get list of block types.
432 *
433 * @return array
434 */
435 protected function get_block_types() {
436 global $pagenow;
437
438 $block_types = array(
439 'ActiveFilters',
440 'AddToCartForm',
441 'AllProducts',
442 'AllReviews',
443 'AttributeFilter',
444 'Breadcrumbs',
445 'CartLink',
446 'CatalogSorting',
447 'CategoryTitle',
448 'CategoryDescription',
449 'ClassicTemplate',
450 'ClassicShortcode',
451 'ComingSoon',
452 'CouponCode',
453 'CustomerAccount',
454 'Dropdown',
455 'EmailContent',
456 'FeaturedCategory',
457 'FeaturedProduct',
458 'FilterWrapper',
459 'HandpickedProducts',
460 'MiniCart',
461 'NextPreviousButtons',
462 'StoreNotices',
463 'PaymentMethodIcons',
464 'PriceFilter',
465 'ProductBestSellers',
466 'ProductButton',
467 'ProductCategories',
468 'ProductCategory',
469 'ProductCollection\Controller',
470 'ProductCollection\NoResults',
471 'ProductFilters',
472 'ProductFilterStatus',
473 'ProductFilterPrice',
474 'ProductFilterPriceSlider',
475 'ProductFilterAttribute',
476 'ProductFilterRating',
477 'ProductFilterActive',
478 'ProductFilterRemovableChips',
479 'ProductFilterClearButton',
480 'ProductFilterCheckboxList',
481 'ProductFilterChips',
482 'ProductFilterTaxonomy',
483 'ProductGallery',
484 'ProductGalleryLargeImage',
485 'ProductGalleryThumbnails',
486 'ProductImage',
487 'ProductImageGallery',
488 'ProductMeta',
489 'ProductNew',
490 'ProductOnSale',
491 'ProductPrice',
492 'ProductTemplate',
493 'ProductQuery',
494 'ProductAverageRating',
495 'ProductRating',
496 'ProductRatingCounter',
497 'ProductRatingStars',
498 'ProductResultsCount',
499 'ProductSaleBadge',
500 'ProductSearch',
501 'ProductSKU',
502 'ProductStockIndicator',
503 'ProductSummary',
504 'ProductTag',
505 'ProductTitle',
506 'ProductTopRated',
507 'ProductsByAttribute',
508 'RatingFilter',
509 'ReviewsByCategory',
510 'ReviewsByProduct',
511 'RelatedProducts',
512 'SingleProduct',
513 'StockFilter',
514 'PageContentWrapper',
515 'OrderConfirmation\Status',
516 'OrderConfirmation\Summary',
517 'OrderConfirmation\Totals',
518 'OrderConfirmation\TotalsWrapper',
519 'OrderConfirmation\Downloads',
520 'OrderConfirmation\DownloadsWrapper',
521 'OrderConfirmation\BillingAddress',
522 'OrderConfirmation\ShippingAddress',
523 'OrderConfirmation\BillingWrapper',
524 'OrderConfirmation\ShippingWrapper',
525 'OrderConfirmation\AdditionalInformation',
526 'OrderConfirmation\AdditionalFieldsWrapper',
527 'OrderConfirmation\AdditionalFields',
528 'OrderConfirmation\CreateAccount',
529 'ProductDetails',
530 'ProductDescription',
531 'ProductSpecifications',
532 // Generic blocks that will be pushed upstream.
533 'Accordion\AccordionGroup',
534 'Accordion\AccordionItem',
535 'Accordion\AccordionPanel',
536 'Accordion\AccordionHeader',
537 // End: generic blocks that will be pushed upstream.
538 'Reviews\ProductReviews',
539 'Reviews\ProductReviewRating',
540 'Reviews\ProductReviewsTitle',
541 'Reviews\ProductReviewForm',
542 'Reviews\ProductReviewDate',
543 'Reviews\ProductReviewContent',
544 'Reviews\ProductReviewAuthorName',
545 'Reviews\ProductReviewsPagination',
546 'Reviews\ProductReviewsPaginationNext',
547 'Reviews\ProductReviewsPaginationPrevious',
548 'Reviews\ProductReviewsPaginationNumbers',
549 'Reviews\ProductReviewTemplate',
550 );
551
552 $block_types = array_merge(
553 $block_types,
554 Cart::get_cart_block_types(),
555 Checkout::get_checkout_block_types(),
556 MiniCartContents::get_mini_cart_block_types()
557 );
558
559 if ( wc_get_container()->get( ShopperListsController::class )->is_enabled( 'saved-for-later' ) ) {
560 $block_types[] = 'SavedForLater';
561 }
562
563 if ( wc_get_container()->get( ShopperListsController::class )->is_enabled( 'wishlist' ) ) {
564 $block_types[] = 'Wishlist';
565 $block_types[] = 'AddToWishlistButton';
566 }
567
568 if ( wp_is_block_theme() ) {
569 $block_types[] = 'AddToCartWithOptions\AddToCartWithOptions';
570 $block_types[] = 'AddToCartWithOptions\QuantitySelector';
571 $block_types[] = 'AddToCartWithOptions\VariationDescription';
572 $block_types[] = 'AddToCartWithOptions\VariationSelector';
573 $block_types[] = 'AddToCartWithOptions\VariationSelectorAttribute';
574 $block_types[] = 'AddToCartWithOptions\VariationSelectorAttributeName';
575 $block_types[] = 'AddToCartWithOptions\GroupedProductSelector';
576 $block_types[] = 'AddToCartWithOptions\GroupedProductItem';
577 $block_types[] = 'AddToCartWithOptions\GroupedProductItemSelector';
578 $block_types[] = 'AddToCartWithOptions\GroupedProductItemLabel';
579 }
580
581 /**
582 * This enables specific blocks in Widget Areas using an opt-in approach.
583 */
584 if ( in_array( $pagenow, array( 'widgets.php', 'themes.php', 'customize.php' ), true ) && ( empty( $_GET['page'] ) || 'gutenberg-edit-site' !== $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
585 $block_types = array_intersect(
586 $block_types,
587 $this->get_widget_area_block_types()
588 );
589 }
590
591 /**
592 * This disables specific blocks in Post and Page editor by not registering them.
593 */
594 if ( in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) ) {
595 $block_types = array_diff(
596 $block_types,
597 array(
598 'Breadcrumbs',
599 'CatalogSorting',
600 'ClassicTemplate',
601 'ProductResultsCount',
602 'ProductReviews',
603 'OrderConfirmation\Status',
604 'OrderConfirmation\Summary',
605 'OrderConfirmation\Totals',
606 'OrderConfirmation\TotalsWrapper',
607 'OrderConfirmation\Downloads',
608 'OrderConfirmation\DownloadsWrapper',
609 'OrderConfirmation\BillingAddress',
610 'OrderConfirmation\ShippingAddress',
611 'OrderConfirmation\BillingWrapper',
612 'OrderConfirmation\ShippingWrapper',
613 'OrderConfirmation\AdditionalInformation',
614 'OrderConfirmation\AdditionalFieldsWrapper',
615 'OrderConfirmation\AdditionalFields',
616 )
617 );
618 }
619
620 /**
621 * Filters the list of allowed block types.
622 *
623 * @since 9.0.0
624 *
625 * @param array $block_types List of block types.
626 */
627 return apply_filters( 'woocommerce_get_block_types', $block_types );
628 }
629
630 /**
631 * By default, when the classic theme is used, block style is always
632 * enqueued even if the block is not used on the page. We want WooCommerce
633 * store to always performant so we have to manually enqueue the block style
634 * on-demand for classic themes.
635 *
636 * @internal
637 *
638 * @param array $args Block metadata.
639 * @param string $block_name Block name.
640 *
641 * @return array Block metadata.
642 */
643 public function enqueue_block_style_for_classic_themes( $args, $block_name ) {
644
645 // Repeatedly checking the theme is expensive. So statically cache this logic result and remove the filter if not needed.
646 static $should_enqueue_block_style_for_classic_themes = null;
647 if ( null === $should_enqueue_block_style_for_classic_themes ) {
648 $should_enqueue_block_style_for_classic_themes = ! (
649 is_admin() ||
650 wp_is_block_theme() ||
651 ( function_exists( 'wp_should_load_block_assets_on_demand' ) && wp_should_load_block_assets_on_demand() ) ||
652 wp_should_load_separate_core_block_assets()
653 );
654 }
655 if ( ! $should_enqueue_block_style_for_classic_themes ) {
656 remove_filter( 'register_block_type_args', array( $this, 'enqueue_block_style_for_classic_themes' ), 10 );
657
658 return $args;
659 }
660
661 if (
662 false === strpos( $block_name, 'woocommerce/' ) ||
663 ( empty( $args['style_handles'] ) && empty( $args['style'] )
664 )
665 ) {
666 return $args;
667 }
668
669 $style_handlers = $args['style_handles'] ?? $args['style'];
670
671 add_filter(
672 'render_block_' . $block_name,
673 static function ( $html ) use ( $style_handlers ) {
674 array_map( 'wp_enqueue_style', $style_handlers );
675
676 return $html;
677 },
678 10
679 );
680
681 $args['style_handles'] = array();
682 $args['style'] = array();
683
684 return $args;
685 }
686
687 /**
688 * Set the preferred taxonomy and term for the breadcrumbs block on the product post type.
689 *
690 * This method mimics the behavior of WC_Breadcrumb::add_crumbs_single() to ensure
691 * consistent breadcrumb term selection between WooCommerce's legacy breadcrumbs
692 * and the Core breadcrumbs block.
693 *
694 * @param array $settings The settings for the breadcrumbs block.
695 * @param string $post_type The post type.
696 * @param int $post_id The current post ID.
697 * @return array The settings for the breadcrumbs block.
698 *
699 * @internal
700 */
701 public function set_product_breadcrumbs_preferred_taxonomy( $settings, $post_type, $post_id = 0 ) {
702 if ( ! is_array( $settings ) || 'product' !== $post_type ) {
703 return $settings;
704 }
705
706 $settings['taxonomy'] = 'product_cat';
707
708 // If we have a post ID, determine the specific term using WooCommerce's logic.
709 if ( ! empty( $post_id ) ) {
710 $terms = wc_get_product_terms(
711 $post_id,
712 'product_cat',
713 /**
714 * Filters the arguments used to fetch product terms for breadcrumbs.
715 *
716 * @since 9.5.0
717 *
718 * @param array $args Array of arguments for `wc_get_product_terms()`.
719 */
720 apply_filters(
721 'woocommerce_breadcrumb_product_terms_args',
722 array(
723 'orderby' => 'parent',
724 'order' => 'DESC',
725 )
726 )
727 );
728
729 if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
730 /**
731 * Filters the main term used in product breadcrumbs.
732 *
733 * @since 9.5.0
734 *
735 * @param \WP_Term $main_term The main term to be used in breadcrumbs.
736 * @param \WP_Term[] $terms Array of all product category terms.
737 */
738 $main_term = apply_filters( 'woocommerce_breadcrumb_main_term', $terms[0], $terms );
739
740 if ( $main_term instanceof \WP_Term ) {
741 $settings['term'] = $main_term->slug;
742 }
743 }
744 }
745
746 return $settings;
747 }
748
749 /**
750 * Apply WooCommerce breadcrumb filters to Core breadcrumbs block items.
751 *
752 * This bridges the Core breadcrumbs block with WooCommerce's legacy breadcrumb filters,
753 * ensuring backward compatibility for sites that have customized breadcrumbs using
754 * the `woocommerce_get_breadcrumb` filter.
755 *
756 * @param array $items Array of breadcrumb items from Core.
757 * @return array Modified breadcrumb items.
758 *
759 * @internal
760 */
761 public function apply_woocommerce_breadcrumb_filters( $items ) {
762 // Convert Core format to WooCommerce format.
763 // Core: array( 'url' => '...', 'label' => '...' )
764 // Woo: array( 'label', 'url' ).
765 $wc_crumbs = array_map(
766 function ( $item ) {
767 return array(
768 $item['label'] ?? '',
769 $item['url'] ?? '',
770 );
771 },
772 $items
773 );
774
775 /**
776 * Filters the breadcrumb trail array.
777 *
778 * @since 2.3.0
779 *
780 * @param array $crumbs The breadcrumb trail.
781 * @param \WC_Breadcrumb|null $breadcrumb The breadcrumb object (null when called from Core block).
782 */
783 $wc_crumbs = apply_filters( 'woocommerce_get_breadcrumb', $wc_crumbs, null );
784
785 // Convert back to Core format.
786 return array_map(
787 function ( $crumb ) {
788 return array(
789 'label' => $crumb[0] ?? '',
790 'url' => $crumb[1] ?? '',
791 );
792 },
793 $wc_crumbs
794 );
795 }
796 }
797