diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 2681acb5686b3..6a4df84a11f7c 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -606,15 +606,35 @@ function _build_block_template_result_from_file( $template_file, $template_type $template->area = $template_file['area']; } + $hooked_blocks = get_hooked_blocks(); + $has_hooked_blocks = ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ); $before_block_visitor = '_inject_theme_attribute_in_template_part_block'; $after_block_visitor = null; - $hooked_blocks = get_hooked_blocks(); - if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { + + if ( $has_hooked_blocks ) { $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); $after_block_visitor = make_after_block_visitor( $hooked_blocks, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); } - $blocks = parse_blocks( $template->content ); - $template->content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); + + if ( 'wp_template_part' === $template->type && $has_hooked_blocks ) { + /** + * In order for hooked blocks to be inserted at positions first_child and last_child in a template part, + * we need to wrap its content a mock template part block and traverse it. + */ + $content = get_comment_delimited_block_content( + 'core/template-part', + array(), + $template->content + ); + $content = traverse_and_serialize_blocks( parse_blocks( $content ), $before_block_visitor, $after_block_visitor ); + $template->content = remove_serialized_parent_block( $content ); + } else { + $template->content = traverse_and_serialize_blocks( + parse_blocks( $template->content ), + $before_block_visitor, + $after_block_visitor + ); + } return $template; } @@ -998,8 +1018,28 @@ function _build_block_template_result_from_post( $post ) { if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); $after_block_visitor = make_after_block_visitor( $hooked_blocks, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); - $blocks = parse_blocks( $template->content ); - $template->content = traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor ); + if ( 'wp_template_part' === $template->type ) { + $existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + $attributes = ! empty( $existing_ignored_hooked_blocks ) ? array( 'metadata' => array( 'ignoredHookedBlocks' => json_decode( $existing_ignored_hooked_blocks, true ) ) ) : array(); + + /** + * In order for hooked blocks to be inserted at positions first_child and last_child in a template part, + * we need to wrap its content a mock template part block and traverse it. + */ + $content = get_comment_delimited_block_content( + 'core/template-part', + $attributes, + $template->content + ); + $content = traverse_and_serialize_blocks( parse_blocks( $content ), $before_block_visitor, $after_block_visitor ); + $template->content = remove_serialized_parent_block( $content ); + } else { + $template->content = traverse_and_serialize_blocks( + parse_blocks( $template->content ), + $before_block_visitor, + $after_block_visitor + ); + } } return $template; @@ -1611,7 +1651,36 @@ function inject_ignored_hooked_blocks_metadata_attributes( $changes, $deprecated return $template; } - $changes->post_content = apply_block_hooks_to_content( $changes->post_content, $template, 'set_ignored_hooked_blocks_metadata' ); + if ( 'wp_template_part' === $post->post_type ) { + $attributes = array(); + $existing_ignored_hooked_blocks = isset( $post->ID ) ? get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ) : ''; + + if ( ! empty( $existing_ignored_hooked_blocks ) ) { + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => json_decode( $existing_ignored_hooked_blocks, true ), + ); + } + + $content = get_comment_delimited_block_content( + 'core/template-part', + $attributes, + $changes->post_content + ); + $content = apply_block_hooks_to_content( $content, $template, 'set_ignored_hooked_blocks_metadata' ); + $changes->post_content = remove_serialized_parent_block( $content ); + + $wrapper_block_markup = extract_serialized_parent_block( $content ); + $wrapper_block = parse_blocks( $wrapper_block_markup )[0]; + $ignored_hooked_blocks = $wrapper_block['attrs']['metadata']['ignoredHookedBlocks'] ?? array(); + if ( ! empty( $ignored_hooked_blocks ) ) { + if ( ! isset( $changes->meta_input ) ) { + $changes->meta_input = array(); + } + $changes->meta_input['_wp_ignored_hooked_blocks'] = wp_json_encode( $ignored_hooked_blocks ); + } + } else { + $changes->post_content = apply_block_hooks_to_content( $changes->post_content, $template, 'set_ignored_hooked_blocks_metadata' ); + } return $changes; } diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 2edcaa8c1ba04..3b1fc25d48824 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1045,6 +1045,23 @@ function remove_serialized_parent_block( $serialized_block ) { return substr( $serialized_block, $start, $end - $start ); } +/** + * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the wrapper block. + * + * @since 6.7.0 + * @access private + * + * @see remove_serialized_parent_block() + * + * @param string $serialized_block The serialized markup of a block and its inner blocks. + * @return string The serialized markup of the wrapper block. + */ +function extract_serialized_parent_block( $serialized_block ) { + $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); + $end = strrpos( $serialized_block, '', $template_part->content ); + } + + /* + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_first_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'first_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template_part' + ); + $this->assertStringStartsWith( '', $template_part->content ); + } + + /* + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_last_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'last_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template_part' + ); + $this->assertStringEndsWith( '', $template_part->content ); + } } diff --git a/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php b/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php index 586e9beded17b..839e5788bc6c3 100644 --- a/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php +++ b/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php @@ -111,6 +111,50 @@ public function test_should_inject_hooked_block_into_template_part() { $this->assertStringEndsWith( '', $template_part->content ); } + /* + * @ticket 59646 + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_first_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'first_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertStringStartsWith( '', $template_part->content ); + } + + /* + * @ticket 59646 + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_last_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'last_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertStringEndsWith( '', $template_part->content ); + } + /** * @ticket 59646 * @ticket 60506 diff --git a/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php b/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php index bc753504bbce0..1f8a66ee8546f 100644 --- a/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php +++ b/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php @@ -17,6 +17,7 @@ public function tear_down() { if ( WP_Block_Type_Registry::get_instance()->is_registered( 'tests/hooked-block' ) ) { unregister_block_type( 'tests/hooked-block' ); } + delete_post_meta( self::$template_part_post->ID, '_wp_ignored_hooked_blocks' ); parent::tear_down(); } @@ -38,14 +39,35 @@ public function test_hooked_block_types_filter_with_newly_created_template() { inject_ignored_hooked_blocks_metadata_attributes( $changes ); - $args = $action->get_args(); - $anchor_block_type = end( $args )[2]; - $context = end( $args )[3]; + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); - $this->assertSame( 'tests/anchor-block', $anchor_block_type ); + $this->assertSame( + array( + 'before', + 'after', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); - $this->assertInstanceOf( 'WP_Block_Template', $context ); + $this->assertSame( + array( + 'tests/anchor-block', + 'tests/anchor-block', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); $this->assertSame( $changes->post_type, $context->type, @@ -69,6 +91,7 @@ public function test_hooked_block_types_filter_with_newly_created_template() { /** * @ticket 60754 + * @ticket 60854 */ public function test_hooked_block_types_filter_with_newly_created_template_part() { $action = new MockAction(); @@ -85,14 +108,48 @@ public function test_hooked_block_types_filter_with_newly_created_template_part( inject_ignored_hooked_blocks_metadata_attributes( $changes ); - $args = $action->get_args(); - $anchor_block_type = end( $args )[2]; - $context = end( $args )[3]; + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); - $this->assertSame( 'tests/anchor-block', $anchor_block_type ); + $this->assertSame( + array( + 'before', + 'after', + 'first_child', + 'before', + 'after', + 'last_child', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); - $this->assertInstanceOf( 'WP_Block_Template', $context ); + $this->assertSame( + array( + 'core/template-part', + 'core/template-part', + 'core/template-part', + 'tests/anchor-block', + 'tests/anchor-block', + 'core/template-part', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertInstanceOf( + 'WP_Block_Template', + $context, + 'The context passed to the hooked_block_types filter is not an instance of WP_Block_Template.' + ); $this->assertSame( $changes->post_type, $context->type, @@ -140,14 +197,35 @@ public function test_hooked_block_types_filter_with_existing_template_file() { inject_ignored_hooked_blocks_metadata_attributes( $changes ); - $args = $action->get_args(); - $anchor_block_type = end( $args )[2]; - $context = end( $args )[3]; + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); - $this->assertSame( 'tests/anchor-block', $anchor_block_type ); + $this->assertSame( + array( + 'before', + 'after', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); - $this->assertInstanceOf( 'WP_Block_Template', $context ); + $this->assertSame( + array( + 'tests/anchor-block', + 'tests/anchor-block', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); $this->assertSame( $changes->post_name, $context->slug, @@ -181,6 +259,7 @@ public function test_hooked_block_types_filter_with_existing_template_file() { /** * @ticket 60754 + * @ticket 60854 */ public function test_hooked_block_types_filter_with_existing_template_part_file() { $action = new MockAction(); @@ -201,14 +280,48 @@ public function test_hooked_block_types_filter_with_existing_template_part_file( inject_ignored_hooked_blocks_metadata_attributes( $changes ); - $args = $action->get_args(); - $anchor_block_type = end( $args )[2]; - $context = end( $args )[3]; + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); - $this->assertSame( 'tests/anchor-block', $anchor_block_type ); + $this->assertSame( + array( + 'before', + 'after', + 'first_child', + 'before', + 'after', + 'last_child', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); - $this->assertInstanceOf( 'WP_Block_Template', $context ); + $this->assertSame( + array( + 'core/template-part', + 'core/template-part', + 'core/template-part', + 'tests/anchor-block', + 'tests/anchor-block', + 'core/template-part', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertInstanceOf( + 'WP_Block_Template', + $context, + 'The context passed to the hooked_block_types filter is not an instance of WP_Block_Template.' + ); $this->assertSame( $changes->post_name, $context->slug, @@ -259,14 +372,35 @@ public function test_hooked_block_types_filter_with_existing_template_post() { inject_ignored_hooked_blocks_metadata_attributes( $changes ); - $args = $action->get_args(); - $anchor_block_type = end( $args )[2]; - $context = end( $args )[3]; + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); - $this->assertSame( 'tests/anchor-block', $anchor_block_type ); + $this->assertSame( + array( + 'before', + 'after', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); - $this->assertInstanceOf( 'WP_Block_Template', $context ); + $this->assertSame( + array( + 'tests/anchor-block', + 'tests/anchor-block', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); $this->assertSame( $changes->post_name, $context->slug, @@ -302,6 +436,7 @@ public function test_hooked_block_types_filter_with_existing_template_post() { /** * @ticket 60754 + * @ticket 60854 */ public function test_hooked_block_types_filter_with_existing_template_part_post() { $action = new MockAction(); @@ -318,14 +453,48 @@ public function test_hooked_block_types_filter_with_existing_template_part_post( inject_ignored_hooked_blocks_metadata_attributes( $changes ); - $args = $action->get_args(); - $anchor_block_type = end( $args )[2]; - $context = end( $args )[3]; + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); - $this->assertSame( 'tests/anchor-block', $anchor_block_type ); + $this->assertSame( + array( + 'before', + 'after', + 'first_child', + 'before', + 'after', + 'last_child', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); - $this->assertInstanceOf( 'WP_Block_Template', $context ); + $this->assertSame( + array( + 'core/template-part', + 'core/template-part', + 'core/template-part', + 'tests/anchor-block', + 'tests/anchor-block', + 'core/template-part', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertInstanceOf( + 'WP_Block_Template', + $context, + 'The context passed to the hooked_block_types filter is not an instance of WP_Block_Template.' + ); $this->assertSame( $changes->post_name, $context->slug, @@ -419,4 +588,37 @@ public function test_inject_ignored_hooked_blocks_metadata_attributes_into_templ 'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.' ); } + + /** + * @ticket 60854 + */ + public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_part_postmeta() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'last_child', + ), + ) + ); + + $id = self::TEST_THEME . '//' . 'my_template_part'; + $template = get_block_template( $id, 'wp_template_part' ); + + $changes = new stdClass(); + $changes->ID = $template->wp_id; + $changes->post_content = 'Hello'; + + $post = inject_ignored_hooked_blocks_metadata_attributes( $changes ); + $this->assertSame( + array( 'tests/hooked-block' ), + json_decode( $post->meta_input['_wp_ignored_hooked_blocks'], true ), + 'The hooked block was not injected into the wp_template_part\'s _wp_ignored_hooked_blocks postmeta.' + ); + $this->assertSame( + $changes->post_content, + $post->post_content, + 'The template part\'s post content was modified.' + ); + } }