Make WordPress Core

Changeset 58041


Ignore:
Timestamp:
04/24/2024 12:00:29 PM (8 weeks 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.

Reviewed by gziolo.
Merges [57919] to the to the 6.5 branch.

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

Location:
branches/6.5
Files:
1 added
5 edited

Legend:

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

    r57947 r58041  
    724724
    725725/**
    726  * Builds a unified template object based a post Object.
    727  *
    728  * @since 5.9.0
    729  * @since 6.3.0 Added `modified` property to template objects.
    730  * @since 6.4.0 Added support for a revision post to be passed to this function.
     726 * Builds a block template object from a post object.
     727 *
     728 * This is a helper function that creates a block template object from a given post object.
     729 * It is self-sufficient in that it only uses information passed as arguments; it does not
     730 * query the database for additional information.
     731 *
     732 * @since 6.5.1
    731733 * @access private
    732734 *
    733  * @param WP_Post $post Template post.
     735 * @param WP_Post $post  Template post.
     736 * @param array   $terms Additional terms to inform the template object.
     737 * @param array   $meta  Additional meta fields to inform the template object.
    734738 * @return WP_Block_Template|WP_Error Template or error object.
    735739 */
    736 function _build_block_template_result_from_post( $post ) {
     740function _build_block_template_object_from_post_object( $post, $terms = array(), $meta = array() ) {
     741    if ( empty( $terms['wp_theme'] ) ) {
     742        return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
     743    }
     744    $theme = $terms['wp_theme'];
     745
    737746    $default_template_types = get_default_block_template_types();
    738747
    739     $post_id = wp_is_post_revision( $post );
    740     if ( ! $post_id ) {
    741         $post_id = $post;
    742     }
    743     $parent_post = get_post( $post_id );
    744 
    745     $terms = get_the_terms( $parent_post, 'wp_theme' );
    746 
    747     if ( is_wp_error( $terms ) ) {
    748         return $terms;
    749     }
    750 
    751     if ( ! $terms ) {
    752         return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
    753     }
    754 
    755     $theme          = $terms[0]->name;
    756748    $template_file  = _get_block_template_file( $post->post_type, $post->post_name );
    757749    $has_theme_file = get_stylesheet() === $theme && null !== $template_file;
    758750
    759     $origin           = get_post_meta( $parent_post->ID, 'origin', true );
    760     $is_wp_suggestion = get_post_meta( $parent_post->ID, 'is_wp_suggestion', true );
    761 
    762751    $template                 = new WP_Block_Template();
    763752    $template->wp_id          = $post->ID;
    764     $template->id             = $theme . '//' . $parent_post->post_name;
     753    $template->id             = $theme . '//' . $post->post_name;
    765754    $template->theme          = $theme;
    766755    $template->content        = $post->post_content;
    767756    $template->slug           = $post->post_name;
    768757    $template->source         = 'custom';
    769     $template->origin         = ! empty( $origin ) ? $origin : null;
     758    $template->origin         = ! empty( $meta['origin'] ) ? $meta['origin'] : null;
    770759    $template->type           = $post->post_type;
    771760    $template->description    = $post->post_excerpt;
     
    773762    $template->status         = $post->post_status;
    774763    $template->has_theme_file = $has_theme_file;
    775     $template->is_custom      = empty( $is_wp_suggestion );
     764    $template->is_custom      = empty( $meta['is_wp_suggestion'] );
    776765    $template->author         = $post->post_author;
    777766    $template->modified       = $post->post_modified;
    778767
    779     if ( 'wp_template' === $parent_post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) {
     768    if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) {
    780769        $template->post_types = $template_file['postTypes'];
    781770    }
    782771
    783     if ( 'wp_template' === $parent_post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
     772    if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) {
    784773        $template->is_custom = false;
    785774    }
     775
     776    if ( 'wp_template_part' === $post->post_type && isset( $terms['wp_template_part_area'] ) ) {
     777        $template->area = $terms['wp_template_part_area'];
     778    }
     779
     780    return $template;
     781}
     782
     783/**
     784 * Builds a unified template object based a post Object.
     785 *
     786 * @since 5.9.0
     787 * @since 6.3.0 Added `modified` property to template objects.
     788 * @since 6.4.0 Added support for a revision post to be passed to this function.
     789 * @access private
     790 *
     791 * @param WP_Post $post Template post.
     792 * @return WP_Block_Template|WP_Error Template or error object.
     793 */
     794function _build_block_template_result_from_post( $post ) {
     795    $post_id = wp_is_post_revision( $post );
     796    if ( ! $post_id ) {
     797        $post_id = $post;
     798    }
     799    $parent_post     = get_post( $post_id );
     800    $post->post_name = $parent_post->post_name;
     801    $post->post_type = $parent_post->post_type;
     802
     803    $terms = get_the_terms( $parent_post, 'wp_theme' );
     804
     805    if ( is_wp_error( $terms ) ) {
     806        return $terms;
     807    }
     808
     809    if ( ! $terms ) {
     810        return new WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.' ) );
     811    }
     812
     813    $terms = array(
     814        'wp_theme' => $terms[0]->name,
     815    );
    786816
    787817    if ( 'wp_template_part' === $parent_post->post_type ) {
    788818        $type_terms = get_the_terms( $parent_post, 'wp_template_part_area' );
    789819        if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
    790             $template->area = $type_terms[0]->name;
    791         }
     820            $terms['wp_template_part_area'] = $type_terms[0]->name;
     821        }
     822    }
     823
     824    $meta = array(
     825        'origin'           => get_post_meta( $parent_post->ID, 'origin', true ),
     826        'is_wp_suggestion' => get_post_meta( $parent_post->ID, 'is_wp_suggestion', true ),
     827    );
     828
     829    $template = _build_block_template_object_from_post_object( $post, $terms, $meta );
     830
     831    if ( is_wp_error( $template ) ) {
     832        return $template;
    792833    }
    793834
     
    14431484 * @access private
    14441485 *
    1445  * @param stdClass        $post    An object representing a template or template part
    1446  *                                 prepared for inserting or updating the database.
    1447  * @param WP_REST_Request $request Request object.
    1448  * @return stdClass The updated object representing a template or template part.
    1449  */
    1450 function inject_ignored_hooked_blocks_metadata_attributes( $post, $request ) {
    1451     $filter_name = current_filter();
    1452     if ( ! str_starts_with( $filter_name, 'rest_pre_insert_' ) ) {
    1453         return $post;
    1454     }
    1455     $post_type = str_replace( 'rest_pre_insert_', '', $filter_name );
     1486 * @param stdClass        $changes    An object representing a template or template part
     1487 *                                    prepared for inserting or updating the database.
     1488 * @param WP_REST_Request $deprecated Deprecated. Not used.
     1489 * @return stdClass|WP_Error The updated object representing a template or template part.
     1490 */
     1491function inject_ignored_hooked_blocks_metadata_attributes( $changes, $deprecated = null ) {
     1492    if ( null !== $deprecated ) {
     1493        _deprecated_argument( __FUNCTION__, '6.5.1' );
     1494    }
    14561495
    14571496    $hooked_blocks = get_hooked_blocks();
    14581497    if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) {
    1459         return $post;
    1460     }
    1461 
    1462     // At this point, the post has already been created.
    1463     // We need to build the corresponding `WP_Block_Template` object as context argument for the visitor.
    1464     // To that end, we need to suppress hooked blocks from getting inserted into the template.
    1465     add_filter( 'hooked_block_types', '__return_empty_array', 99999, 0 );
    1466     $template = $request['id'] ? get_block_template( $request['id'], $post_type ) : null;
    1467     remove_filter( 'hooked_block_types', '__return_empty_array', 99999 );
     1498        return $changes;
     1499    }
     1500
     1501    $meta  = isset( $changes->meta_input ) ? $changes->meta_input : array();
     1502    $terms = isset( $changes->tax_input ) ? $changes->tax_input : array();
     1503
     1504    if ( empty( $changes->ID ) ) {
     1505        // There's no post object for this template in the database for this template yet.
     1506        $post = $changes;
     1507    } else {
     1508        // Find the existing post object.
     1509        $post = get_post( $changes->ID );
     1510
     1511        // If the post is a revision, use the parent post's post_name and post_type.
     1512        $post_id = wp_is_post_revision( $post );
     1513        if ( $post_id ) {
     1514            $parent_post     = get_post( $post_id );
     1515            $post->post_name = $parent_post->post_name;
     1516            $post->post_type = $parent_post->post_type;
     1517        }
     1518
     1519        // Apply the changes to the existing post object.
     1520        $post = (object) array_merge( (array) $post, (array) $changes );
     1521
     1522        $type_terms        = get_the_terms( $changes->ID, 'wp_theme' );
     1523        $terms['wp_theme'] = ! is_wp_error( $type_terms ) && ! empty( $type_terms ) ? $type_terms[0]->name : null;
     1524    }
     1525
     1526    // Required for the WP_Block_Template. Update the post object with the current time.
     1527    $post->post_modified = current_time( 'mysql' );
     1528
     1529    // If the post_author is empty, set it to the current user.
     1530    if ( empty( $post->post_author ) ) {
     1531        $post->post_author = get_current_user_id();
     1532    }
     1533
     1534    if ( 'wp_template_part' === $post->post_type && ! isset( $terms['wp_template_part_area'] ) ) {
     1535        $area_terms                     = get_the_terms( $changes->ID, 'wp_template_part_area' );
     1536        $terms['wp_template_part_area'] = ! is_wp_error( $area_terms ) && ! empty( $area_terms ) ? $area_terms[0]->name : null;
     1537    }
     1538
     1539    $template = _build_block_template_object_from_post_object( new WP_Post( $post ), $terms, $meta );
     1540
     1541    if ( is_wp_error( $template ) ) {
     1542        return $template;
     1543    }
    14681544
    14691545    $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, 'set_ignored_hooked_blocks_metadata' );
    14701546    $after_block_visitor  = make_after_block_visitor( $hooked_blocks, $template, 'set_ignored_hooked_blocks_metadata' );
    14711547
    1472     $blocks  = parse_blocks( $post->post_content );
    1473     $content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
    1474 
    1475     $post->post_content = $content;
    1476     return $post;
    1477 }
     1548    $blocks                = parse_blocks( $changes->post_content );
     1549    $changes->post_content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
     1550
     1551    return $changes;
     1552}
  • branches/6.5/src/wp-includes/default-filters.php

    r57802 r58041  
    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 );
  • branches/6.5/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php

    r57802 r58041  
    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 ) {
  • branches/6.5/tests/phpunit/tests/block-template-utils.php

    r57803 r58041  
    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}
  • branches/6.5/tests/phpunit/tests/rest-api/wpRestTemplatesController.php

    r57802 r58041  
    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.