From 1b6e00c30642a4f9451297e7d25de2d761fc0c07 Mon Sep 17 00:00:00 2001 From: Bernhard Reiter Date: Mon, 30 Sep 2024 12:23:17 +0000 Subject: [PATCH] Block Hooks: Respect `"multiple": false` in hooked blocks. If a prospective hooked block has its `multiple` block-supports field set to `false` (thus allowing only one instance of the block to be present), ensure that: 1. Only one instance of the block will be inserted if it's not yet present in the current context. 2. The block will not be inserted at all if an instance of it is already present in the current context. As always in Block Hooks parlance, "context" denotes the containing template, template part, pattern, or navigation post that a hooked block is supposed to be inserted into. The markup of a webpage that uses a Block Theme typically comprises a number of such contexts -- one template and any number of template parts, patterns, and navigation posts. Note that the limitation imposed by this changeset only applies on a per-context basis, so it's still possible that the resulting page contains more than one instance of a hooked block with `"multiple": false` set, as each context could contribute up to one such instance. Props bernhard-reiter, jonsurrell, gziolo. Fixes #61902. Built from https://develop.svn.wordpress.org/trunk@59124 git-svn-id: http://core.svn.wordpress.org/trunk@58520 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/blocks.php | 72 +++++++++++++++++++++++++++++++++++++++-- wp-includes/version.php | 2 +- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/wp-includes/blocks.php b/wp-includes/blocks.php index edcc68284a..ca84130995 100644 --- a/wp-includes/blocks.php +++ b/wp-includes/blocks.php @@ -1056,9 +1056,77 @@ function apply_block_hooks_to_content( $content, $context, $callback = 'insert_h $after_block_visitor = make_after_block_visitor( $hooked_blocks, $context, $callback ); } - $blocks = parse_blocks( $content ); + $block_allows_multiple_instances = array(); + /* + * Remove hooked blocks from `$hooked_block_types` if they have `multiple` set to false and + * are already present in `$content`. + */ + foreach ( $hooked_blocks as $anchor_block_type => $relative_positions ) { + foreach ( $relative_positions as $relative_position => $hooked_block_types ) { + foreach ( $hooked_block_types as $index => $hooked_block_type ) { + $hooked_block_type_definition = + WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type ); - return traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); + $block_allows_multiple_instances[ $hooked_block_type ] = + block_has_support( $hooked_block_type_definition, 'multiple', true ); + + if ( + ! $block_allows_multiple_instances[ $hooked_block_type ] && + has_block( $hooked_block_type, $content ) + ) { + unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ][ $index ] ); + } + } + if ( empty( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) { + unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ); + } + } + if ( empty( $hooked_blocks[ $anchor_block_type ] ) ) { + unset( $hooked_blocks[ $anchor_block_type ] ); + } + } + + /* + * We also need to cover the case where the hooked block is not present in + * `$content` at first and we're allowed to insert it once -- but not again. + */ + $suppress_single_instance_blocks = static function ( $hooked_block_types ) use ( &$block_allows_multiple_instances, $content ) { + static $single_instance_blocks_present_in_content = array(); + foreach ( $hooked_block_types as $index => $hooked_block_type ) { + if ( ! isset( $block_allows_multiple_instances[ $hooked_block_type ] ) ) { + $hooked_block_type_definition = + WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type ); + + $block_allows_multiple_instances[ $hooked_block_type ] = + block_has_support( $hooked_block_type_definition, 'multiple', true ); + } + + if ( $block_allows_multiple_instances[ $hooked_block_type ] ) { + continue; + } + + // The block doesn't allow multiple instances, so we need to check if it's already present. + if ( + in_array( $hooked_block_type, $single_instance_blocks_present_in_content, true ) || + has_block( $hooked_block_type, $content ) + ) { + unset( $hooked_block_types[ $index ] ); + } else { + // We can insert the block once, but need to remember not to insert it again. + $single_instance_blocks_present_in_content[] = $hooked_block_type; + } + } + return $hooked_block_types; + }; + add_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX ); + $content = traverse_and_serialize_blocks( + parse_blocks( $content ), + $before_block_visitor, + $after_block_visitor + ); + remove_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX ); + + return $content; } /** diff --git a/wp-includes/version.php b/wp-includes/version.php index 20a527282e..d734011e97 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.7-alpha-59123'; +$wp_version = '6.7-alpha-59124'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.