Skip to content

Commit

Permalink
Templates perf: resolve patterns server side (WordPress#60349)
Browse files Browse the repository at this point in the history
Co-authored-by: ellatrix <ellatrix@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: mcsf <mcsf@git.wordpress.org>
  • Loading branch information
5 people authored and cbravobernal committed Apr 9, 2024
1 parent 9fa6260 commit 056d855
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 9 deletions.
97 changes: 97 additions & 0 deletions lib/compat/wordpress-6.6/resolve-patterns.php
@@ -0,0 +1,97 @@
<?php

/**
* Replaces pattern blocks with their content.
*
* @param array $blocks Array of blocks.
* @param array $inner_content Optional array of inner content.
* @return array Array of blocks with patterns replaced.
*/
function gutenberg_replace_pattern_blocks( $blocks, &$inner_content = null ) {
// 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'] ) {
$slug = $blocks[ $i ]['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 );
$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 );
1 change: 1 addition & 0 deletions lib/load.php
Expand Up @@ -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';
Expand Down
68 changes: 59 additions & 9 deletions test/e2e/specs/editor/plugins/pattern-recursion.spec.js
Expand Up @@ -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:
'<!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph --><!-- wp:pattern {"slug":"evil/recursive"} /-->',
},
],
} );
} );
} );

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'
Expand All @@ -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' },
},
] );
} );
} );

0 comments on commit 056d855

Please sign in to comment.