diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index ba9fc4f344..169e4b3c25 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -125,10 +125,9 @@ function get_dynamic_block_names() { * @return string The parsed and filtered content. */ function excerpt_remove_blocks( $content ) { - $allowed_blocks = array( + $allowed_inner_blocks = array( // Classic blocks have their blockName set to null. null, - 'core/columns', 'core/freeform', 'core/heading', 'core/html', @@ -141,6 +140,9 @@ function excerpt_remove_blocks( $content ) { 'core/table', 'core/verse', ); + + $allowed_blocks = array_merge( $allowed_inner_blocks, array( 'core/columns' ) ); + /** * Filters the list of blocks that can contribute to the excerpt. * @@ -154,12 +156,55 @@ function excerpt_remove_blocks( $content ) { $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); $blocks = parse_blocks( $content ); $output = ''; + foreach ( $blocks as $block ) { if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { + if ( ! empty( $block['innerBlocks'] ) ) { + if ( 'core/columns' === $block['blockName'] ) { + $output .= _excerpt_render_inner_columns_blocks( $block, $allowed_inner_blocks ); + continue; + } + + // Skip the block if it has disallowed or nested inner blocks. + foreach ( $block['innerBlocks'] as $inner_block ) { + if ( + ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || + ! empty( $inner_block['innerBlocks'] ) + ) { + continue 2; + } + } + } + $output .= render_block( $block ); } } - return $output; + + return $output; +} + +/** + * Render inner blocks from the `core/columns` block for generating an excerpt. + * + * @since 5.2.0 + * @access private + * + * @param array $columns The parsed columns block. + * @param array $allowed_blocks The list of allowed inner blocks. + * @return string The rendered inner blocks. + */ +function _excerpt_render_inner_columns_blocks( $columns, $allowed_blocks ) { + $output = ''; + + foreach ( $columns['innerBlocks'] as $column ) { + foreach ( $column['innerBlocks'] as $inner_block ) { + if ( in_array( $inner_block['blockName'], $allowed_blocks, true ) && empty( $inner_block['innerBlocks'] ) ) { + $output .= render_block( $inner_block ); + } + } + } + + return $output; } /** diff --git a/tests/phpunit/tests/formatting/ExcerptRemoveBlocks.php b/tests/phpunit/tests/formatting/ExcerptRemoveBlocks.php new file mode 100644 index 0000000000..8a27f18e81 --- /dev/null +++ b/tests/phpunit/tests/formatting/ExcerptRemoveBlocks.php @@ -0,0 +1,129 @@ + +

paragraph

+ + + + + + +
+ +
+ + + +

paragraph inside column

+ +
+ +
+ +'; + + public $filtered_content = ' + +

paragraph

+ + + + +

paragraph inside column

+ +'; + + /** + * Fake block rendering function. + * + * @since 5.2.0 + * + * @return string Block output. + */ + function render_fake_block() { + return get_the_excerpt( self::$post_id ); + } + + /** + * Set up. + * + * @since 5.2.0 + */ + function setUp() { + parent::setUp(); + self::$post_id = $this->factory()->post->create( + array( + 'post_excerpt' => '', // Empty excerpt, so it has to be generated. + 'post_content' => '', + ) + ); + register_block_type( + 'core/fake', + array( + 'render_callback' => array( $this, 'render_fake_block' ), + ) + ); + } + + /** + * Tear down. + * + * @since 5.2.0 + */ + function tearDown() { + parent::tearDown(); + $registry = WP_Block_Type_Registry::get_instance(); + $registry->unregister( 'core/fake' ); + wp_delete_post( self::$post_id, true ); + } + + /** + * Tests excerpt_remove_blocks(). + * + * @ticket 46133 + */ + function test_excerpt_remove_blocks() { + // Simple dynamic block.. + $content = ''; + + $this->assertEmpty( excerpt_remove_blocks( $content ) ); + + // Dynamic block with options, embedded in other content. + $this->assertEquals( $this->filtered_content, excerpt_remove_blocks( $this->content ) ); + } + + /** + * Tests that dynamic blocks don't cause an out-of-memory error. + * + * When dynamic blocks happen to generate an excerpt, they can cause an + * infinite loop if that block is part of the post's content. + * + * `wp_trim_excerpt()` applies the `the_content` filter, which has + * `do_blocks` attached to it, trying to render the block which again will + * attempt to return an excerpt of that post. + * + * This infinite loop can be avoided by stripping dynamic blocks before + * `the_content` gets applied, just like shortcodes. + * + * @ticket 46133 + */ + function test_excerpt_infinite_loop() { + $query = new WP_Query( + array( + 'post__in' => array( self::$post_id ), + ) + ); + $query->the_post(); + $this->assertEmpty( do_blocks( '' ) ); + } +}