diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index b953993b67c3c..6942d67181a8a 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -822,6 +822,58 @@ function _build_block_template_object_from_post_object( $post, $terms = array(), return $template; } +/** + * Receives an object once it has been prepared for inserting or updating the database and builds a unified template object. + * + * @since 6.6.0 + * @access private + * + * @param stdClass $obj An object representing a template or template part + * prepared for inserting or updating the database. + * @return WP_Block_Template|WP_Error Template or error object. + */ +function _build_block_template_object_from_database_object( $obj ) { + $meta = isset( $obj->meta_input ) ? $obj->meta_input : array(); + $terms = isset( $obj->tax_input ) ? $obj->tax_input : array(); + + if ( empty( $obj->ID ) ) { + // There's no post object for this template in the database for this template yet. + $post = $obj; + } else { + // Find the existing post object. + $post = get_post( $obj->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) $obj ); + + $type_terms = get_the_terms( $obj->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( $obj->ID, 'wp_template_part_area' ); + $terms['wp_template_part_area'] = ! is_wp_error( $area_terms ) && ! empty( $area_terms ) ? $area_terms[0]->name : null; + } + + return _build_block_template_object_from_post_object( new WP_Post( $post ), $terms, $meta ); +} + /** * Builds a unified template object based a post Object. * @@ -1528,82 +1580,3 @@ function get_template_hierarchy( $slug, $is_custom = false, $template_prefix = ' } return $template_hierarchy; } - -/** - * Inject ignoredHookedBlocks metadata attributes into a template or template part. - * - * Given an object that represents a `wp_template` or `wp_template_part` post object - * prepared for inserting or updating the database, locate all blocks that have - * hooked blocks, and inject a `metadata.ignoredHookedBlocks` attribute into the anchor - * blocks to reflect the latter. - * - * @since 6.5.0 - * @access private - * - * @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( $changes, $deprecated = null ) { - if ( null !== $deprecated ) { - _deprecated_argument( __FUNCTION__, '6.5.3' ); - } - - $hooked_blocks = get_hooked_blocks(); - if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) { - return $changes; - } - - $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( $changes->post_content ); - $changes->post_content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); - - return $changes; -} diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 986af9a865b7c..480bb994bb38e 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1111,6 +1111,96 @@ function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert }; } +/** + * Inject ignoredHookedBlocks metadata attributes into a template, template part or navigation. + * + * Given an object that represents a `wp_template`, `wp_template_part` or `wp_navigation` post object + * prepared for inserting or updating the database, locate all blocks that have + * hooked blocks, and inject a `metadata.ignoredHookedBlocks` attribute into the anchor + * blocks to reflect the latter. + * + * @since 6.5.0 + * @since 6.6.0 The function now also supports `wp_navigation` post_type. + * @access private + * + * @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( $changes, $deprecated = null ) { + if ( null !== $deprecated ) { + _deprecated_argument( __FUNCTION__, '6.5.3' ); + } + + /** + * Bail early if there are no hooked blocks. + */ + $hooked_blocks = get_hooked_blocks(); + if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) { + return $changes; + } + + /** + * Skip meta generation when consumers intentionally omit the content update. + */ + if ( ! isset( $changes->post_content ) ) { + return $changes; + } + + $blocks = parse_blocks( $changes->post_content ); + + if ( isset( $changes->post_type ) && 'wp_navigation' === $changes->post_type ) { + /* + * In this scenario the user has likely tried to create a navigation via the REST API. + * In which case we won't have a post ID to work with and store meta against. + */ + if ( empty( $changes->ID ) ) { + return $changes; + } + + /* + * Block Hooks logic requires a `WP_Post` object (rather than the `stdClass` with the updates that + * we're getting from the `rest_pre_insert_wp_navigation` filter). + * + * This will also return a serialized mocked navigation block with the `ignoredHookedBlocks` metadata. + */ + $navigation_markup = block_core_navigation_set_ignored_hooked_blocks_metadata( $blocks, get_post( $changes->ID ) ); + + $root_navigation_block = parse_blocks( $navigation_markup )[0]; + $ignored_hooked_blocks = isset( $root_navigation_block['attrs']['metadata']['ignoredHookedBlocks'] ) + ? $root_navigation_block['attrs']['metadata']['ignoredHookedBlocks'] + : array(); + + if ( ! empty( $ignored_hooked_blocks ) ) { + $existing_ignored_hooked_blocks = get_post_meta( $changes->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $existing_ignored_hooked_blocks ) ) { + $existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true ); + $ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) ); + } + update_post_meta( $changes->ID, '_wp_ignored_hooked_blocks', json_encode( $ignored_hooked_blocks ) ); + } + + $changes->post_content = block_core_navigation_remove_serialized_parent_block( $navigation_markup ); + + return $changes; + + } + + $template = _build_block_template_object_from_database_object( $changes ); + + 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' ); + + $changes->post_content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); + + return $changes; +} + /** * Given an array of attributes, returns a string in the serialized attributes * format prepared for post content. diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 1bcd85e79aaf5..52904e1e9e77a 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -757,4 +757,9 @@ 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' ); +// Note: When can we remove this conditional? +if ( ! has_filter( 'rest_pre_insert_wp_navigation', 'block_core_navigation_update_ignore_hooked_blocks_meta' ) && ! has_filter( 'rest_pre_insert_wp_navigation', 'gutenberg_block_core_navigation_update_ignore_hooked_blocks_meta' ) ) { + add_filter( 'rest_pre_insert_wp_navigation', 'inject_ignored_hooked_blocks_metadata_attributes' ); +} + unset( $filter, $action ); diff --git a/tests/phpunit/tests/block-templates/base.php b/tests/phpunit/tests/block-templates/base.php index daa58c041cbd6..0fed3748c9694 100644 --- a/tests/phpunit/tests/block-templates/base.php +++ b/tests/phpunit/tests/block-templates/base.php @@ -9,6 +9,11 @@ abstract class WP_Block_Templates_UnitTestCase extends WP_UnitTestCase { protected static $template_post; protected static $template_part_post; + protected static $uncustomized_template_db_object; + protected static $customized_template_db_object; + protected static $uncustomized_template_part_db_object; + protected static $customized_template_part_db_object; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { /* @@ -72,6 +77,55 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Setup uncustomized template db object. + self::$uncustomized_template_db_object = (object) array( + 'post_status' => 'publish', + 'tax_input' => array( + 'wp_theme' => self::TEST_THEME, + ), + 'meta_input' => array( + 'origin' => 'theme', + ), + 'post_content' => '

Template

', + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_excerpt' => 'Description of my template', + ); + + // Setup customized template db object. + self::$customized_template_db_object = (object) array( + 'post_name' => 'my_template', + 'post_title' => 'My Customized Template', + 'post_status' => 'publish', + 'post_content' => '

Template

', + ); + + // Setup uncustomized template part db object. + self::$uncustomized_template_part_db_object = (object) array( + 'post_status' => 'publish', + 'tax_input' => array( + 'wp_theme' => self::TEST_THEME, + 'wp_template_part_area' => WP_TEMPLATE_PART_AREA_HEADER, + ), + 'meta_input' => array( + 'origin' => 'theme', + ), + 'post_content' => '

Template Part

', + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_excerpt' => 'Description of my template part', + ); + + // Setup customised template part db object. + self::$customized_template_part_db_object = (object) array( + 'post_name' => 'my_template_part', + 'post_title' => 'My Customized Template Part', + 'post_status' => 'publish', + 'post_content' => '

Template Customized Part

', + ); } public static function wpTearDownAfterClass() { diff --git a/tests/phpunit/tests/block-templates/buildBlockTemplateObjectFromDatabaseObject.php b/tests/phpunit/tests/block-templates/buildBlockTemplateObjectFromDatabaseObject.php new file mode 100644 index 0000000000000..082ece3085c8f --- /dev/null +++ b/tests/phpunit/tests/block-templates/buildBlockTemplateObjectFromDatabaseObject.php @@ -0,0 +1,99 @@ +is_registered( 'tests/my-block' ) ) { + $registry->unregister( 'tests/my-block' ); + } + + if ( $registry->is_registered( 'tests/ignored' ) ) { + $registry->unregister( 'tests/ignored' ); + } + + parent::tear_down(); + } + + /** + * @ticket 60759 + */ + public function test_should_build_template_from_uncustomized_object() { + $template = _build_block_template_object_from_database_object( self::$uncustomized_template_db_object ); + + $this->assertNotWPError( $template ); + $this->assertSame( get_stylesheet() . '//my_template', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'My Template', $template->title ); + $this->assertSame( 'Description of my template', $template->description ); + $this->assertSame( 'wp_template', $template->type ); + } + + /** + * @ticket 60759 + */ + public function test_should_build_template_from_customized_object() { + self::$customized_template_db_object->ID = self::$template_post->ID; + $template = _build_block_template_object_from_database_object( self::$customized_template_db_object ); + + $this->assertNotWPError( $template ); + $this->assertSame( get_stylesheet() . '//my_template', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'My Customized Template', $template->title ); + $this->assertSame( 'Description of my template', $template->description ); + $this->assertSame( 'wp_template', $template->type ); + } + + /** + * @ticket 60759 + */ + public function test_should_build_template_part_from_uncustomized_object() { + $template = _build_block_template_object_from_database_object( self::$uncustomized_template_part_db_object ); + + $this->assertNotWPError( $template ); + $this->assertSame( get_stylesheet() . '//my_template_part', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template_part', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'My Template Part', $template->title ); + $this->assertSame( 'Description of my template part', $template->description ); + $this->assertSame( 'wp_template_part', $template->type ); + } + + /** + * @ticket 60759 + */ + public function test_should_build_template_part_from_customized_object() { + self::$customized_template_part_db_object->ID = self::$template_part_post->ID; + $template = _build_block_template_object_from_database_object( self::$customized_template_part_db_object ); + + $this->assertNotWPError( $template ); + $this->assertSame( get_stylesheet() . '//my_template_part', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template_part', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'My Customized Template Part', $template->title ); + $this->assertSame( 'Description of my template part', $template->description ); + $this->assertSame( 'wp_template_part', $template->type ); + } +}