diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index a8ed76d3bd71f..dff2ada10b6eb 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1844,6 +1844,7 @@ function traverse_and_serialize_block( $block, $pre_callback = null, $post_callb * Replaces patterns in a block tree with their content. * * @since 6.6.0 + * @since 7.0.0 Adds metadata to attributes of single-pattern container blocks. * * @param array $blocks An array blocks. * @@ -1880,7 +1881,39 @@ function resolve_pattern_blocks( $blocks ) { continue; } - $blocks_to_insert = parse_blocks( $pattern['content'] ); + $blocks_to_insert = parse_blocks( trim( $pattern['content'] ) ); + + /* + * For single-root patterns, add the pattern name to make this a pattern instance in the editor. + * If the pattern has metadata, merge it with the existing metadata. + */ + if ( count( $blocks_to_insert ) === 1 ) { + $block_metadata = $blocks_to_insert[0]['attrs']['metadata'] ?? array(); + $block_metadata['patternName'] = $slug; + + /* + * Merge pattern metadata with existing block metadata. + * Pattern metadata takes precedence, but existing block metadata + * is preserved as a fallback when the pattern doesn't define that field. + * Only the defined fields (name, description, categories) are updated; + * other metadata keys are preserved. + */ + foreach ( array( + 'name' => 'title', // 'title' is the field in the pattern object 'name' is the field in the block metadata. + 'description' => 'description', + 'categories' => 'categories', + ) as $key => $pattern_key ) { + $value = $pattern[ $pattern_key ] ?? $block_metadata[ $key ] ?? null; + if ( $value ) { + $block_metadata[ $key ] = is_array( $value ) + ? array_map( 'sanitize_text_field', $value ) + : sanitize_text_field( $value ); + } + } + + $blocks_to_insert[0]['attrs']['metadata'] = $block_metadata; + } + $seen_refs[ $slug ] = true; $prev_inner_content = $inner_content; $inner_content = null; diff --git a/tests/phpunit/tests/blocks/resolvePatternBlocks.php b/tests/phpunit/tests/blocks/resolvePatternBlocks.php index b2e6fa6463f7d..4b28db4613f00 100644 --- a/tests/phpunit/tests/blocks/resolvePatternBlocks.php +++ b/tests/phpunit/tests/blocks/resolvePatternBlocks.php @@ -30,12 +30,74 @@ public function set_up() { 'description' => 'Recursive pattern.', ) ); + register_block_pattern( + 'core/single-root', + array( + 'title' => 'Single Root Pattern', + 'content' => 'Single root content', + 'description' => 'A single root pattern.', + 'categories' => array( 'text' ), + ) + ); + register_block_pattern( + 'core/single-root-with-forbidden-chars-in-attrs', + array( + 'title' => 'Single Root Pattern', + 'content' => 'Single root content', + 'description' => 'A single root pattern.', + 'categories' => array( + 'text', + 'bad\'); DROP TABLE wp_posts;--', + '', + "evil\x00null\nbyte", + 'category with html tags', + ), + ) + ); + register_block_pattern( + 'core/with-attrs', + array( + 'title' => 'Pattern With Attrs', + 'content' => 'Content', + 'description' => 'A pattern with existing attributes.', + ) + ); + register_block_pattern( + 'core/nested-single', + array( + 'title' => 'Nested Pattern', + 'content' => 'Nested content', + 'description' => 'A nested single root pattern.', + 'categories' => array( 'featured' ), + ) + ); + register_block_pattern( + 'core/existing-metadata', + array( + 'title' => 'Existing Metadata Pattern', + 'content' => 'Existing metadata content', + ) + ); + register_block_pattern( + 'core/with-custom-metadata', + array( + 'title' => 'Pattern With Custom Metadata', + 'content' => 'Content with custom metadata', + 'description' => 'A pattern with custom metadata keys.', + 'categories' => array( 'test' ), + ) + ); } public function tear_down() { unregister_block_pattern( 'core/test' ); unregister_block_pattern( 'core/recursive' ); - + unregister_block_pattern( 'core/single-root' ); + unregister_block_pattern( 'core/single-root-with-forbidden-chars-in-attrs' ); + unregister_block_pattern( 'core/with-attrs' ); + unregister_block_pattern( 'core/nested-single' ); + unregister_block_pattern( 'core/existing-metadata' ); + unregister_block_pattern( 'core/with-custom-metadata' ); parent::tear_down(); } @@ -60,13 +122,55 @@ public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expect public function data_should_resolve_pattern_blocks_as_expected() { return array( // Works without attributes, leaves the block as is. - 'pattern with no slug attribute' => array( '', '' ), + 'pattern with no slug attribute' => array( + '', + '', + ), // Resolves the pattern. - 'test pattern' => array( '', 'HelloWorld' ), + 'test pattern' => array( + '', + 'HelloWorld', + ), // Skips recursive patterns. - 'recursive pattern' => array( '', 'Recursive' ), + 'recursive pattern' => array( + '', + 'Recursive', + ), // Resolves the pattern within a block. - 'pattern within a block' => array( 'BeforeAfter', 'BeforeHelloWorldAfter' ), + 'pattern within a block' => array( + 'BeforeAfter', + 'BeforeHelloWorldAfter', + ), + // Resolves the single-root pattern and adds metadata. + 'single-root pattern' => array( + '', + 'Single root content', + ), + // Existing attributes are preserved when adding metadata. + 'existing attributes preserved' => array( + '', + 'Content', + ), + // Resolves the nested single-root pattern and adds metadata. + 'nested single-root pattern' => array( + '', + 'Nested contentSingle root content', + ), + // Sanitizes fields. + 'sanitized pattern attrs' => array( + '', + 'Single root content', + ), + // Metadata is merged with existing metadata and existing metadata is preserved. + 'existing metadata preserved' => array( + '', + 'Existing metadata content', + ), + // Custom metadata keys are preserved when resolving patterns. + 'custom metadata preserved' => array( + '', + 'Content with custom metadata', + ), ); } }