Skip to content

Commit

Permalink
Block Hooks: Pass correct context to filters.
Browse files Browse the repository at this point in the history
The `$context` argument passed to filters such as `hooked_block_types`, `hooked_block`, and `hooked_block_{$hooked_block_type}` allows them to conditionally insert a hooked block. If the anchor block is contained in a template or template part, `$context` will be set to a `WP_Block_Template` object reflecting that template or part.

The aforementioned filters are applied when hooked block insertion is run upon reading a template (or part) from the DB (and before sending the template/part content with hooked blocks inserted over the REST API to the client), but also upon writing to the DB, as that's when the `ignoredHookedBlocks` metadata attribute is set.

Prior to this changeset, the `$context` passed to Block Hooks related filters in the latter case reflected the template/part that was already stored in the database (if any), which is a bug; instead, it needs to reflect the template/part that will result from the incoming `POST` network request that will trigger a database update.

Those incoming changes are encapsulated in the `$changes` argument passed to the `reset_pre_insert_template` and  `reset_pre_insert_template_part` filters, respectively, and thus to the `inject_ignored_hooked_blocks_metadata_attributes` function that is hooked to them. `$changes` is of type `stdClass` and only contains the fields that need to be updated. That means that in order to create a `WP_Block_Template` object, a two-step process is needed:

- Emulate what the updated `wp_template` or `wp_template_part` post object in the database will look like by merging `$changes` on top of the existing `$post` object fetched from the DB, or from the theme's block template (part) file, if any.
- Create a `WP_Block_Template` from the resulting object.

To achieve the latter, a new helper method (`_build_block_template_object_from_post_object`) is extracted from the existing `_build_block_template_result_from_post` function. (The latter cannot be used directly as it includes a few database calls that will fail if no post object for the template has existed yet in the database.)

While somewhat complicated to implement, the overall change allows for better separation of concerns and isolation of entities. This is visible e.g. in the fact that `inject_ignored_hooked_blocks_metadata_attributes` no longer requires a `$request` argument, which is reflected by unit tests no longer needing to create a `$request` object to pass to it, thus decoupling the function from the templates endpoint controller.

Unit tests for `inject_ignored_hooked_blocks_metadata_attributes` have been moved to a new, separate file. Test coverage has been added such that now, all three relevant scenarios are covered:

- The template doesn't exist in the DB, nor is there a block theme template file for it.
- The template doesn't exist in the DB, but there is a block theme template file for it.
- The template already exists in the DB.

Those scenarios also correspond to the logical branching inside `WP_REST_Templates_Controller::prepare_item_for_database`, which is where `inject_ignored_hooked_blocks_metadata_attributes` gets its data from.

Props tomjcafferkey, bernhard-reiter, gziolo, swissspidy.
Fixes #60754.

git-svn-id: https://develop.svn.wordpress.org/trunk@57919 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
ockham committed Apr 3, 2024
1 parent f158f6d commit aa04408
Show file tree
Hide file tree
Showing 6 changed files with 588 additions and 137 deletions.
177 changes: 126 additions & 51 deletions src/wp-includes/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -725,74 +725,115 @@ function _wp_build_title_and_description_for_taxonomy_block_template( $taxonomy,
}

/**
* Builds a unified template object based a post Object.
* Builds a block template object from a post object.
*
* @since 5.9.0
* @since 6.3.0 Added `modified` property to template objects.
* @since 6.4.0 Added support for a revision post to be passed to this function.
* This is a helper function that creates a block template object from a given post object.
* It is self-sufficient in that it only uses information passed as arguments; it does not
* query the database for additional information.
*
* @since 6.5.1
* @access private
*
* @param WP_Post $post Template post.
* @param WP_Post $post Template post.
* @param array $terms Additional terms to inform the template object.
* @param array $meta Additional meta fields to inform the template object.
* @return WP_Block_Template|WP_Error Template or error object.
*/
function _build_block_template_result_from_post( $post ) {
$default_template_types = get_default_block_template_types();

$post_id = wp_is_post_revision( $post );
if ( ! $post_id ) {
$post_id = $post;
}
$parent_post = get_post( $post_id );

$terms = get_the_terms( $parent_post, 'wp_theme' );

if ( is_wp_error( $terms ) ) {
return $terms;
}

if ( ! $terms ) {
function _build_block_template_object_from_post_object( $post, $terms = array(), $meta = array() ) {
if ( empty( $terms['wp_theme'] ) ) {
return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
}
$theme = $terms['wp_theme'];

$default_template_types = get_default_block_template_types();

$theme = $terms[0]->name;
$template_file = _get_block_template_file( $post->post_type, $post->post_name );
$has_theme_file = get_stylesheet() === $theme && null !== $template_file;

$origin = get_post_meta( $parent_post->ID, 'origin', true );
$is_wp_suggestion = get_post_meta( $parent_post->ID, 'is_wp_suggestion', true );

$template = new WP_Block_Template();
$template->wp_id = $post->ID;
$template->id = $theme . '//' . $parent_post->post_name;
$template->id = $theme . '//' . $post->post_name;
$template->theme = $theme;
$template->content = $post->post_content;
$template->slug = $post->post_name;
$template->source = 'custom';
$template->origin = ! empty( $origin ) ? $origin : null;
$template->origin = ! empty( $meta['origin'] ) ? $meta['origin'] : null;
$template->type = $post->post_type;
$template->description = $post->post_excerpt;
$template->title = $post->post_title;
$template->status = $post->post_status;
$template->has_theme_file = $has_theme_file;
$template->is_custom = empty( $is_wp_suggestion );
$template->is_custom = empty( $meta['is_wp_suggestion'] );
$template->author = $post->post_author;
$template->modified = $post->post_modified;

if ( 'wp_template' === $parent_post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) {
if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) {
$template->post_types = $template_file['postTypes'];
}

if ( 'wp_template' === $parent_post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
$template->is_custom = false;
}

if ( 'wp_template_part' === $post->post_type && isset( $terms['wp_template_part_area'] ) ) {
$template->area = $terms['wp_template_part_area'];
}

return $template;
}

/**
* Builds a unified template object based a post Object.
*
* @since 5.9.0
* @since 6.3.0 Added `modified` property to template objects.
* @since 6.4.0 Added support for a revision post to be passed to this function.
* @access private
*
* @param WP_Post $post Template post.
* @return WP_Block_Template|WP_Error Template or error object.
*/
function _build_block_template_result_from_post( $post ) {
$post_id = wp_is_post_revision( $post );
if ( ! $post_id ) {
$post_id = $post;
}
$parent_post = get_post( $post_id );
$post->post_name = $parent_post->post_name;
$post->post_type = $parent_post->post_type;

$terms = get_the_terms( $parent_post, 'wp_theme' );

if ( is_wp_error( $terms ) ) {
return $terms;
}

if ( ! $terms ) {
return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
}

$terms = array(
'wp_theme' => $terms[0]->name,
);

if ( 'wp_template_part' === $parent_post->post_type ) {
$type_terms = get_the_terms( $parent_post, 'wp_template_part_area' );
if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
$template->area = $type_terms[0]->name;
$terms['wp_template_part_area'] = $type_terms[0]->name;
}
}

$meta = array(
'origin' => get_post_meta( $parent_post->ID, 'origin', true ),
'is_wp_suggestion' => get_post_meta( $parent_post->ID, 'is_wp_suggestion', true ),
);

$template = _build_block_template_object_from_post_object( $post, $terms, $meta );

if ( is_wp_error( $template ) ) {
return $template;
}

// Check for a block template without a description and title or with a title equal to the slug.
if ( 'wp_template' === $parent_post->post_type && empty( $template->description ) && ( empty( $template->title ) || $template->title === $template->slug ) ) {
$matches = array();
Expand Down Expand Up @@ -1444,36 +1485,70 @@ function get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '
* @since 6.5.0
* @access private
*
* @param stdClass $post An object representing a template or template part
* prepared for inserting or updating the database.
* @param WP_REST_Request $request Request object.
* @return stdClass The updated object representing a template or template part.
* @param stdClass $changes An object representing a template or template part
* prepared for inserting or updating the database.
* @param WP_REST_Request $deprecated Deprecated. Not used.
* @return stdClass|WP_Error The updated object representing a template or template part.
*/
function inject_ignored_hooked_blocks_metadata_attributes( $post, $request ) {
$filter_name = current_filter();
if ( ! str_starts_with( $filter_name, 'rest_pre_insert_' ) ) {
return $post;
function inject_ignored_hooked_blocks_metadata_attributes( $changes, $deprecated = null ) {
if ( null !== $deprecated ) {
_deprecated_argument( __FUNCTION__, '6.5.1' );
}
$post_type = str_replace( 'rest_pre_insert_', '', $filter_name );

$hooked_blocks = get_hooked_blocks();
if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) {
return $post;
return $changes;
}

// 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 = $request['id'] ? get_block_template( $request['id'], $post_type ) : null;
remove_filter( 'hooked_block_types', '__return_empty_array', 99999 );
$meta = isset( $changes->meta_input ) ? $changes->meta_input : array();
$terms = isset( $changes->tax_input ) ? $changes->tax_input : array();

if ( empty( $changes->ID ) ) {
// There's no post object for this template in the database for this template yet.
$post = $changes;
} else {
// Find the existing post object.
$post = get_post( $changes->ID );

// If the post is a revision, use the parent post's post_name and post_type.
$post_id = wp_is_post_revision( $post );
if ( $post_id ) {
$parent_post = get_post( $post_id );
$post->post_name = $parent_post->post_name;
$post->post_type = $parent_post->post_type;
}

// Apply the changes to the existing post object.
$post = (object) array_merge( (array) $post, (array) $changes );

$type_terms = get_the_terms( $changes->ID, 'wp_theme' );
$terms['wp_theme'] = ! is_wp_error( $type_terms ) && ! empty( $type_terms ) ? $type_terms[0]->name : null;
}

// Required for the WP_Block_Template. Update the post object with the current time.
$post->post_modified = current_time( 'mysql' );

// If the post_author is empty, set it to the current user.
if ( empty( $post->post_author ) ) {
$post->post_author = get_current_user_id();
}

if ( 'wp_template_part' === $post->post_type && ! isset( $terms['wp_template_part_area'] ) ) {
$area_terms = get_the_terms( $changes->ID, 'wp_template_part_area' );
$terms['wp_template_part_area'] = ! is_wp_error( $area_terms ) && ! empty( $area_terms ) ? $area_terms[0]->name : null;
}

$template = _build_block_template_object_from_post_object( new WP_Post( $post ), $terms, $meta );

if ( is_wp_error( $template ) ) {
return $template;
}

$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( $post->post_content );
$content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
$blocks = parse_blocks( $changes->post_content );
$changes->post_content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );

$post->post_content = $content;
return $post;
return $changes;
}
4 changes: 2 additions & 2 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@
add_action( 'init', '_wp_register_default_font_collections' );

// Add ignoredHookedBlocks metadata attribute to the template and template part post types.
add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 2 );
add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 2 );
add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' );
add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' );

unset( $filter, $action );
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ public function delete_item( $request ) {
* @since 5.8.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass Changes to pass to wp_update_post.
* @return stdClass|WP_Error Changes to pass to wp_update_post.
*/
protected function prepare_item_for_database( $request ) {
$template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null;
Expand Down
68 changes: 0 additions & 68 deletions tests/phpunit/tests/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,72 +403,4 @@ public function test_wp_generate_block_templates_export_file() {
}
$this->assertTrue( $has_html_files, 'contains at least one html file' );
}

/**
* @ticket 60671
*
* @covers inject_ignored_hooked_blocks_metadata_attributes
*/
public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template() {
global $wp_current_filter;
// Mock currently set filter. The $wp_current_filter global is reset during teardown by
// WP_UnitTestCase_Base::_restore_hooks() in tests/phpunit/includes/abstract-testcase.php.
$wp_current_filter[] = 'rest_pre_insert_wp_template';

register_block_type(
'tests/hooked-block',
array(
'block_hooks' => array(
'tests/anchor-block' => 'after',
),
)
);

$id = self::TEST_THEME . '//' . 'my_template';
$request = new WP_REST_Request( 'POST', '/wp/v2/templates/' . $id );

$changes = new stdClass();
$changes->post_content = '<!-- wp:tests/anchor-block -->Hello<!-- /wp:tests/anchor-block -->';

$post = inject_ignored_hooked_blocks_metadata_attributes( $changes, $request );
$this->assertSame(
'<!-- wp:tests/anchor-block {"metadata":{"ignoredHookedBlocks":["tests/hooked-block"]}} -->Hello<!-- /wp:tests/anchor-block -->',
$post->post_content,
'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.'
);
}

/**
* @ticket 60671
*
* @covers inject_ignored_hooked_blocks_metadata_attributes
*/
public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_part() {
global $wp_current_filter;
// Mock currently set filter. The $wp_current_filter global is reset during teardown by
// WP_UnitTestCase_Base::_restore_hooks() in tests/phpunit/includes/abstract-testcase.php.
$wp_current_filter[] = 'rest_pre_insert_wp_template_part';

register_block_type(
'tests/hooked-block',
array(
'block_hooks' => array(
'tests/anchor-block' => 'after',
),
)
);

$id = self::TEST_THEME . '//' . 'my_template_part';
$request = new WP_REST_Request( 'POST', '/wp/v2/template-parts/' . $id );

$changes = new stdClass();
$changes->post_content = '<!-- wp:tests/anchor-block -->Hello<!-- /wp:tests/anchor-block -->';

$post = inject_ignored_hooked_blocks_metadata_attributes( $changes, $request );
$this->assertSame(
'<!-- wp:tests/anchor-block {"metadata":{"ignoredHookedBlocks":["tests/hooked-block"]}} -->Hello<!-- /wp:tests/anchor-block -->',
$post->post_content,
'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.'
);
}
}

0 comments on commit aa04408

Please sign in to comment.