Make WordPress Core

Changeset 57919


Ignore:
Timestamp:
04/03/2024 03:09:38 PM (3 months ago)
Author:
Bernhard Reiter
Message:

Block Hooks: Pass correct context to filters.

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.

Location:
trunk
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-template-utils.php

    r57790 r57919  
    726726
    727727/**
    728  * Builds a unified template object based a post Object.
    729  *
    730  * @since 5.9.0
    731  * @since 6.3.0 Added `modified` property to template objects.
    732  * @since 6.4.0 Added support for a revision post to be passed to this function.
     728 * Builds a block template object from a post object.
     729 *
     730 * This is a helper function that creates a block template object from a given post object.
     731 * It is self-sufficient in that it only uses information passed as arguments; it does not
     732 * query the database for additional information.
     733 *
     734 * @since 6.5.1
    733735 * @access private
    734736 *
    735  * @param WP_Post $post Template post.
     737 * @param WP_Post $post  Template post.
     738 * @param array   $terms Additional terms to inform the template object.
     739 * @param array   $meta  Additional meta fields to inform the template object.
    736740 * @return WP_Block_Template|WP_Error Template or error object.
    737741 */
    738 function _build_block_template_result_from_post( $post ) {
     742function _build_block_template_object_from_post_object( $post, $terms = array(), $meta = array() ) {
     743    if ( empty( $terms['wp_theme'] ) ) {
     744        return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
     745    }
     746    $theme = $terms['wp_theme'];
     747
    739748    $default_template_types = get_default_block_template_types();
    740749
    741     $post_id = wp_is_post_revision( $post );
    742     if ( ! $post_id ) {
    743         $post_id = $post;
    744     }
    745     $parent_post = get_post( $post_id );
    746 
    747     $terms = get_the_terms( $parent_post, 'wp_theme' );
    748 
    749     if ( is_wp_error( $terms ) ) {
    750         return $terms;
    751     }
    752 
    753     if ( ! $terms ) {
    754         return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
    755     }
    756 
    757     $theme          = $terms[0]->name;
    758750    $template_file  = _get_block_template_file( $post->post_type, $post->post_name );
    759751    $has_theme_file = get_stylesheet() === $theme && null !== $template_file;
    760752
    761     $origin           = get_post_meta( $parent_post->ID, 'origin', true );
    762     $is_wp_suggestion = get_post_meta( $parent_post->ID, 'is_wp_suggestion', true );
    763 
    764753    $template                 = new WP_Block_Template();
    765754    $template->wp_id          = $post->ID;
    766     $template->id             = $theme . '//' . $parent_post->post_name;
     755    $template->id             = $theme . '//' . $post->post_name;
    767756    $template->theme          = $theme;
    768757    $template->content        = $post->post_content;
    769758    $template->slug           = $post->post_name;
    770759    $template->source         = 'custom';
    771     $template->origin         = ! empty( $origin ) ? $origin : null;
     760    $template->origin         = ! empty( $meta['origin'] ) ? $meta['origin'] : null;
    772761    $template->type           = $post->post_type;
    773762    $template->description    = $post->post_excerpt;
     
    775764    $template->status         = $post->post_status;
    776765    $template->has_theme_file = $has_theme_file;
    777     $template->is_custom      = empty( $is_wp_suggestion );
     766    $template->is_custom      = empty( $meta['is_wp_suggestion'] );
    778767    $template->author         = $post->post_author;
    779768    $template->modified       = $post->post_modified;
    780769
    781     if ( 'wp_template' === $parent_post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) {
     770    if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) {
    782771        $template->post_types = $template_file['postTypes'];
    783772    }
    784773
    785     if ( 'wp_template' === $parent_post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
     774    if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
    786775        $template->is_custom = false;
    787776    }
     777
     778    if ( 'wp_template_part' === $post->post_type && isset( $terms['wp_template_part_area'] ) ) {
     779        $template->area = $terms['wp_template_part_area'];
     780    }
     781
     782    return $template;
     783}
     784
     785/**
     786 * Builds a unified template object based a post Object.
     787 *
     788 * @since 5.9.0
     789 * @since 6.3.0 Added `modified` property to template objects.
     790 * @since 6.4.0 Added support for a revision post to be passed to this function.
     791 * @access private
     792 *
     793 * @param WP_Post $post Template post.
     794 * @return WP_Block_Template|WP_Error Template or error object.
     795 */
     796function _build_block_template_result_from_post( $post ) {
     797    $post_id = wp_is_post_revision( $post );
     798    if ( ! $post_id ) {
     799        $post_id = $post;
     800    }
     801    $parent_post     = get_post( $post_id );
     802    $post->post_name = $parent_post->post_name;
     803    $post->post_type = $parent_post->post_type;
     804
     805    $terms = get_the_terms( $parent_post, 'wp_theme' );
     806
     807    if ( is_wp_error( $terms ) ) {
     808        return $terms;
     809    }
     810
     811    if ( ! $terms ) {
     812        return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
     813    }
     814
     815    $terms = array(
     816        'wp_theme' => $terms[0]->name,
     817    );
    788818
    789819    if ( 'wp_template_part' === $parent_post->post_type ) {
    790820        $type_terms = get_the_terms( $parent_post, 'wp_template_part_area' );
    791821        if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
    792             $template->area = $type_terms[0]->name;
    793         }
     822            $terms['wp_template_part_area'] = $type_terms[0]->name;
     823        }
     824    }
     825
     826    $meta = array(
     827        'origin'           => get_post_meta( $parent_post->ID, 'origin', true ),
     828        'is_wp_suggestion' => get_post_meta( $parent_post->ID, 'is_wp_suggestion', true ),
     829    );
     830
     831    $template = _build_block_template_object_from_post_object( $post, $terms, $meta );
     832
     833    if ( is_wp_error( $template ) ) {
     834        return $template;
    794835    }
    795836
     
    14451486 * @access private
    14461487 *
    1447  * @param stdClass        $post    An object representing a template or template part
    1448  *                                 prepared for inserting or updating the database.
    1449  * @param WP_REST_Request $request Request object.
    1450  * @return stdClass The updated object representing a template or template part.
    1451  */
    1452 function inject_ignored_hooked_blocks_metadata_attributes( $post, $request ) {
    1453     $filter_name = current_filter();
    1454     if ( ! str_starts_with( $filter_name, 'rest_pre_insert_' ) ) {
    1455         return $post;
    1456     }
    1457     $post_type = str_replace( 'rest_pre_insert_', '', $filter_name );
     1488 * @param stdClass        $changes    An object representing a template or template part
     1489 *                                    prepared for inserting or updating the database.
     1490 * @param WP_REST_Request $deprecated Deprecated. Not used.
     1491 * @return stdClass|WP_Error The updated object representing a template or template part.
     1492 */
     1493function inject_ignored_hooked_blocks_metadata_attributes( $changes, $deprecated = null ) {
     1494    if ( null !== $deprecated ) {
     1495        _deprecated_argument( __FUNCTION__, '6.5.1' );
     1496    }
    14581497
    14591498    $hooked_blocks = get_hooked_blocks();
    14601499    if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) {
    1461         return $post;
    1462     }
    1463 
    1464     // At this point, the post has already been created.
    1465     // We need to build the corresponding `WP_Block_Template` object as context argument for the visitor.
    1466     // To that end, we need to suppress hooked blocks from getting inserted into the template.
    1467     add_filter( 'hooked_block_types', '__return_empty_array', 99999, 0 );
    1468     $template = $request['id'] ? get_block_template( $request['id'], $post_type ) : null;
    1469     remove_filter( 'hooked_block_types', '__return_empty_array', 99999 );
     1500        return $changes;
     1501    }
     1502
     1503    $meta  = isset( $changes->meta_input ) ? $changes->meta_input : array();
     1504    $terms = isset( $changes->tax_input ) ? $changes->tax_input : array();
     1505
     1506    if ( empty( $changes->ID ) ) {
     1507        // There's no post object for this template in the database for this template yet.
     1508        $post = $changes;
     1509    } else {
     1510        // Find the existing post object.
     1511        $post = get_post( $changes->ID );
     1512
     1513        // If the post is a revision, use the parent post's post_name and post_type.
     1514        $post_id = wp_is_post_revision( $post );
     1515        if ( $post_id ) {
     1516            $parent_post     = get_post( $post_id );
     1517            $post->post_name = $parent_post->post_name;
     1518            $post->post_type = $parent_post->post_type;
     1519        }
     1520
     1521        // Apply the changes to the existing post object.
     1522        $post = (object) array_merge( (array) $post, (array) $changes );
     1523
     1524        $type_terms        = get_the_terms( $changes->ID, 'wp_theme' );
     1525        $terms['wp_theme'] = ! is_wp_error( $type_terms ) && ! empty( $type_terms ) ? $type_terms[0]->name : null;
     1526    }
     1527
     1528    // Required for the WP_Block_Template. Update the post object with the current time.
     1529    $post->post_modified = current_time( 'mysql' );
     1530
     1531    // If the post_author is empty, set it to the current user.
     1532    if ( empty( $post->post_author ) ) {
     1533        $post->post_author = get_current_user_id();
     1534    }
     1535
     1536    if ( 'wp_template_part' === $post->post_type && ! isset( $terms['wp_template_part_area'] ) ) {
     1537        $area_terms                     = get_the_terms( $changes->ID, 'wp_template_part_area' );
     1538        $terms['wp_template_part_area'] = ! is_wp_error( $area_terms ) && ! empty( $area_terms ) ? $area_terms[0]->name : null;
     1539    }
     1540
     1541    $template = _build_block_template_object_from_post_object( new WP_Post( $post ), $terms, $meta );
     1542
     1543    if ( is_wp_error( $template ) ) {
     1544        return $template;
     1545    }
    14701546
    14711547    $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, 'set_ignored_hooked_blocks_metadata' );
    14721548    $after_block_visitor  = make_after_block_visitor( $hooked_blocks, $template, 'set_ignored_hooked_blocks_metadata' );
    14731549
    1474     $blocks  = parse_blocks( $post->post_content );
    1475     $content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
    1476 
    1477     $post->post_content = $content;
    1478     return $post;
    1479 }
     1550    $blocks                = parse_blocks( $changes->post_content );
     1551    $changes->post_content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
     1552
     1553    return $changes;
     1554}
  • trunk/src/wp-includes/default-filters.php

    r57790 r57919  
    754754
    755755// Add ignoredHookedBlocks metadata attribute to the template and template part post types.
    756 add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 2 );
    757 add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 2 );
     756add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' );
     757add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' );
    758758
    759759unset( $filter, $action );
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php

    r57790 r57919  
    533533     *
    534534     * @param WP_REST_Request $request Request object.
    535      * @return stdClass Changes to pass to wp_update_post.
     535     * @return stdClass|WP_Error Changes to pass to wp_update_post.
    536536     */
    537537    protected function prepare_item_for_database( $request ) {
  • trunk/tests/phpunit/tests/block-template-utils.php

    r57799 r57919  
    404404        $this->assertTrue( $has_html_files, 'contains at least one html file' );
    405405    }
    406 
    407     /**
    408      * @ticket 60671
    409      *
    410      * @covers inject_ignored_hooked_blocks_metadata_attributes
    411      */
    412     public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template() {
    413         global $wp_current_filter;
    414         // Mock currently set filter. The $wp_current_filter global is reset during teardown by
    415         // WP_UnitTestCase_Base::_restore_hooks() in tests/phpunit/includes/abstract-testcase.php.
    416         $wp_current_filter[] = 'rest_pre_insert_wp_template';
    417 
    418         register_block_type(
    419             'tests/hooked-block',
    420             array(
    421                 'block_hooks' => array(
    422                     'tests/anchor-block' => 'after',
    423                 ),
    424             )
    425         );
    426 
    427         $id      = self::TEST_THEME . '//' . 'my_template';
    428         $request = new WP_REST_Request( 'POST', '/wp/v2/templates/' . $id );
    429 
    430         $changes               = new stdClass();
    431         $changes->post_content = '<!-- wp:tests/anchor-block -->Hello<!-- /wp:tests/anchor-block -->';
    432 
    433         $post = inject_ignored_hooked_blocks_metadata_attributes( $changes, $request );
    434         $this->assertSame(
    435             '<!-- wp:tests/anchor-block {"metadata":{"ignoredHookedBlocks":["tests/hooked-block"]}} -->Hello<!-- /wp:tests/anchor-block -->',
    436             $post->post_content,
    437             'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.'
    438         );
    439     }
    440 
    441     /**
    442      * @ticket 60671
    443      *
    444      * @covers inject_ignored_hooked_blocks_metadata_attributes
    445      */
    446     public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_part() {
    447         global $wp_current_filter;
    448         // Mock currently set filter. The $wp_current_filter global is reset during teardown by
    449         // WP_UnitTestCase_Base::_restore_hooks() in tests/phpunit/includes/abstract-testcase.php.
    450         $wp_current_filter[] = 'rest_pre_insert_wp_template_part';
    451 
    452         register_block_type(
    453             'tests/hooked-block',
    454             array(
    455                 'block_hooks' => array(
    456                     'tests/anchor-block' => 'after',
    457                 ),
    458             )
    459         );
    460 
    461         $id      = self::TEST_THEME . '//' . 'my_template_part';
    462         $request = new WP_REST_Request( 'POST', '/wp/v2/template-parts/' . $id );
    463 
    464         $changes               = new stdClass();
    465         $changes->post_content = '<!-- wp:tests/anchor-block -->Hello<!-- /wp:tests/anchor-block -->';
    466 
    467         $post = inject_ignored_hooked_blocks_metadata_attributes( $changes, $request );
    468         $this->assertSame(
    469             '<!-- wp:tests/anchor-block {"metadata":{"ignoredHookedBlocks":["tests/hooked-block"]}} -->Hello<!-- /wp:tests/anchor-block -->',
    470             $post->post_content,
    471             'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.'
    472         );
    473     }
    474406}
  • trunk/tests/phpunit/tests/rest-api/wpRestTemplatesController.php

    r57790 r57919  
    1515     */
    1616    protected static $admin_id;
    17     private static $post;
     17    private static $template_post;
     18    private static $template_part_post;
    1819
    1920    /**
     
    3031
    3132        // Set up template post.
    32         $args       = array(
     33        $args                = array(
    3334            'post_type'    => 'wp_template',
    3435            'post_name'    => 'my_template',
     
    4243            ),
    4344        );
    44         self::$post = self::factory()->post->create_and_get( $args );
    45         wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' );
     45        self::$template_post = self::factory()->post->create_and_get( $args );
     46        wp_set_post_terms( self::$template_post->ID, get_stylesheet(), 'wp_theme' );
     47
     48        // Set up template part post.
     49        $args                     = array(
     50            'post_type'    => 'wp_template_part',
     51            'post_name'    => 'my_template_part',
     52            'post_title'   => 'My Template Part',
     53            'post_content' => 'Content',
     54            'post_excerpt' => 'Description of my template part.',
     55            'tax_input'    => array(
     56                'wp_theme'              => array(
     57                    get_stylesheet(),
     58                ),
     59                'wp_template_part_area' => array(
     60                    WP_TEMPLATE_PART_AREA_HEADER,
     61                ),
     62            ),
     63        );
     64        self::$template_part_post = self::factory()->post->create_and_get( $args );
     65        wp_set_post_terms( self::$template_part_post->ID, get_stylesheet(), 'wp_theme' );
     66        wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' );
    4667    }
    4768
    4869    public static function wpTearDownAfterClass() {
    49         wp_delete_post( self::$post->ID );
     70        wp_delete_post( self::$template_post->ID );
    5071    }
    5172
     
    5778    public function tear_down() {
    5879        if ( has_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' ) ) {
    59             remove_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes', 10 );
     80            remove_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' );
    6081        }
    6182        if ( WP_Block_Type_Registry::get_instance()->is_registered( 'tests/block' ) ) {
     
    131152                ),
    132153                'status'          => 'publish',
    133                 'wp_id'           => self::$post->ID,
     154                'wp_id'           => self::$template_post->ID,
    134155                'has_theme_file'  => false,
    135156                'is_custom'       => true,
    136157                'author'          => 0,
    137                 'modified'        => mysql_to_rfc3339( self::$post->post_modified ),
     158                'modified'        => mysql_to_rfc3339( self::$template_post->post_modified ),
    138159                'author_text'     => 'Test Blog',
    139160                'original_source' => 'site',
     
    178199                ),
    179200                'status'          => 'publish',
    180                 'wp_id'           => self::$post->ID,
     201                'wp_id'           => self::$template_post->ID,
    181202                'has_theme_file'  => false,
    182203                'is_custom'       => true,
    183204                'author'          => 0,
    184                 'modified'        => mysql_to_rfc3339( self::$post->post_modified ),
     205                'modified'        => mysql_to_rfc3339( self::$template_post->post_modified ),
    185206                'author_text'     => 'Test Blog',
    186207                'original_source' => 'site',
     
    217238                ),
    218239                'status'          => 'publish',
    219                 'wp_id'           => self::$post->ID,
     240                'wp_id'           => self::$template_post->ID,
    220241                'has_theme_file'  => false,
    221242                'is_custom'       => true,
    222243                'author'          => 0,
    223                 'modified'        => mysql_to_rfc3339( self::$post->post_modified ),
     244                'modified'        => mysql_to_rfc3339( self::$template_post->post_modified ),
    224245                'author_text'     => 'Test Blog',
    225246                'original_source' => 'site',
     
    945966        );
    946967
    947         add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes', 10, 2 );
     968        add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' );
    948969
    949970        $endpoint = new WP_REST_Templates_Controller( 'wp_template_part' );
     
    952973        $prepare_item_for_database->setAccessible( true );
    953974
     975        $id          = get_stylesheet() . '//' . 'my_template_part';
    954976        $body_params = array(
    955             'title'   => 'Untitled Template Part',
    956             'slug'    => 'untitled-template-part',
     977            'id'      => $id,
     978            'slug'    => 'my_template_part',
    957979            'content' => '<!-- wp:tests/anchor-block -->Hello<!-- /wp:tests/anchor-block -->',
    958980        );
Note: See TracChangeset for help on using the changeset viewer.