Skip to content

Commit

Permalink
Block Hooks: Add function to encapsulate wrapping in ad-hoc parent.
Browse files Browse the repository at this point in the history
Introduce a new function, `apply_block_hooks_to_content_from_post_object`, to colocate the logic used to temporarily wrap content in a parent block (with `ignoredHookedBlocks` information fetched from post meta) alongside the call to `apply_block_hooks_to_content`. Fetching that information from post meta is required for all block types that get their content from post objects, i.e. Post Content, Synced Pattern, and Navigation blocks.

Additionally, the newly introduced function contains logic to ensure that insertion of a hooked block into the `first_child` or `last_child` position of a given Post Content block works, even if that block only contains "classic" markup (i.e. no blocks).

Props bernhard-reiter, gziolo, mamaduka.
Fixes #61074, #62716.

git-svn-id: https://develop.svn.wordpress.org/trunk@59838 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
ockham committed Feb 19, 2025
1 parent 95c00d0 commit d455334
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 35 deletions.
143 changes: 109 additions & 34 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,106 @@ 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 );
}

/*
* 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.
$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 );

// 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;
}

/**
* Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks.
*
Expand Down Expand Up @@ -1297,57 +1397,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;
Expand Down
2 changes: 1 addition & 1 deletion src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
145 changes: 145 additions & 0 deletions tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php
/**
* Tests for the apply_block_hooks_to_content_from_post_object function.
*
* @package WordPress
* @subpackage Blocks
*
* @since 6.8.0
*
* @group blocks
* @group block-hooks
*
* @covers ::apply_block_hooks_to_content_from_post_object
*/
class Tests_Blocks_ApplyBlockHooksToContentFromPostObject extends WP_UnitTestCase {
/**
* Post object.
*
* @var WP_Post
*/
protected static $post;

/**
* Post object.
*
* @var WP_Post
*/
protected static $post_with_ignored_hooked_block;

/**
* Post object.
*
* @var WP_Post
*/
protected static $post_with_non_block_content;

/**
*
* Set up.
*
* @ticket 62716
*/
public static function wpSetUpBeforeClass() {
self::$post = self::factory()->post->create_and_get(
array(
'post_type' => 'post',
'post_status' => 'publish',
'post_title' => 'Test Post',
'post_content' => '<!-- wp:heading {"level":1} --><h1>Hello World!</h1><!-- /wp:heading -->',
)
);

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' => '<!-- wp:heading {"level":1} --><h1>Hello World!</h1><!-- /wp:heading -->',
'meta_input' => array(
'_wp_ignored_hooked_blocks' => '["tests/hooked-block-first-child"]',
),
)
);

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' => '<h1>Hello World!</h1>',
)
);

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 = '<!-- wp:tests/hooked-block-first-child /-->' .
self::$post->post_content .
'<!-- wp:tests/hooked-block /-->';
$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 . '<!-- wp:tests/hooked-block /-->';
$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 );
}

/**
* @ticket 62716
*/
public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block_if_content_contains_no_blocks() {
$expected = '<!-- wp:tests/hooked-block-first-child /-->' . 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( $expected, $actual );
}
}

0 comments on commit d455334

Please sign in to comment.