Commit ce7fe4f4ede for woocommerce
commit ce7fe4f4ede837d8d962064f327bd3d3c9d95c5f
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Mon Sep 14 09:10:29 2026 +0200
Fix warning when certain blocks were used in the Single Product template of a password-protected block (#68477)
* Fix warning when certain blocks were used in the Single Product template of a password-protected block
* Move complex logic to simpler guards
* Add test
* Convert test comment to testdox
diff --git a/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php b/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php
index ef3f56373f1..c4e4d2f42ae 100644
--- a/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/SingleProductTemplate.php
@@ -126,13 +126,103 @@ class SingleProductTemplate extends AbstractTemplate {
/**
* Replace the first single product template block with the password form. Remove all other single product template blocks.
*
- * @param array $parsed_blocks Array of parsed block objects.
- * @param boolean $is_already_replaced If the password form has already been added.
- * @return array Parsed blocks
+ * @param array $parsed_blocks Array of parsed block objects.
+ * @return array Parsed blocks.
*/
- private static function replace_first_single_product_template_block_with_password_form( $parsed_blocks, $is_already_replaced ) {
- // We want to replace the first single product template block with the password form. We also want to remove all other single product template blocks.
- // This array doesn't contains all the blocks. For example, it missing the breadcrumbs blocks: it doesn't make sense replace the breadcrumbs with the password form.
+ private static function replace_first_single_product_template_block_with_password_form( $parsed_blocks ) {
+ $blocks = array();
+ $is_already_replaced = false;
+
+ foreach ( $parsed_blocks as $block ) {
+ $processed = self::process_block_for_password_form( $block, $is_already_replaced );
+ $is_already_replaced = $processed['is_already_replaced'];
+ $blocks = array_merge( $blocks, $processed['blocks'] );
+ }
+
+ return $blocks;
+ }
+
+ /**
+ * Replace or remove a parsed block when rendering a password-protected product.
+ *
+ * @param array $block Parsed block.
+ * @param bool $is_already_replaced If the password form has already been added.
+ * @return array{blocks: array, is_already_replaced: bool} Replacement blocks (empty when removed).
+ */
+ private static function process_block_for_password_form( $block, $is_already_replaced ) {
+ if ( self::is_password_protected_replacement_block( $block ) ) {
+ if ( $is_already_replaced ) {
+ return array(
+ 'blocks' => array(),
+ 'is_already_replaced' => true,
+ );
+ }
+
+ return array(
+ 'blocks' => array( parse_blocks( '<!-- wp:html -->' . get_the_password_form() . '<!-- /wp:html -->' )[0] ),
+ 'is_already_replaced' => true,
+ );
+ }
+
+ if ( empty( $block['innerBlocks'] ) || ! isset( $block['innerContent'] ) || ! is_array( $block['innerContent'] ) ) {
+ return array(
+ 'blocks' => array( $block ),
+ 'is_already_replaced' => $is_already_replaced,
+ );
+ }
+
+ $new_inner_blocks = array();
+ $new_inner_content = array();
+ $inner_block_index = 0;
+
+ foreach ( $block['innerContent'] as $chunk ) {
+ if ( is_string( $chunk ) ) {
+ $new_inner_content[] = $chunk;
+ continue;
+ }
+
+ if ( ! isset( $block['innerBlocks'][ $inner_block_index ] ) ) {
+ continue;
+ }
+
+ $processed = self::process_block_for_password_form( $block['innerBlocks'][ $inner_block_index ], $is_already_replaced );
+ $is_already_replaced = $processed['is_already_replaced'];
+ ++$inner_block_index;
+
+ foreach ( $processed['blocks'] as $processed_block ) {
+ $new_inner_blocks[] = $processed_block;
+ $new_inner_content[] = null;
+ }
+ }
+
+ if ( count( $new_inner_blocks ) === 0 ) {
+ return array(
+ 'blocks' => array(),
+ 'is_already_replaced' => $is_already_replaced,
+ );
+ }
+
+ $block['innerBlocks'] = $new_inner_blocks;
+ $block['innerContent'] = $new_inner_content;
+
+ return array(
+ 'blocks' => array( $block ),
+ 'is_already_replaced' => $is_already_replaced,
+ );
+ }
+
+ /**
+ * Whether this block should be replaced with the password form (or removed after the first replacement).
+ *
+ * This list is not every block that can appear on a single product template. Blocks such as breadcrumbs
+ * stay visible on password-protected products.
+ *
+ * @param array $block Parsed block.
+ * @return bool
+ */
+ private static function is_password_protected_replacement_block( $block ) {
+ $block_name = $block['blockName'] ?? '';
+
$single_product_template_blocks = array(
'woocommerce/product-image-gallery',
'woocommerce/product-details',
@@ -149,98 +239,13 @@ class SingleProductTemplate extends AbstractTemplate {
'core/post-excerpt',
);
- return array_reduce(
- $parsed_blocks,
- function ( $carry, $block ) use ( $single_product_template_blocks ) {
- if ( in_array( $block['blockName'], $single_product_template_blocks, true ) || ( 'core/pattern' === $block['blockName'] && isset( $block['attrs']['slug'] ) && 'woocommerce-blocks/related-products' === $block['attrs']['slug'] ) ) {
- if ( $carry['is_already_replaced'] ) {
- return array(
- 'blocks' => $carry['blocks'],
- 'html_block' => null,
- 'removed' => true,
- 'is_already_replaced' => true,
-
- );
- }
-
- return array(
- 'blocks' => $carry['blocks'],
- 'html_block' => parse_blocks( '<!-- wp:html -->' . get_the_password_form() . '<!-- /wp:html -->' )[0],
- 'removed' => false,
- 'is_already_replaced' => $carry['is_already_replaced'],
- );
-
- }
-
- if ( isset( $block['innerBlocks'] ) && count( $block['innerBlocks'] ) > 0 ) {
- $index = 0;
- $new_inner_blocks = array();
- $new_inner_contents = $block['innerContent'];
- foreach ( $block['innerContent'] as $inner_content ) {
- // Don't process the closing tag of the block.
- if ( count( $block['innerBlocks'] ) === $index ) {
- break;
- }
-
- $blocks = self::replace_first_single_product_template_block_with_password_form( array( $block['innerBlocks'][ $index ] ), $carry['is_already_replaced'] );
- $new_blocks = $blocks['blocks'];
- $html_block = $blocks['html_block'];
- $is_removed = $blocks['removed'];
- $carry['is_already_replaced'] = $blocks['is_already_replaced'];
-
- if ( isset( $html_block ) ) {
- $new_inner_blocks = array_merge( $new_inner_blocks, $new_blocks, array( $html_block ) );
- $carry['is_already_replaced'] = true;
- } else {
- $new_inner_blocks = array_merge( $new_inner_blocks, $new_blocks );
- }
-
- if ( $is_removed ) {
- unset( $new_inner_contents[ $index ] );
- // The last element of the inner contents contains the closing tag of the block. We don't want to remove it.
- if ( $index + 1 < count( $new_inner_contents ) ) {
- unset( $new_inner_contents[ $index + 1 ] );
- }
- $new_inner_contents = array_values( $new_inner_contents );
- }
-
- $index++;
- }
-
- $block['innerBlocks'] = $new_inner_blocks;
- $block['innerContent'] = $new_inner_contents;
-
- if ( count( $new_inner_blocks ) === 0 ) {
- return array(
- 'blocks' => $carry['blocks'],
- 'html_block' => null,
- 'removed' => true,
- 'is_already_replaced' => $carry['is_already_replaced'],
- );
- }
-
- return array(
- 'blocks' => array_merge( $carry['blocks'], array( $block ) ),
- 'html_block' => null,
- 'removed' => false,
- 'is_already_replaced' => $carry['is_already_replaced'],
- );
- }
+ if ( in_array( $block_name, $single_product_template_blocks, true ) ) {
+ return true;
+ }
- return array(
- 'blocks' => array_merge( $carry['blocks'], array( $block ) ),
- 'html_block' => null,
- 'removed' => false,
- 'is_already_replaced' => $carry['is_already_replaced'],
- );
- },
- array(
- 'blocks' => array(),
- 'html_block' => null,
- 'removed' => false,
- 'is_already_replaced' => $is_already_replaced,
- )
- );
+ return 'core/pattern' === $block_name
+ && isset( $block['attrs']['slug'] )
+ && 'woocommerce-blocks/related-products' === $block['attrs']['slug'];
}
/**
@@ -251,8 +256,7 @@ class SingleProductTemplate extends AbstractTemplate {
*/
public static function add_password_form( $content ) {
$parsed_blocks = parse_blocks( $content );
- $blocks = self::replace_first_single_product_template_block_with_password_form( $parsed_blocks, false );
- $serialized_blocks = serialize_blocks( $blocks['blocks'] );
+ $serialized_blocks = serialize_blocks( self::replace_first_single_product_template_block_with_password_form( $parsed_blocks ) );
return $serialized_blocks;
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Templates/SingleProductTemplateTests.php b/plugins/woocommerce/tests/php/src/Blocks/Templates/SingleProductTemplateTests.php
index 63434bed382..f1bd225de10 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Templates/SingleProductTemplateTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Templates/SingleProductTemplateTests.php
@@ -362,4 +362,32 @@ class SingleProductTemplateTests extends WP_UnitTestCase {
TemplateContentUtils::strip_whitespace_and_password_form_ids( $result )
);
}
+
+ /**
+ * @testdox Adds the password form when product blocks are at the top level of the template.
+ */
+ public function test_replace_top_level_single_product_blocks_with_input_form() {
+ $default_single_product_template = '
+ <!-- wp:template-part {"slug":"header","theme":"twentytwentythree","tagName":"header"} /-->
+ <!-- wp:woocommerce/product-image-gallery /-->
+ <!-- wp:woocommerce/product-price {"isDescendentOfSingleProductTemplate":true} /-->
+ <!-- wp:template-part {"slug":"footer","theme":"twentytwentythree","tagName":"footer"} /-->';
+
+ $expected_single_product_template = sprintf(
+ '
+ <!-- wp:template-part {"slug":"header","theme":"twentytwentythree","tagName":"header"} /-->
+ <!-- wp:html -->%s<!-- /wp:html -->
+ <!-- wp:template-part {"slug":"footer","theme":"twentytwentythree","tagName":"footer"} /-->',
+ get_the_password_form()
+ );
+
+ $result = SingleProductTemplate::add_password_form(
+ $default_single_product_template
+ );
+
+ $this->assertEquals(
+ TemplateContentUtils::strip_whitespace_and_password_form_ids( $result ),
+ TemplateContentUtils::strip_whitespace_and_password_form_ids( $expected_single_product_template )
+ );
+ }
}