Commit ef0506c0532 for woocommerce

commit ef0506c0532f0827ac9fa7cf53a0836fe0e4c474
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Tue Jul 14 17:28:04 2026 +0200

    Keep WooCommerce blocks registered while filtering inserter availability (#66314)

    * register blocks based on the editor context

    * Fix PHPStan block type filtering errors

    * add changelog

    * simplify logic

    * fix logic

diff --git a/plugins/woocommerce/changelog/66314-keep-woocommerce-blocks-registered b/plugins/woocommerce/changelog/66314-keep-woocommerce-blocks-registered
new file mode 100644
index 00000000000..34d5f926a11
--- /dev/null
+++ b/plugins/woocommerce/changelog/66314-keep-woocommerce-blocks-registered
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep WooCommerce blocks registered while filtering block inserter availability by editor context.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index ba0c6f381de..b9db30d3e9d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -62,6 +62,7 @@ final class BlockTypesController {
 		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' ) );
+		add_filter( 'allowed_block_types_all', array( $this, 'filter_allowed_block_types' ), 10, 2 );
 		add_filter( 'register_block_type_args', array( $this, 'enqueue_block_style_for_classic_themes' ), 10, 2 );
 		add_filter( 'block_core_breadcrumbs_post_type_settings', array( $this, 'set_product_breadcrumbs_preferred_taxonomy' ), 10, 3 );
 		add_filter( 'block_core_breadcrumbs_items', array( $this, 'apply_woocommerce_breadcrumb_filters' ), 10, 1 );
@@ -358,6 +359,80 @@ final class BlockTypesController {
 		return $widget_types;
 	}

+	/**
+	 * Filter WooCommerce block availability by editor context.
+	 *
+	 * Blocks remain registered on the server for compatibility, but some WooCommerce
+	 * blocks are hidden from specific editor inserters.
+	 *
+	 * @param bool|string[]            $allowed_block_types Array of block type slugs, or boolean to enable/disable all.
+	 * @param \WP_Block_Editor_Context $block_editor_context The current block editor context.
+	 * @return bool|string[] Filtered list of allowed block types, or boolean to enable/disable all.
+	 */
+	public function filter_allowed_block_types( $allowed_block_types, $block_editor_context ) {
+		if ( false === $allowed_block_types ) {
+			return false;
+		}
+
+		$editor_name = $block_editor_context->name;
+
+		if ( 'core/edit-site' === $editor_name ) {
+			return $allowed_block_types;
+		}
+
+		if ( 'core/edit-post' === $editor_name ) {
+			return $this->remove_allowed_block_types(
+				$allowed_block_types,
+				$this->get_post_editor_hidden_block_types()
+			);
+		}
+
+		if ( in_array( $editor_name, array( 'core/edit-widgets', 'core/customize-widgets' ), true ) ) {
+			$registered_block_types             = $this->get_registered_block_types();
+			$registered_woocommerce_block_types = array_filter(
+				$registered_block_types,
+				static function ( $block_type ) {
+					return 0 === strpos( $block_type, 'woocommerce/' );
+				}
+			);
+
+			return $this->remove_allowed_block_types(
+				$allowed_block_types,
+				array_diff( $registered_woocommerce_block_types, $this->get_widget_area_block_types() )
+			);
+		}
+
+		return $allowed_block_types;
+	}
+
+	/**
+	 * Remove block types from the current allowed block list.
+	 *
+	 * @param bool|string[] $allowed_block_types Array of block type slugs, or true to allow all registered block types.
+	 * @param string[]      $block_types_to_remove Block type slugs to remove.
+	 * @return string[] Filtered block type slugs.
+	 */
+	private function remove_allowed_block_types( $allowed_block_types, array $block_types_to_remove ) {
+		$allowed_block_types = true === $allowed_block_types ? $this->get_registered_block_types() : (array) $allowed_block_types;
+		$allowed_block_types = array_filter( $allowed_block_types, 'is_string' );
+
+		return array_values(
+			array_diff(
+				$allowed_block_types,
+				$block_types_to_remove
+			)
+		);
+	}
+
+	/**
+	 * Get all registered block type slugs.
+	 *
+	 * @return string[] Registered block type slugs.
+	 */
+	private function get_registered_block_types() {
+		return array_keys( \WP_Block_Type_Registry::get_instance()->get_all_registered() );
+	}
+
 	/**
 	 * Delete product transients when a product is deleted.
 	 *
@@ -369,72 +444,98 @@ final class BlockTypesController {
 	}

 	/**
-	 * Get list of block types allowed in Widget Areas. New blocks won't be
+	 * Get list of WooCommerce block types allowed in Widget Areas. New blocks won't be
 	 * exposed in the Widget Area unless specifically added here.
 	 *
-	 * @return array Array of block types.
+	 * @return string[] Array of block type slugs.
 	 */
 	protected function get_widget_area_block_types() {
 		return array(
-			'AllReviews',
-			'Breadcrumbs',
-			'CartLink',
-			'CatalogSorting',
-			'ClassicShortcode',
-			'CustomerAccount',
-			'Dropdown',
-			'FeaturedCategory',
-			'FeaturedProduct',
-			'MiniCart',
-			'ProductCategories',
-			'ProductResultsCount',
-			'ProductSearch',
-			'ReviewsByCategory',
-			'ReviewsByProduct',
-			'ProductFilters',
-			'ProductFilterStatus',
-			'ProductFilterPrice',
-			'ProductFilterPriceSlider',
-			'ProductFilterAttribute',
-			'ProductFilterRating',
-			'ProductFilterActive',
-			'ProductFilterRemovableChips',
-			'ProductFilterClearButton',
-			'ProductFilterCheckboxList',
-			'ProductFilterChips',
-			'ProductFilterTaxonomy',
+			'woocommerce/all-reviews',
+			'woocommerce/breadcrumbs',
+			'woocommerce/cart-link',
+			'woocommerce/catalog-sorting',
+			'woocommerce/classic-shortcode',
+			'woocommerce/customer-account',
+			'woocommerce/dropdown',
+			'woocommerce/featured-category',
+			'woocommerce/featured-product',
+			'woocommerce/mini-cart',
+			'woocommerce/product-categories',
+			'woocommerce/product-results-count',
+			'woocommerce/product-search',
+			'woocommerce/reviews-by-category',
+			'woocommerce/reviews-by-product',
+			'woocommerce/product-filters',
+			'woocommerce/product-filter-status',
+			'woocommerce/product-filter-price',
+			'woocommerce/product-filter-price-slider',
+			'woocommerce/product-filter-attribute',
+			'woocommerce/product-filter-rating',
+			'woocommerce/product-filter-active',
+			'woocommerce/product-filter-removable-chips',
+			'woocommerce/product-filter-clear-button',
+			'woocommerce/product-filter-checkbox-list',
+			'woocommerce/product-filter-chips',
+			'woocommerce/product-filter-taxonomy',

 			// Keep hidden legacy filter blocks for backward compatibility.
-			'ActiveFilters',
-			'AttributeFilter',
-			'FilterWrapper',
-			'PriceFilter',
-			'RatingFilter',
-			'StockFilter',
+			'woocommerce/active-filters',
+			'woocommerce/attribute-filter',
+			'woocommerce/filter-wrapper',
+			'woocommerce/price-filter',
+			'woocommerce/rating-filter',
+			'woocommerce/stock-filter',
 			// End: legacy filter blocks.

 			// Below product grids are hidden from inserter however they could have been used in widgets.
 			// Keep them for backward compatibility.
-			'HandpickedProducts',
-			'ProductBestSellers',
-			'ProductNew',
-			'ProductOnSale',
-			'ProductTopRated',
-			'ProductsByAttribute',
-			'ProductCategory',
-			'ProductTag',
+			'woocommerce/handpicked-products',
+			'woocommerce/product-best-sellers',
+			'woocommerce/product-new',
+			'woocommerce/product-on-sale',
+			'woocommerce/product-top-rated',
+			'woocommerce/products-by-attribute',
+			'woocommerce/product-category',
+			'woocommerce/product-tag',
 			// End: legacy product grids blocks.
 		);
 	}

+	/**
+	 * Get list of WooCommerce block types hidden from post editors.
+	 *
+	 * @return string[] Array of block type slugs.
+	 */
+	protected function get_post_editor_hidden_block_types() {
+		return array(
+			'woocommerce/breadcrumbs',
+			'woocommerce/catalog-sorting',
+			'woocommerce/legacy-template',
+			'woocommerce/product-results-count',
+			'woocommerce/product-reviews',
+			'woocommerce/order-confirmation-status',
+			'woocommerce/order-confirmation-summary',
+			'woocommerce/order-confirmation-totals',
+			'woocommerce/order-confirmation-totals-wrapper',
+			'woocommerce/order-confirmation-downloads',
+			'woocommerce/order-confirmation-downloads-wrapper',
+			'woocommerce/order-confirmation-billing-address',
+			'woocommerce/order-confirmation-shipping-address',
+			'woocommerce/order-confirmation-billing-wrapper',
+			'woocommerce/order-confirmation-shipping-wrapper',
+			'woocommerce/order-confirmation-additional-information',
+			'woocommerce/order-confirmation-additional-fields-wrapper',
+			'woocommerce/order-confirmation-additional-fields',
+		);
+	}
+
 	/**
 	 * Get list of block types.
 	 *
 	 * @return array
 	 */
 	protected function get_block_types() {
-		global $pagenow;
-
 		$block_types = array(
 			'ActiveFilters',
 			'AddToCartForm',
@@ -578,45 +679,6 @@ final class BlockTypesController {
 			$block_types[] = 'AddToCartWithOptions\GroupedProductItemLabel';
 		}

-		/**
-		 * This enables specific blocks in Widget Areas using an opt-in approach.
-		 */
-		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
-			$block_types = array_intersect(
-				$block_types,
-				$this->get_widget_area_block_types()
-			);
-		}
-
-		/**
-		 * This disables specific blocks in Post and Page editor by not registering them.
-		 */
-		if ( in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) ) {
-			$block_types = array_diff(
-				$block_types,
-				array(
-					'Breadcrumbs',
-					'CatalogSorting',
-					'ClassicTemplate',
-					'ProductResultsCount',
-					'ProductReviews',
-					'OrderConfirmation\Status',
-					'OrderConfirmation\Summary',
-					'OrderConfirmation\Totals',
-					'OrderConfirmation\TotalsWrapper',
-					'OrderConfirmation\Downloads',
-					'OrderConfirmation\DownloadsWrapper',
-					'OrderConfirmation\BillingAddress',
-					'OrderConfirmation\ShippingAddress',
-					'OrderConfirmation\BillingWrapper',
-					'OrderConfirmation\ShippingWrapper',
-					'OrderConfirmation\AdditionalInformation',
-					'OrderConfirmation\AdditionalFieldsWrapper',
-					'OrderConfirmation\AdditionalFields',
-				)
-			);
-		}
-
 		/**
 		 * Filters the list of allowed block types.
 		 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
index b75082a26fd..515fb607586 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
@@ -20,6 +20,13 @@ class BlockTypesController extends \WP_UnitTestCase {
 	 */
 	private $block_types_controller;

+	/**
+	 * Block types registered during a test.
+	 *
+	 * @var string[]
+	 */
+	private $registered_test_block_types = array();
+
 	/**
 	 * Sets up a new TestedBlockTypesController so it can be tested.
 	 *
@@ -34,6 +41,23 @@ class BlockTypesController extends \WP_UnitTestCase {
 		);
 	}

+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		foreach ( $this->registered_test_block_types as $block_type ) {
+			if ( \WP_Block_Type_Registry::get_instance()->is_registered( $block_type ) ) {
+				unregister_block_type( $block_type );
+			}
+		}
+
+		$this->registered_test_block_types = array();
+
+		remove_filter( 'allowed_block_types_all', array( $this->block_types_controller, 'filter_allowed_block_types' ), 10 );
+
+		parent::tearDown();
+	}
+
 	/**
 	 * Register 3 blocks, one will be allowed by full name, one by namespace,and one because it has a parent with a
 	 * woocommerce namespace.
@@ -94,4 +118,210 @@ class BlockTypesController extends \WP_UnitTestCase {
 		$answer = $this->block_types_controller->block_should_have_data_attributes( 'child-of-woo/block-name' );
 		$this->assertTrue( $answer );
 	}
+
+	/**
+	 * @testdox Should hide post-editor WooCommerce blocks from unrestricted post editors.
+	 */
+	public function test_edit_post_context_hides_post_editor_block_types_from_unrestricted_list(): void {
+		foreach (
+			array(
+				'core/test-paragraph',
+				'third-party/test-block',
+				'woocommerce/product-search',
+				'woocommerce/breadcrumbs',
+				'woocommerce/product-reviews',
+				'woocommerce/order-confirmation-status',
+			) as $block_type
+		) {
+			$this->register_test_block_type( $block_type );
+		}
+
+		$result = $this->block_types_controller->filter_allowed_block_types(
+			true,
+			$this->get_block_editor_context( 'core/edit-post' )
+		);
+
+		$this->assertContains( 'core/test-paragraph', $result, 'Non-WooCommerce blocks should remain available.' );
+		$this->assertContains( 'third-party/test-block', $result, 'Third-party blocks should remain available.' );
+		$this->assertContains( 'woocommerce/product-search', $result, 'WooCommerce blocks outside the denylist should remain available.' );
+		$this->assertNotContains( 'woocommerce/breadcrumbs', $result, 'Store Breadcrumbs should be hidden in post editors.' );
+		$this->assertNotContains( 'woocommerce/product-reviews', $result, 'Product Reviews should be hidden in post editors.' );
+		$this->assertNotContains( 'woocommerce/order-confirmation-status', $result, 'Order Confirmation blocks should be hidden in post editors.' );
+	}
+
+	/**
+	 * @testdox Should keep only widget-area WooCommerce blocks in widget editors.
+	 *
+	 * @dataProvider widget_editor_context_provider
+	 *
+	 * @param string $editor_context_name Editor context name.
+	 */
+	public function test_widget_context_hides_woocommerce_blocks_not_allowed_in_widget_areas( string $editor_context_name ): void {
+		foreach (
+			array(
+				'core/test-paragraph',
+				'third-party/test-block',
+				'woocommerce/product-search',
+				'woocommerce/product-filters',
+				'woocommerce/checkout',
+				'woocommerce/order-confirmation-status',
+			) as $block_type
+		) {
+			$this->register_test_block_type( $block_type );
+		}
+
+		$result = $this->block_types_controller->filter_allowed_block_types(
+			true,
+			$this->get_block_editor_context( $editor_context_name )
+		);
+
+		$this->assertContains( 'core/test-paragraph', $result, 'Non-WooCommerce blocks should remain available.' );
+		$this->assertContains( 'third-party/test-block', $result, 'Third-party blocks should remain available.' );
+		$this->assertContains( 'woocommerce/product-search', $result, 'Widget-area WooCommerce blocks should remain available.' );
+		$this->assertContains( 'woocommerce/product-filters', $result, 'Widget-area WooCommerce blocks should remain available.' );
+		$this->assertNotContains( 'woocommerce/checkout', $result, 'WooCommerce blocks outside the widget allowlist should be hidden.' );
+		$this->assertNotContains( 'woocommerce/order-confirmation-status', $result, 'WooCommerce blocks outside the widget allowlist should be hidden.' );
+	}
+
+	/**
+	 * @testdox Should leave Site Editor block availability unchanged.
+	 */
+	public function test_site_editor_context_leaves_allowed_block_types_unchanged(): void {
+		$allowed_block_types = array(
+			'core/test-paragraph',
+			'woocommerce/breadcrumbs',
+			'woocommerce/checkout',
+		);
+
+		$result = $this->block_types_controller->filter_allowed_block_types(
+			$allowed_block_types,
+			$this->get_block_editor_context( 'core/edit-site' )
+		);
+
+		$this->assertSame( $allowed_block_types, $result, 'Site Editor block availability should not be changed.' );
+	}
+
+	/**
+	 * @testdox Should preserve an existing post editor allowlist while removing denied WooCommerce blocks.
+	 */
+	public function test_edit_post_context_preserves_existing_allowlist(): void {
+		$allowed_block_types = array(
+			'core/test-paragraph',
+			'woocommerce/product-search',
+			'woocommerce/catalog-sorting',
+			'third-party/test-block',
+		);
+
+		$result = $this->block_types_controller->filter_allowed_block_types(
+			$allowed_block_types,
+			$this->get_block_editor_context( 'core/edit-post' )
+		);
+
+		$this->assertSame(
+			array(
+				'core/test-paragraph',
+				'woocommerce/product-search',
+				'third-party/test-block',
+			),
+			$result,
+			'Existing allowlists should keep their original restrictions.'
+		);
+	}
+
+	/**
+	 * @testdox Should preserve an existing widget editor allowlist while removing disallowed WooCommerce blocks.
+	 */
+	public function test_widget_context_preserves_existing_allowlist(): void {
+		$allowed_block_types = array(
+			'core/test-paragraph',
+			'woocommerce/product-search',
+			'woocommerce/checkout',
+			'third-party/test-block',
+		);
+
+		$result = $this->block_types_controller->filter_allowed_block_types(
+			$allowed_block_types,
+			$this->get_block_editor_context( 'core/edit-widgets' )
+		);
+
+		$this->assertSame(
+			array(
+				'core/test-paragraph',
+				'woocommerce/product-search',
+				'third-party/test-block',
+			),
+			$result,
+			'Existing allowlists should keep their original restrictions.'
+		);
+	}
+
+	/**
+	 * @testdox Should preserve false block availability for restricted editors.
+	 *
+	 * @dataProvider restricted_editor_context_provider
+	 *
+	 * @param string $editor_context_name Editor context name.
+	 */
+	public function test_restricted_context_preserves_false_allowed_block_types( string $editor_context_name ): void {
+		$result = $this->block_types_controller->filter_allowed_block_types(
+			false,
+			$this->get_block_editor_context( $editor_context_name )
+		);
+
+		$this->assertFalse( $result, 'Existing disabled block availability should be preserved.' );
+	}
+
+	/**
+	 * Data provider for widget editor contexts.
+	 *
+	 * @return array<string, array<string>>
+	 */
+	public function widget_editor_context_provider(): array {
+		return array(
+			'widgets editor'    => array( 'core/edit-widgets' ),
+			'customizer editor' => array( 'core/customize-widgets' ),
+		);
+	}
+
+	/**
+	 * Data provider for restricted editor contexts.
+	 *
+	 * @return array<string, array<string>>
+	 */
+	public function restricted_editor_context_provider(): array {
+		return array(
+			'post editor'       => array( 'core/edit-post' ),
+			'widgets editor'    => array( 'core/edit-widgets' ),
+			'customizer editor' => array( 'core/customize-widgets' ),
+		);
+	}
+
+	/**
+	 * Register a block type and track it for cleanup.
+	 *
+	 * @param string $block_type Block type slug.
+	 */
+	private function register_test_block_type( string $block_type ): void {
+		if ( \WP_Block_Type_Registry::get_instance()->is_registered( $block_type ) ) {
+			return;
+		}
+
+		register_block_type( $block_type );
+
+		$this->registered_test_block_types[] = $block_type;
+	}
+
+	/**
+	 * Get a block editor context for tests.
+	 *
+	 * @param string $editor_context_name Editor context name.
+	 * @return \WP_Block_Editor_Context Block editor context.
+	 */
+	private function get_block_editor_context( string $editor_context_name ): \WP_Block_Editor_Context {
+		return new \WP_Block_Editor_Context(
+			array(
+				'name' => $editor_context_name,
+			)
+		);
+	}
 }