Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -4050,6 +4050,19 @@ function wp_trim_excerpt( $text = '', $post = null ) {
*/
$filter_block_removed = remove_filter( 'the_content', 'do_blocks', 9 );

/*
* Temporarily unhook apply_block_hooks_to_content_from_post_object().
* The excerpt is generated from already-rendered content and
* do_blocks() is unhooked above, so any hooked blocks inserted here
* could never be rendered. The callback wraps delimiter-free content
* in a temporary core/post-content wrapper block to anchor hooked
* blocks, so the algorithm would still run and any markup added via
* the 'hooked_block' filter would leak into the excerpt as text. It
* would also resolve its context from the current loop post via
* get_post() rather than the post being excerpted.
*/
$filter_block_hooks_removed = remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 );

/** This filter is documented in wp-includes/post-template.php */
$text = apply_filters( 'the_content', $text );
$text = str_replace( ']]>', ']]>', $text );
Expand All @@ -4059,6 +4072,10 @@ function wp_trim_excerpt( $text = '', $post = null ) {
add_filter( 'the_content', 'do_blocks', 9 );
}

if ( $filter_block_hooks_removed ) {
add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 );
}

/*
* Only restore the filter callback if it was removed above. The logic
* to unhook and restore only applies on the default priority of 10,
Expand Down
190 changes: 190 additions & 0 deletions tests/phpunit/tests/formatting/wpTrimExcerpt.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,194 @@ public function test_wp_trim_excerpt_does_not_restore_do_blocks_if_previously_un
// Assert that the filter callback was not restored after running 'the_content'.
$this->assertFalse( has_filter( 'the_content', 'do_blocks' ) );
}

/**
* Tests that `wp_trim_excerpt()` unhooks `apply_block_hooks_to_content_from_post_object()` from 'the_content' filter.
*
* @ticket 66110
*/
public function test_wp_trim_excerpt_unhooks_apply_block_hooks_to_content_from_post_object() {
$post = self::factory()->post->create();

/*
* Record that during 'the_content' filter run by wp_trim_excerpt() the
* apply_block_hooks_to_content_from_post_object() callback is not used.
*/
$has_filter = true;
add_filter(
'the_content',
static function ( $content ) use ( &$has_filter ) {
$has_filter = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' );
return $content;
}
);

wp_trim_excerpt( '', $post );

$this->assertFalse( $has_filter, 'apply_block_hooks_to_content_from_post_object() was not unhooked in wp_trim_excerpt()' );
}

/**
* Tests that `wp_trim_excerpt()` doesn't permanently unhook `apply_block_hooks_to_content_from_post_object()` from 'the_content' filter.
*
* @ticket 66110
*/
public function test_wp_trim_excerpt_should_not_permanently_unhook_apply_block_hooks_to_content_from_post_object() {
$post = self::factory()->post->create();

wp_trim_excerpt( '', $post );

$this->assertSame( 8, has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' ), 'apply_block_hooks_to_content_from_post_object() was not restored in wp_trim_excerpt()' );
}

/**
* Tests that `wp_trim_excerpt()` doesn't restore `apply_block_hooks_to_content_from_post_object()` if it was previously unhooked.
*
* @ticket 66110
*/
public function test_wp_trim_excerpt_does_not_restore_apply_block_hooks_to_content_from_post_object_if_previously_unhooked() {
$post = self::factory()->post->create();

// Remove apply_block_hooks_to_content_from_post_object() from 'the_content' filter generally.
remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 );

wp_trim_excerpt( '', $post );

// Assert that the filter callback was not restored after running 'the_content'.
$this->assertFalse( has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' ) );
}

/**
* Tests that the Block Hooks algorithm is not run while `wp_trim_excerpt()`
* generates an excerpt.
*
* The 'hooked_block_types' filter only fires inside the Block Hooks
* algorithm, so counting its invocations detects whether the algorithm ran.
*
* @ticket 66110
*/
public function test_wp_trim_excerpt_does_not_run_block_hooks_algorithm() {
$post = self::factory()->post->create(
array(
'post_content' => '<!-- wp:paragraph --><p>A test paragraph.</p><!-- /wp:paragraph -->',
)
);

register_block_type(
'tests/hooked-block',
array(
'block_hooks' => array(
'core/post-content' => 'last_child',
),
)
);

$hooked_block_types_count = 0;
$spy = static function ( $hooked_block_types ) use ( &$hooked_block_types_count ) {
++$hooked_block_types_count;
return $hooked_block_types;
};
add_filter( 'hooked_block_types', $spy );

wp_trim_excerpt( '', $post );

remove_filter( 'hooked_block_types', $spy );
unregister_block_type( 'tests/hooked-block' );

$this->assertSame( 0, $hooked_block_types_count, 'Block Hooks algorithm ran during wp_trim_excerpt()' );
}

/**
* Tests that markup of a hooked block does not leak into the generated excerpt.
*
* apply_block_hooks_to_content_from_post_object() wraps delimiter-free
* content in a temporary core/post-content wrapper block, which acts as an
* anchor for hooked blocks. Since do_blocks() is unhooked while the excerpt
* is generated, an inserted hooked block is never rendered, and any markup
* provided via the 'hooked_block' filter would leak into the excerpt as
* plain text once the block delimiters are stripped.
*
* @ticket 66110
*/
public function test_wp_trim_excerpt_does_not_leak_hooked_block_markup() {
$post = self::factory()->post->create(
array(
'post_content' => '<!-- wp:paragraph --><p>Visible text.</p><!-- /wp:paragraph -->',
)
);

// Emulate a front-end loop, as the callback resolves context via get_post().
$GLOBALS['post'] = get_post( $post );
setup_postdata( $GLOBALS['post'] );

register_block_type(
'tests/hooked-block',
array(
'block_hooks' => array(
'core/post-content' => 'last_child',
),
)
);

// Give the hooked block actual markup, as the 'hooked_block' filter allows.
$filter = static function ( array $parsed_hooked_block ): array {
$parsed_hooked_block['innerHTML'] = '<p>LEAKED HOOKED TEXT</p>';
$parsed_hooked_block['innerContent'] = array( '<p>LEAKED HOOKED TEXT</p>' );
return $parsed_hooked_block;
};
add_filter( 'hooked_block', $filter );

$excerpt = wp_trim_excerpt( '', $post );

remove_filter( 'hooked_block', $filter );
unregister_block_type( 'tests/hooked-block' );

$this->assertSame( 'Visible text.', $excerpt, 'Hooked block markup leaked into the excerpt generated by wp_trim_excerpt()' );
}

/**
* Tests that Block Hooks still run on 'the_content' after `wp_trim_excerpt()`
* has generated an excerpt.
*
* @ticket 66110
*/
public function test_wp_trim_excerpt_restores_block_hooks_for_subsequent_the_content() {
$post = self::factory()->post->create(
array(
'post_content' => '<!-- wp:paragraph --><p>Visible text.</p><!-- /wp:paragraph -->',
)
);

// Emulate a front-end loop, as the callback resolves context via get_post().
$GLOBALS['post'] = get_post( $post );
setup_postdata( $GLOBALS['post'] );

register_block_type(
'tests/hooked-block',
array(
'block_hooks' => array(
'core/post-content' => 'last_child',
),
)
);

$filter = static function ( array $parsed_hooked_block ): array {
$parsed_hooked_block['innerHTML'] = '<p>HOOKED TEXT</p>';
$parsed_hooked_block['innerContent'] = array( '<p>HOOKED TEXT</p>' );
return $parsed_hooked_block;
};
add_filter( 'hooked_block', $filter );

// Generate an excerpt, which unhooks and restores the callback.
wp_trim_excerpt( '', $post );

// A subsequent front-end run of 'the_content' must still apply Block Hooks.
/** This filter is documented in wp-includes/post-template.php */
$content = apply_filters( 'the_content', get_the_content( '', false, $post ) );

remove_filter( 'hooked_block', $filter );
unregister_block_type( 'tests/hooked-block' );

$this->assertStringContainsString( 'HOOKED TEXT', $content, 'Block Hooks did not run on the_content after wp_trim_excerpt()' );
}
}
Loading