Make WordPress Core

Changeset 58041


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