diff --git a/lib/compat/wordpress-6.6/resolve-patterns.php b/lib/compat/wordpress-6.6/resolve-patterns.php new file mode 100644 index 00000000000000..f9d9cd675987d9 --- /dev/null +++ b/lib/compat/wordpress-6.6/resolve-patterns.php @@ -0,0 +1,97 @@ +get_registered( $slug ); + $blocks_to_insert = parse_blocks( $pattern['content'] ); + $seen_refs[ $slug ] = true; + $blocks_to_insert = gutenberg_replace_pattern_blocks( $blocks_to_insert ); + 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'] ) ) { + $blocks[ $i ]['innerBlocks'] = gutenberg_replace_pattern_blocks( + $blocks[ $i ]['innerBlocks'], + $blocks[ $i ]['innerContent'] + ); + } + ++$i; + } + } + return $blocks; +} + +function gutenberg_replace_pattern_blocks_get_block_templates( $templates ) { + foreach ( $templates as $template ) { + $blocks = parse_blocks( $template->content ); + $blocks = gutenberg_replace_pattern_blocks( $blocks ); + $template->content = serialize_blocks( $blocks ); + } + return $templates; +} + +function gutenberg_replace_pattern_blocks_get_block_template( $template ) { + $blocks = parse_blocks( $template->content ); + $blocks = gutenberg_replace_pattern_blocks( $blocks ); + $template->content = serialize_blocks( $blocks ); + return $template; +} + +function gutenberg_replace_pattern_blocks_patterns_endpoint( $result, $server, $request ) { + if ( $request->get_route() !== '/wp/v2/block-patterns/patterns' ) { + return $result; + } + + $data = $result->get_data(); + + foreach ( $data as $index => $pattern ) { + $blocks = parse_blocks( $pattern['content'] ); + $blocks = gutenberg_replace_pattern_blocks( $blocks ); + $data[ $index ]['content'] = serialize_blocks( $blocks ); + } + + $result->set_data( $data ); + + return $result; +} + +// For core merge, we should avoid the double parse and replace the patterns in templates here: +// https://github.com/WordPress/wordpress-develop/blob/02fb53498f1ce7e63d807b9bafc47a7dba19d169/src/wp-includes/block-template-utils.php#L558 +add_filter( 'get_block_templates', 'gutenberg_replace_pattern_blocks_get_block_templates' ); +add_filter( 'get_block_template', 'gutenberg_replace_pattern_blocks_get_block_template' ); +// Similarly, for patterns, we can avoid the double parse here: +// https://github.com/WordPress/wordpress-develop/blob/02fb53498f1ce7e63d807b9bafc47a7dba19d169/src/wp-includes/class-wp-block-patterns-registry.php#L175 +add_filter( 'rest_post_dispatch', 'gutenberg_replace_pattern_blocks_patterns_endpoint', 10, 3 ); diff --git a/lib/load.php b/lib/load.php index d6c705790d9704..8f03f59db8ab45 100644 --- a/lib/load.php +++ b/lib/load.php @@ -125,6 +125,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.5/script-loader.php'; // WordPress 6.6 compat. +require __DIR__ . '/compat/wordpress-6.6/resolve-patterns.php'; require __DIR__ . '/compat/wordpress-6.6/block-bindings/pattern-overrides.php'; require __DIR__ . '/compat/wordpress-6.6/option.php'; require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php'; diff --git a/test/e2e/specs/editor/plugins/pattern-recursion.spec.js b/test/e2e/specs/editor/plugins/pattern-recursion.spec.js index 72498bcdf9914d..069f33d671d683 100644 --- a/test/e2e/specs/editor/plugins/pattern-recursion.spec.js +++ b/test/e2e/specs/editor/plugins/pattern-recursion.spec.js @@ -3,7 +3,42 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -test.describe( 'Preventing Pattern Recursion', () => { +test.describe( 'Preventing Pattern Recursion (client)', () => { + test.beforeEach( async ( { admin, editor, page } ) => { + await admin.createNewPost(); + await editor.canvas + .locator( 'role=button[name="Add default block"i]' ) + .click(); + await page.evaluate( () => { + window.wp.data.dispatch( 'core/block-editor' ).updateSettings( { + __experimentalBlockPatterns: [ + { + name: 'evil/recursive', + title: 'Evil recursive', + description: 'Evil recursive', + content: + '
Hello
', + }, + ], + } ); + } ); + } ); + + test( 'prevents infinite loops due to recursive patterns', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/pattern', + attributes: { slug: 'evil/recursive' }, + } ); + const warning = editor.canvas.getByText( + 'Pattern "evil/recursive" cannot be rendered inside itself' + ); + await expect( warning ).toBeVisible(); + } ); +} ); + +test.describe( 'Preventing Pattern Recursion (server)', () => { test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activatePlugin( 'gutenberg-test-protection-against-recursive-patterns' @@ -21,15 +56,30 @@ test.describe( 'Preventing Pattern Recursion', () => { } ); test( 'prevents infinite loops due to recursive patterns', async ( { + page, editor, } ) => { - await editor.insertBlock( { - name: 'core/pattern', - attributes: { slug: 'evil/recursive' }, - } ); - const warning = editor.canvas.getByText( - 'Pattern "evil/recursive" cannot be rendered inside itself' - ); - await expect( warning ).toBeVisible(); + // Click the Toggle block inserter button + await page + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + // Click the Patterns tab + await page.getByRole( 'tab', { name: 'Patterns' } ).click(); + // Click the Uncategorized tab + await page.getByRole( 'button', { name: 'Uncategorized' } ).click(); + // Click the Evil recursive pattern + await page.getByRole( 'option', { name: 'Evil recursive' } ).click(); + // By simply checking the editor content, we know that the pattern + // endpoint did not crash. + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'Hello' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Hello' }, + }, + ] ); } ); } );