From 44c7524ea8acaea2831c3c01e96b5e6c43ba5a6b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 29 Jan 2025 15:35:36 +0100 Subject: [PATCH 1/8] Block Hooks: Add apply_block_hooks_to_content_from_post_object --- src/wp-includes/blocks.php | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index c09321edfa5bd..718f8d8b5759c 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1140,6 +1140,74 @@ function apply_block_hooks_to_content( $content, $context = null, $callback = 'i return $content; } +/** + * Run the Block Hooks algorithm on a post object's content. + * + * This function is different from `apply_block_hooks_to_content` in that + * it takes ignored hooked block information from the post's metadata into + * account. This ensures that any blocks hooked as first or last child + * of the block that corresponds to the post type are handled correctly. + * + * @since 6.8.0 + * @access private + * + * @param string $content Serialized content. + * @param WP_Post|null $post A post object that the content belongs to. If set to `null`, + * `get_post()` will be called to use the current post as context. + * Default: `null`. + * @param callable $callback A function that will be called for each block to generate + * the markup for a given list of blocks that are hooked to it. + * Default: 'insert_hooked_blocks'. + * @return string The serialized markup. + */ +function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post = null, $callback = 'insert_hooked_blocks' ) { + // Default to the current post if no context is provided. + if ( null === $post ) { + $post = get_post(); + } + + if ( ! $post instanceof WP_Post ) { + return apply_block_hooks_to_content( $content, $post, $callback ); + } + + $attributes = array(); + + // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta. + $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $ignored_hooked_blocks ) ) { + $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => $ignored_hooked_blocks, + ); + } + + // We need to wrap the content in a temporary wrapper block with that metadata + // so the Block Hooks algorithm can insert blocks that are hooked as first or last child + // of the wrapper block. + // To that end, we need to determine the wrapper block type based on the post type. + if ( 'wp_navigation' === $post->post_type ) { + $wrapper_block_type = 'core/navigation'; + } elseif ( 'wp_block' === $post->post_type ) { + $wrapper_block_type = 'core/block'; + } else { + $wrapper_block_type = 'core/post-content'; + } + + $content = get_comment_delimited_block_content( + $wrapper_block_type, + $attributes, + $content + ); + + // Apply Block Hooks. + $content = apply_block_hooks_to_content( $content, $post, $callback ); + + // Finally, we need to remove the temporary wrapper block. + $content = remove_serialized_parent_block( $content ); + + return $content; +} + /** * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks. * From ea9dd1620540d22330e5e1b7b6dab04ff6c43311 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 29 Jan 2025 16:25:26 +0100 Subject: [PATCH 2/8] Use new filter for the_content --- src/wp-includes/default-filters.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 54883b840d7f4..f94f9a3cd3fd8 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -197,7 +197,7 @@ add_filter( 'the_title', 'convert_chars' ); add_filter( 'the_title', 'trim' ); -add_filter( 'the_content', 'apply_block_hooks_to_content', 8 ); // BEFORE do_blocks(). +add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 ); // BEFORE do_blocks(). add_filter( 'the_content', 'do_blocks', 9 ); add_filter( 'the_content', 'wptexturize' ); add_filter( 'the_content', 'convert_smilies', 20 ); From 7c9240fdfc429b75ca76d166fcc829ea6f804c69 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 30 Jan 2025 22:43:55 +0100 Subject: [PATCH 3/8] Add basic test coverage --- ...applyBlockHooksToContentFromPostObject.php | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php diff --git a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php new file mode 100644 index 0000000000000..b8961cf5a9ec5 --- /dev/null +++ b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php @@ -0,0 +1,116 @@ +post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_content' => '

Hello World!

', + ) + ); + + self::$post_with_ignored_hooked_block = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_content' => '

Hello World!

', + 'meta_input' => array( + '_wp_ignored_hooked_blocks' => '["tests/hooked-block-first-child"]', + ), + ) + ); + + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + register_block_type( + 'tests/hooked-block-first-child', + array( + 'block_hooks' => array( + 'core/post-content' => 'first_child', + ), + ) + ); + } + + /** + * Tear down. + * + * @ticket 62716 + */ + public static function wpTearDownAfterClass() { + $registry = WP_Block_Type_Registry::get_instance(); + + $registry->unregister( 'tests/hooked-block' ); + $registry->unregister( 'tests/hooked-block-first-child' ); + } + + /** + * @ticket 62716 + */ + public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block() { + $expected = '' . + self::$post->post_content . + ''; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post->post_content, + self::$post, + 'insert_hooked_blocks' + ); + $this->assertSame( $expected, $actual ); + } + + /** + * @ticket 62716 + */ + public function test_apply_block_hooks_to_content_from_post_object_respects_ignored_hooked_blocks_post_meta() { + $expected = self::$post_with_ignored_hooked_block->post_content . ''; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post_with_ignored_hooked_block->post_content, + self::$post_with_ignored_hooked_block, + 'insert_hooked_blocks' + ); + $this->assertSame( $expected, $actual ); + } +} From 0d64b6051724844beafb568a270903b810fe9a1d Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 17 Feb 2025 23:11:42 +0100 Subject: [PATCH 4/8] Update insert_hooked_blocks_into_rest_response --- src/wp-includes/blocks.php | 43 ++++++++------------------------------ 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 718f8d8b5759c..d63989a50136f 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1365,57 +1365,32 @@ function insert_hooked_blocks_into_rest_response( $response, $post ) { return $response; } - $attributes = array(); - $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); - if ( ! empty( $ignored_hooked_blocks ) ) { - $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); - $attributes['metadata'] = array( - 'ignoredHookedBlocks' => $ignored_hooked_blocks, - ); - } - - if ( 'wp_navigation' === $post->post_type ) { - $wrapper_block_type = 'core/navigation'; - } elseif ( 'wp_block' === $post->post_type ) { - $wrapper_block_type = 'core/block'; - } else { - $wrapper_block_type = 'core/post-content'; - } - - $content = get_comment_delimited_block_content( - $wrapper_block_type, - $attributes, - $response->data['content']['raw'] - ); - - $content = apply_block_hooks_to_content( - $content, + $response->data['content']['raw'] = apply_block_hooks_to_content_from_post_object( + $response->data['content']['raw'], $post, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); - // Remove mock block wrapper. - $content = remove_serialized_parent_block( $content ); - - $response->data['content']['raw'] = $content; - // If the rendered content was previously empty, we leave it like that. if ( empty( $response->data['content']['rendered'] ) ) { return $response; } // `apply_block_hooks_to_content` is called above. Ensure it is not called again as a filter. - $priority = has_filter( 'the_content', 'apply_block_hooks_to_content' ); + $priority = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' ); if ( false !== $priority ) { - remove_filter( 'the_content', 'apply_block_hooks_to_content', $priority ); + remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority ); } /** This filter is documented in wp-includes/post-template.php */ - $response->data['content']['rendered'] = apply_filters( 'the_content', $content ); + $response->data['content']['rendered'] = apply_filters( + 'the_content', + $response->data['content']['raw'] + ); // Restore the filter if it was set initially. if ( false !== $priority ) { - add_filter( 'the_content', 'apply_block_hooks_to_content', $priority ); + add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority ); } return $response; From 097d0c668a681f952b00579567a96df5cdab73a7 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Tue, 18 Feb 2025 14:02:36 +0100 Subject: [PATCH 5/8] Add test coverage for non-block content --- ...applyBlockHooksToContentFromPostObject.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php index b8961cf5a9ec5..2574543936f74 100644 --- a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php +++ b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php @@ -27,6 +27,13 @@ class Tests_Blocks_ApplyBlockHooksToContentFromPostObject extends WP_UnitTestCas */ protected static $post_with_ignored_hooked_block; + /** + * Post object. + * + * @var WP_Post + */ + protected static $post_with_non_block_content; + /** * * Set up. @@ -55,6 +62,15 @@ public static function wpSetUpBeforeClass() { ) ); + self::$post_with_non_block_content = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_content' => '

Hello World!

', + ) + ); + register_block_type( 'tests/hooked-block', array( @@ -113,4 +129,16 @@ public function test_apply_block_hooks_to_content_from_post_object_respects_igno ); $this->assertSame( $expected, $actual ); } + + /** + * @ticket 62716 + */ + public function test_apply_block_hooks_to_content_from_post_object_preserves_non_block_content() { + $actual = apply_block_hooks_to_content_from_post_object( + self::$post_with_non_block_content->post_content, + self::$post_with_non_block_content, + 'insert_hooked_blocks' + ); + $this->assertSame( self::$post_with_non_block_content->post_content, $actual ); + } } From 970eef515bb910bf8c68a1414a8637263a67763b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 19 Feb 2025 11:01:00 +0100 Subject: [PATCH 6/8] Enable first/last child insertion next to Classic block --- src/wp-includes/blocks.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index d63989a50136f..a932b29c94039 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1170,6 +1170,25 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post return apply_block_hooks_to_content( $content, $post, $callback ); } + /* + * If the content was created using the classic editor or using a single Classic block + * (`core/freeform`), it might not contain any block markup at all. + * However, we still might need to inject hooked blocks in the first child or last child + * positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap + * the content in a `core/freeform` wrapper block. + */ + if ( ! has_blocks( $content ) ) { + $original_content = $content; + + $content_wrapped_in_classic_block = get_comment_delimited_block_content( + 'core/freeform', + array(), + $content + ); + + $content = $content_wrapped_in_classic_block; + } + $attributes = array(); // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta. @@ -1205,6 +1224,17 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post // Finally, we need to remove the temporary wrapper block. $content = remove_serialized_parent_block( $content ); + // If we wrapped the content in a `core/freeform` block, we also need to remove that. + if ( ! empty( $content_wrapped_in_classic_block ) ) { + /* + * We cannot simply use remove_serialized_parent_block() here, + * as that function assumes that the block wrapper is at the top level. + * However, there might now be a hooked block inserted next to it + * (as first or last child of the parent). + */ + $content = str_replace( $content_wrapped_in_classic_block, $original_content, $content ); + } + return $content; } From 0ba659c1e0d15e7f41d79e1b8e711c1d1fb21e16 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 19 Feb 2025 11:17:14 +0100 Subject: [PATCH 7/8] Update test --- .../blocks/applyBlockHooksToContentFromPostObject.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php index 2574543936f74..df9d560ac9139 100644 --- a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php +++ b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php @@ -133,12 +133,13 @@ public function test_apply_block_hooks_to_content_from_post_object_respects_igno /** * @ticket 62716 */ - public function test_apply_block_hooks_to_content_from_post_object_preserves_non_block_content() { - $actual = apply_block_hooks_to_content_from_post_object( + public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block_if_content_contains_no_blocks() { + $expected = '' . self::$post_with_non_block_content->post_content; + $actual = apply_block_hooks_to_content_from_post_object( self::$post_with_non_block_content->post_content, self::$post_with_non_block_content, 'insert_hooked_blocks' ); - $this->assertSame( self::$post_with_non_block_content->post_content, $actual ); + $this->assertSame( $expected, $actual ); } } From e3ec0106f43bc22274c7ff017f3d28fd6326140e Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 19 Feb 2025 15:27:54 +0100 Subject: [PATCH 8/8] Change comment format --- src/wp-includes/blocks.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index a932b29c94039..3268983534566 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1200,10 +1200,12 @@ function apply_block_hooks_to_content_from_post_object( $content, WP_Post $post ); } - // We need to wrap the content in a temporary wrapper block with that metadata - // so the Block Hooks algorithm can insert blocks that are hooked as first or last child - // of the wrapper block. - // To that end, we need to determine the wrapper block type based on the post type. + /* + * We need to wrap the content in a temporary wrapper block with that metadata + * so the Block Hooks algorithm can insert blocks that are hooked as first or last child + * of the wrapper block. + * To that end, we need to determine the wrapper block type based on the post type. + */ if ( 'wp_navigation' === $post->post_type ) { $wrapper_block_type = 'core/navigation'; } elseif ( 'wp_block' === $post->post_type ) {