Skip to content
3 changes: 3 additions & 0 deletions backport-changelog/7.0/10248.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/10248

* https://github.com/WordPress/gutenberg/pull/72988
115 changes: 115 additions & 0 deletions lib/compat/wordpress-7.0/blocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php
/**
* Compatibility shims for block APIs for WordPress 7.0.
*
* @package gutenberg
*/

if ( ! function_exists( 'gutenberg_resolve_pattern_blocks' ) ) {
/**
* 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.
*
* @return array An array of blocks with patterns replaced by their content.
*/
function gutenberg_resolve_pattern_blocks( $blocks ) {
static $inner_content;
// Keep track of seen references to avoid infinite loops.
static $seen_refs = array();
$i = 0;
while ( $i < count( $blocks ) ) {
if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) {
$attrs = $blocks[ $i ]['attrs'];

if ( empty( $attrs['slug'] ) ) {
++$i;
continue;
}

$slug = $attrs['slug'];

if ( isset( $seen_refs[ $slug ] ) ) {
// Skip recursive patterns.
array_splice( $blocks, $i, 1 );
continue;
}

$registry = WP_Block_Patterns_Registry::get_instance();
$pattern = $registry->get_registered( $slug );

// Skip unknown patterns.
if ( ! $pattern ) {
++$i;
continue;
}
//////////////////////////////
// START CORE MODIFICATIONS //
//////////////////////////////
$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();
$metadata = array( 'patternName' => $slug );

foreach ( array(
'name' => 'title',
'description' => 'description',
'categories' => 'categories',
) as $key => $pattern_key ) {
$value = $pattern[ $pattern_key ] ?? $block_metadata[ $key ] ?? null;
Comment thread
ramonjd marked this conversation as resolved.
Resolved
if ( $value ) {
$metadata[ $key ] = 'categories' === $key && is_array( $value )
? array_map( 'sanitize_text_field', $value )
: sanitize_text_field( $value );
}
}

$blocks_to_insert[0]['attrs']['metadata'] = $metadata;
Comment thread
ramonjd marked this conversation as resolved.
Resolved
}
//////////////////////////////
// END CORE MODIFICATIONS //
//////////////////////////////

$seen_refs[ $slug ] = true;
$prev_inner_content = $inner_content;
$inner_content = null;
$blocks_to_insert = gutenberg_resolve_pattern_blocks( $blocks_to_insert );
$inner_content = $prev_inner_content;
unset( $seen_refs[ $slug ] );
array_splice( $blocks, $i, 1, $blocks_to_insert );

// If we have inner content, we need to insert nulls in the
// inner content array, otherwise serialize_blocks will skip
// blocks.
if ( $inner_content ) {
$null_indices = array_keys( $inner_content, null, true );
$content_index = $null_indices[ $i ];
$nulls = array_fill( 0, count( $blocks_to_insert ), null );
array_splice( $inner_content, $content_index, 1, $nulls );
}

// Skip inserted blocks.
$i += count( $blocks_to_insert );
} else {
if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) {
$prev_inner_content = $inner_content;
$inner_content = $blocks[ $i ]['innerContent'];
$blocks[ $i ]['innerBlocks'] = gutenberg_resolve_pattern_blocks(
$blocks[ $i ]['innerBlocks']
);
$blocks[ $i ]['innerContent'] = $inner_content;
$inner_content = $prev_inner_content;
}
++$i;
}
}
return $blocks;
}
}
67 changes: 67 additions & 0 deletions lib/compat/wordpress-7.0/rest-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* WordPress 7.0 compatibility functions for the Gutenberg
* editor plugin changes related to REST API.
*
* @package gutenberg
*/

/**
* Retrieves a single unified template object using its id.
* Parses pattern blocks in the template content.
*
* @param WP_Block_Template|null $block_template The found block template, or null if there isn't one.
* @param string $id Template unique identifier (example: 'theme_slug//template_slug').
* @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'.
*/
function gutenberg_parse_pattern_blocks_in_block_template( $block_template, $id, $template_type ) {
if ( 'wp_template' !== $template_type ) {
return $block_template;
}

if ( ! empty( $block_template->content ) ) {
$blocks = parse_blocks( $block_template->content );
if ( ! empty( $blocks ) ) {
$blocks = gutenberg_resolve_pattern_blocks( $blocks );
$block_template->content = serialize_blocks( $blocks );
}
}
return $block_template;
}

add_filter( 'get_block_template', 'gutenberg_parse_pattern_blocks_in_block_template', 10, 3 );
add_filter( 'get_block_file_template', 'gutenberg_parse_pattern_blocks_in_block_template', 10, 3 );

/**
* Retrieves a list of unified template objects based on a query.
* Parses pattern blocks in the template content items.
*
* @param WP_Block_Template[] $query_result Array of found block templates.
* @param array $query {
* Arguments to retrieve templates. All arguments are optional.
*
* @type string[] $slug__in List of slugs to include.
* @type int $wp_id Post ID of customized template.
* @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only).
* @type string $post_type Post type to get the templates for.
* }
* @param string $template_type wp_template or wp_template_part.
*/
function gutenberg_parse_pattern_blocks_in_block_templates( $query_result, $query, $template_type ) {
if ( 'wp_template' !== $template_type ) {
return $query_result;
}

if ( ! empty( $query_result ) ) {
foreach ( $query_result as $template ) {
$blocks = parse_blocks( $template->content );
if ( ! empty( $blocks ) ) {
$blocks = gutenberg_resolve_pattern_blocks( $blocks );
$template->content = serialize_blocks( $blocks );
}
}
}
return $query_result;
}

add_filter( 'get_block_templates', 'gutenberg_parse_pattern_blocks_in_block_templates', 10, 3 );
4 changes: 4 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.9/block-comments.php';
require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-comment-controller-6-9.php';

// WordPress 7.0 compat.
require __DIR__ . '/compat/wordpress-7.0/rest-api.php';

// Plugin specific code.
require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php';
require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php';
Expand Down Expand Up @@ -94,6 +97,7 @@ function gutenberg_is_experiment_enabled( $name ) {

// WordPress 7.0 compat.
require __DIR__ . '/compat/wordpress-7.0/php-only-blocks.php';
require __DIR__ . '/compat/wordpress-7.0/blocks.php';

// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
Expand Down
125 changes: 125 additions & 0 deletions phpunit/blocks/gutenberg-resolve-pattern-blocks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
/**
* Tests for the gutenberg_resolve_pattern_blocks function.
*
* @package WordPress
* @subpackage Blocks
*/

/**
* Test the gutenberg_resolve_pattern_blocks function.
*
* @group blocks
* @covers ::gutenberg_resolve_pattern_blocks
*/
class Gutenberg_Resolve_Pattern_Blocks_Test extends WP_UnitTestCase {

public function set_up() {
parent::set_up();

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 -->',
)
);
}

public function tear_down() {
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' );
parent::tear_down();
}


/**
* @dataProvider data_should_resolve_pattern_blocks_as_expected
*
* @param string $blocks A string representing blocks that need resolving.
* @param string $expected Expected result.
*/
public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expected ) {
$actual = gutenberg_resolve_pattern_blocks( parse_blocks( $blocks ) );
$this->assertSame( $expected, serialize_blocks( $actual ) );
}

/**
* Data provider.
*
* @return array
*/
public function data_should_resolve_pattern_blocks_as_expected() {
return array(
// 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","name":"Existing Metadata Pattern","description":"A existing metadata pattern.","categories":["cake"]}} -->Existing metadata content<!-- /wp:paragraph -->',
),
);
}
}
Loading
Loading