Skip to content
Open
35 changes: 34 additions & 1 deletion src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down
114 changes: 109 additions & 5 deletions tests/phpunit/tests/blocks/resolvePatternBlocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,74 @@ public function set_up() {
'description' => 'Recursive pattern.',
)
);
register_block_pattern(
'core/single-root',
array(
'title' => 'Single Root Pattern',
'content' => '<!-- wp:paragraph -->Single root content<!-- /wp:paragraph -->',
'description' => 'A single root pattern.',
'categories' => array( 'text' ),
)
);
register_block_pattern(
'core/single-root-with-forbidden-chars-in-attrs',
array(
'title' => 'Single Root Pattern<script>alert("XSS")</script>',
'content' => '<!-- wp:paragraph -->Single root content<!-- /wp:paragraph -->',
'description' => 'A single root pattern.<script>alert("XSS")</script><img src=x onerror=alert(1)>',
'categories' => array(
'text<script>alert("XSS")</script>',
'bad\'); DROP TABLE wp_posts;--',
'<img src=x onerror=alert(1)>',
"evil\x00null\nbyte",
'category with <strong>html</strong> tags',
),
)
);
register_block_pattern(
'core/with-attrs',
array(
'title' => 'Pattern With Attrs',
'content' => '<!-- wp:paragraph {"className":"custom-class"} -->Content<!-- /wp:paragraph -->',
'description' => 'A pattern with existing attributes.',
)
);
register_block_pattern(
'core/nested-single',
array(
'title' => 'Nested Pattern',
'content' => '<!-- wp:group --><!-- wp:paragraph -->Nested content<!-- /wp:paragraph --><!-- wp:pattern {"slug":"core/single-root"} /--><!-- /wp:group -->',
'description' => 'A nested single root pattern.',
'categories' => array( 'featured' ),
)
);
register_block_pattern(
'core/existing-metadata',
array(
'title' => 'Existing Metadata Pattern',
'content' => '<!-- wp:paragraph {"metadata":{"patternName":"core/existing-metadata-should-not-overwrite","description":"A existing metadata pattern.","categories":["cake"]}} -->Existing metadata content<!-- /wp:paragraph -->',
)
);
register_block_pattern(
'core/with-custom-metadata',
array(
'title' => 'Pattern With Custom Metadata',
'content' => '<!-- wp:paragraph {"metadata":{"customKey":"customValue","anotherKey":123,"booleanKey":true}} -->Content with custom metadata<!-- /wp:paragraph -->',
'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();
}

Expand All @@ -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( '<!-- wp:pattern /-->', '<!-- wp:pattern /-->' ),
'pattern with no slug attribute' => array(
'<!-- wp:pattern /-->',
'<!-- wp:pattern /-->',
),
// Resolves the pattern.
'test pattern' => array( '<!-- wp:pattern {"slug":"core/test"} /-->', '<!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph -->' ),
'test pattern' => array(
'<!-- wp:pattern {"slug":"core/test"} /-->',
'<!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph -->',
),
// Skips recursive patterns.
'recursive pattern' => array( '<!-- wp:pattern {"slug":"core/recursive"} /-->', '<!-- wp:paragraph -->Recursive<!-- /wp:paragraph -->' ),
'recursive pattern' => array(
'<!-- wp:pattern {"slug":"core/recursive"} /-->',
'<!-- wp:paragraph -->Recursive<!-- /wp:paragraph -->',
),
// Resolves the pattern within a block.
'pattern within a block' => array( '<!-- wp:group --><!-- wp:paragraph -->Before<!-- /wp:paragraph --><!-- wp:pattern {"slug":"core/test"} /--><!-- wp:paragraph -->After<!-- /wp:paragraph --><!-- /wp:group -->', '<!-- wp:group --><!-- wp:paragraph -->Before<!-- /wp:paragraph --><!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph --><!-- wp:paragraph -->After<!-- /wp:paragraph --><!-- /wp:group -->' ),
'pattern within a block' => array(
'<!-- wp:group --><!-- wp:paragraph -->Before<!-- /wp:paragraph --><!-- wp:pattern {"slug":"core/test"} /--><!-- wp:paragraph -->After<!-- /wp:paragraph --><!-- /wp:group -->',
'<!-- wp:group --><!-- wp:paragraph -->Before<!-- /wp:paragraph --><!-- wp:paragraph -->Hello<!-- /wp:paragraph --><!-- wp:paragraph -->World<!-- /wp:paragraph --><!-- wp:paragraph -->After<!-- /wp:paragraph --><!-- /wp:group -->',
),
// Resolves the single-root pattern and adds metadata.
'single-root pattern' => array(
'<!-- wp:pattern {"slug":"core/single-root"} /-->',
'<!-- wp:paragraph {"metadata":{"patternName":"core/single-root","name":"Single Root Pattern","description":"A single root pattern.","categories":["text"]}} -->Single root content<!-- /wp:paragraph -->',
),
// Existing attributes are preserved when adding metadata.
'existing attributes preserved' => array(
'<!-- wp:pattern {"slug":"core/with-attrs"} /-->',
'<!-- wp:paragraph {"className":"custom-class","metadata":{"patternName":"core/with-attrs","name":"Pattern With Attrs","description":"A pattern with existing attributes."}} -->Content<!-- /wp:paragraph -->',
),
// Resolves the nested single-root pattern and adds metadata.
'nested single-root pattern' => array(
'<!-- wp:pattern {"slug":"core/nested-single"} /-->',
'<!-- wp:group {"metadata":{"patternName":"core/nested-single","name":"Nested Pattern","description":"A nested single root pattern.","categories":["featured"]}} --><!-- wp:paragraph -->Nested content<!-- /wp:paragraph --><!-- wp:paragraph {"metadata":{"patternName":"core/single-root","name":"Single Root Pattern","description":"A single root pattern.","categories":["text"]}} -->Single root content<!-- /wp:paragraph --><!-- /wp:group -->',
),
// Sanitizes fields.
'sanitized pattern attrs' => array(
'<!-- wp:pattern {"slug":"core/single-root-with-forbidden-chars-in-attrs"} /-->',
'<!-- wp:paragraph {"metadata":{"patternName":"core/single-root-with-forbidden-chars-in-attrs","name":"Single Root Pattern","description":"A single root pattern.","categories":["text","bad\'); DROP TABLE wp_posts;\u002d\u002d","","evil\u0000null byte","category with html tags"]}} -->Single root content<!-- /wp:paragraph -->',
),
// Metadata is merged with existing metadata and existing metadata is preserved.
'existing metadata preserved' => array(
'<!-- wp:pattern {"slug":"core/existing-metadata"} /-->',
'<!-- wp:paragraph {"metadata":{"patternName":"core/existing-metadata","description":"A existing metadata pattern.","categories":["cake"],"name":"Existing Metadata Pattern"}} -->Existing metadata content<!-- /wp:paragraph -->',
),
// Custom metadata keys are preserved when resolving patterns.
'custom metadata preserved' => array(
'<!-- wp:pattern {"slug":"core/with-custom-metadata"} /-->',
'<!-- wp:paragraph {"metadata":{"customKey":"customValue","anotherKey":123,"booleanKey":true,"patternName":"core/with-custom-metadata","name":"Pattern With Custom Metadata","description":"A pattern with custom metadata keys.","categories":["test"]}} -->Content with custom metadata<!-- /wp:paragraph -->',
),
);
}
}
Loading