Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Hooks: Set ignoredHookedBlocks metadata upon saving #6087

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b5aaa44
Don't set ignoredHookedBlocks metadata upon read
ockham Feb 12, 2024
2e28810
Don't pass anchor_block by reference
ockham Feb 12, 2024
40fa3fb
Update PHPDoc
ockham Feb 12, 2024
1f0d087
Add new set_ignored_hooked_blocks_metadata() function
ockham Feb 12, 2024
c434d75
Whitespace
ockham Feb 12, 2024
b63d367
Fix first batch of unit tests
ockham Feb 12, 2024
36c0fc3
Fix remaining unit tests
ockham Feb 12, 2024
2358241
There can never be enough callbacks
ockham Feb 12, 2024
0330473
Tweak set_ignored_hooked_blocks_metadata to match callback signature
ockham Feb 12, 2024
e6eea6e
Wire it all up
ockham Feb 12, 2024
eb1daa1
Whitespace
ockham Feb 13, 2024
d057ece
Bail early
ockham Feb 13, 2024
ab6a8c9
Actually update post
ockham Feb 13, 2024
998147f
Use correct action
ockham Feb 13, 2024
1296344
Add action for template parts
ockham Feb 13, 2024
27724fc
Add note on action vs filter
ockham Feb 13, 2024
012004b
Clarify comment
ockham Feb 13, 2024
27a2ec8
Fix more unit tests
ockham Feb 13, 2024
12ccd0d
Start adding test coverage for set_ignored_hooked_blocks_metadata
ockham Feb 13, 2024
1128b5f
Add more test coverage and a small fix
ockham Feb 13, 2024
5817b49
Add test coverage for hooked block added by filter
ockham Feb 13, 2024
60b2732
Add test to cover context-aware filter
ockham Feb 13, 2024
61847cb
Remove obsolete comment about post_filtered_content
ockham Feb 13, 2024
712260f
Move set_ignored_hooked_blocks_metadata function definition below ins…
ockham Feb 13, 2024
7c7f01e
Inline get_hooked_block_markup
ockham Feb 13, 2024
f4f856b
Coding Standards in test
ockham Feb 13, 2024
7d43f21
Add test coverage to verify hooked blocks aren't added if ignored
ockham Feb 13, 2024
3c9aa4b
Move set_ignored_hooked_blocks_metadata_upon_rest_insert to block-tem…
ockham Feb 13, 2024
51832fe
Rename to inject_ignored_hooked_blocks_metadata_attributes
ockham Feb 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/wp-includes/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -1432,3 +1432,39 @@ function get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '
$template_hierarchy[] = 'index';
return $template_hierarchy;
}
/**
* Inject ignoredHookedBlocks metadata attributes into a template or template part.
*
* Given a `wp_template` or `wp_template_part` post object, locate all blocks that have
* hooked blocks, and inject a `metadata.ignoredHookedBlocks` attribute into the anchor
* blocks to reflect the latter.
*
* @param WP_Post $post A post object with post type set to `wp_template` or `wp_template_part`.
* @return WP_Post The updated post object.
*/
function inject_ignored_hooked_blocks_metadata_attributes( $post ) {
$hooked_blocks = get_hooked_blocks();
if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) {
return;
}

// At this point, the post has already been created.
// We need to build the corresponding `WP_Block_Template` object as context argument for the visitor.
// To that end, we need to suppress hooked blocks from getting inserted into the template.
add_filter( 'hooked_block_types', '__return_empty_array', 99999, 0 );
$template = _build_block_template_result_from_post( $post );
remove_filter( 'hooked_block_types', '__return_empty_array', 99999 );

$before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, 'set_ignored_hooked_blocks_metadata' );
$after_block_visitor = make_after_block_visitor( $hooked_blocks, $template, 'set_ignored_hooked_blocks_metadata' );

$blocks = parse_blocks( $template->content );
$content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );

wp_update_post(
array(
'ID' => $post->ID,
'post_content' => $content,
)
);
}
125 changes: 77 additions & 48 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -850,44 +850,6 @@ function get_hooked_blocks() {
return $hooked_blocks;
}

/**
* Conditionally returns the markup for a given hooked block.
*
* Accepts three arguments: A hooked block, its type, and a reference to an anchor block.
* If the anchor block has already been processed, and the given hooked block type is in the list
* of ignored hooked blocks, an empty string is returned.
*
* The hooked block type is specified separately as it's possible that a filter might've modified
* the hooked block such that `$hooked_block['blockName']` does no longer reflect the original type.
*
* This function is meant for internal use only.
*
* @since 6.5.0
* @access private
*
* @param array $hooked_block The hooked block, represented as a parsed block array.
* @param string $hooked_block_type The type of the hooked block. This could be different from
* $hooked_block['blockName'], as a filter might've modified the latter.
* @param array $anchor_block The anchor block, represented as a parsed block array.
* Passed by reference.
* @return string The markup for the given hooked block, or an empty string if the block is ignored.
*/
function get_hooked_block_markup( $hooked_block, $hooked_block_type, &$anchor_block ) {
if ( ! isset( $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) ) {
$anchor_block['attrs']['metadata']['ignoredHookedBlocks'] = array();
}

if ( in_array( $hooked_block_type, $anchor_block['attrs']['metadata']['ignoredHookedBlocks'], true ) ) {
return '';
}

// The following is only needed for the REST API endpoint.
// However, its presence does not affect the frontend.
$anchor_block['attrs']['metadata']['ignoredHookedBlocks'][] = $hooked_block_type;

return serialize_block( $hooked_block );
}

/**
* Returns the markup for blocks hooked to the given anchor block in a specific relative position.
*
Expand Down Expand Up @@ -946,13 +908,60 @@ function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooke
$parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $relative_position, $parsed_anchor_block, $context );

// It's possible that the `hooked_block_{$hooked_block_type}` filter returned a block of a different type,
// so we need to pass the original $hooked_block_type as well.
$markup .= get_hooked_block_markup( $parsed_hooked_block, $hooked_block_type, $parsed_anchor_block );
// so we explicitly look for the original `$hooked_block_type` in the `ignoredHookedBlocks` metadata.
if (
! isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) ||
! in_array( $hooked_block_type, $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'], true )
) {
$markup .= serialize_block( $parsed_hooked_block );
}
}

return $markup;
}

/**
* Adds a list of hooked block types to an anchor block's ignored hooked block types.
*
* This function is meant for internal use only.
*
* @since 6.5.0
* @access private
*
* @param array $parsed_anchor_block The anchor block, in parsed block array format.
* @param string $relative_position The relative position of the hooked blocks.
* Can be one of 'before', 'after', 'first_child', or 'last_child'.
* @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position.
* @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to.
* @return string An empty string.
*/
function set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) {
$anchor_block_type = $parsed_anchor_block['blockName'];
$hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] )
? $hooked_blocks[ $anchor_block_type ][ $relative_position ]
: array();

/** This filter is documented in wp-includes/blocks.php */
$hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context );
if ( empty( $hooked_block_types ) ) {
return '';
}

$previously_ignored_hooked_blocks = isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] )
? $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks']
: array();

$parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] = array_unique(
array_merge(
$previously_ignored_hooked_blocks,
$hooked_block_types
)
);

// Markup for the hooked blocks has already been created (in `insert_hooked_blocks`).
return '';
}

/**
* Returns a function that injects the theme attribute into, and hooked blocks before, a given block.
*
Expand All @@ -963,15 +972,19 @@ function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooke
* This function is meant for internal use only.
*
* @since 6.4.0
* @since 6.5.0 Added $callback argument.
* @access private
*
* @param array $hooked_blocks An array of blocks hooked to another given block.
* @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object,
* or pattern that the blocks belong to.
* @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 callable A function that returns the serialized markup for the given block,
* including the markup for any hooked blocks before it.
*/
function make_before_block_visitor( $hooked_blocks, $context ) {
function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) {
/**
* Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup.
*
Expand All @@ -984,17 +997,23 @@ function make_before_block_visitor( $hooked_blocks, $context ) {
* @param array $prev The previous sibling block of the given block. Default null.
* @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it.
*/
return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context ) {
return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) {
_inject_theme_attribute_in_template_part_block( $block );

$markup = '';

if ( $parent_block && ! $prev ) {
// Candidate for first-child insertion.
$markup .= insert_hooked_blocks( $parent_block, 'first_child', $hooked_blocks, $context );
$markup .= call_user_func_array(
$callback,
array( &$parent_block, 'first_child', $hooked_blocks, $context )
);
}

$markup .= insert_hooked_blocks( $block, 'before', $hooked_blocks, $context );
$markup .= call_user_func_array(
$callback,
array( &$block, 'before', $hooked_blocks, $context )
);

return $markup;
};
Expand All @@ -1010,15 +1029,19 @@ function make_before_block_visitor( $hooked_blocks, $context ) {
* This function is meant for internal use only.
*
* @since 6.4.0
* @since 6.5.0 Added $callback argument.
* @access private
*
* @param array $hooked_blocks An array of blocks hooked to another block.
* @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object,
* or pattern that the blocks belong to.
* @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 callable A function that returns the serialized markup for the given block,
* including the markup for any hooked blocks after it.
*/
function make_after_block_visitor( $hooked_blocks, $context ) {
function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) {
/**
* Injects hooked blocks after the given block, and returns the serialized markup.
*
Expand All @@ -1030,12 +1053,18 @@ function make_after_block_visitor( $hooked_blocks, $context ) {
* @param array $next The next sibling block of the given block. Default null.
* @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it.
*/
return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context ) {
$markup = insert_hooked_blocks( $block, 'after', $hooked_blocks, $context );
return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context, $callback ) {
$markup = call_user_func_array(
$callback,
array( &$block, 'after', $hooked_blocks, $context )
);

if ( $parent_block && ! $next ) {
// Candidate for last-child insertion.
$markup .= insert_hooked_blocks( $parent_block, 'last_child', $hooked_blocks, $context );
$markup .= call_user_func_array(
$callback,
array( &$parent_block, 'last_child', $hooked_blocks, $context )
);
}

return $markup;
Expand Down
5 changes: 5 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -752,4 +752,9 @@
add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 );
add_action( 'init', '_wp_register_default_font_collections' );

// It might be nice to use a filter instead of an action, but the `WP_REST_Templates_Controller` doesn't
// provide one (unlike e.g. `WP_REST_Posts_Controller`, which has `rest_pre_insert_{$this->post_type}`).
add_action( 'rest_after_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 3 );
add_action( 'rest_after_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 3 );

unset( $filter, $action );
4 changes: 2 additions & 2 deletions tests/phpunit/tests/block-templates/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
'post_type' => 'wp_template',
'post_name' => 'my_template',
'post_title' => 'My Template',
'post_content' => '<!-- wp:heading {"level":1} --><h1>Template</h1><!-- /wp:heading -->',
'post_content' => '<!-- wp:heading {"level":1,"metadata":{"ignoredHookedBlocks":["tests/ignored"]}} --><h1>Template</h1><!-- /wp:heading -->',
'post_excerpt' => 'Description of my template',
'tax_input' => array(
'wp_theme' => array(
Expand All @@ -57,7 +57,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
'post_type' => 'wp_template_part',
'post_name' => 'my_template_part',
'post_title' => 'My Template Part',
'post_content' => '<!-- wp:heading {"level":2} --><h2>Template Part</h2><!-- /wp:heading -->',
'post_content' => '<!-- wp:heading {"level":2,"metadata":{"ignoredHookedBlocks":["tests/ignored"]}} --><h2>Template Part</h2><!-- /wp:heading -->',
'post_excerpt' => 'Description of my template part',
'tax_input' => array(
'wp_theme' => array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public function tear_down() {
$registry->unregister( 'tests/my-block' );
}

if ( $registry->is_registered( 'tests/ignored' ) ) {
$registry->unregister( 'tests/ignored' );
}

parent::tear_down();
}

Expand Down Expand Up @@ -67,6 +71,7 @@ public function test_should_build_template_part() {

/**
* @ticket 59646
* @ticket 60506
*/
public function test_should_inject_hooked_block_into_template() {
register_block_type(
Expand All @@ -83,11 +88,11 @@ public function test_should_inject_hooked_block_into_template() {
'wp_template'
);
$this->assertStringStartsWith( '<!-- wp:tests/my-block /-->', $template->content );
$this->assertStringContainsString( '"metadata":{"ignoredHookedBlocks":["tests/my-block"]}', $template->content );
}

/**
* @ticket 59646
* @ticket 60506
*/
public function test_should_inject_hooked_block_into_template_part() {
register_block_type(
Expand All @@ -104,6 +109,47 @@ public function test_should_inject_hooked_block_into_template_part() {
'wp_template_part'
);
$this->assertStringEndsWith( '<!-- wp:tests/my-block /-->', $template_part->content );
$this->assertStringContainsString( '"metadata":{"ignoredHookedBlocks":["tests/my-block"]}', $template_part->content );
}

/**
* @ticket 59646
* @ticket 60506
*/
public function test_should_not_inject_ignored_hooked_block_into_template() {
register_block_type(
'tests/ignored',
array(
'block_hooks' => array(
'core/heading' => 'after',
),
)
);

$template = _build_block_template_result_from_post(
self::$template_post,
'wp_template'
);
$this->assertStringNotContainsString( '<!-- wp:tests/ignored /-->', $template->content );
}

/**
* @ticket 59646
* @ticket 60506
*/
public function test_should_not_inject_ignored_hooked_block_into_template_part() {
register_block_type(
'tests/ignored',
array(
'block_hooks' => array(
'core/heading' => 'after',
),
)
);

$template_part = _build_block_template_result_from_post(
self::$template_part_post,
'wp_template_part'
);
$this->assertStringNotContainsString( '<!-- wp:tests/ignored /-->', $template_part->content );
}
}
Loading
Loading