Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure main query loop is entered for singular content in block themes #5104

29 changes: 26 additions & 3 deletions src/wp-includes/block-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,12 @@ function _block_template_render_title_tag() {
*
* @global string $_wp_current_template_content
* @global WP_Embed $wp_embed
* @global WP_Query $wp_query
*
* @return string Block template markup.
*/
function get_the_block_template_html() {
global $_wp_current_template_content;
global $wp_embed;
global $_wp_current_template_content, $wp_embed, $wp_query;

if ( ! $_wp_current_template_content ) {
if ( is_user_logged_in() ) {
Expand All @@ -228,7 +228,30 @@ function get_the_block_template_html() {
$content = $wp_embed->autoembed( $content );
$content = shortcode_unautop( $content );
$content = do_shortcode( $content );
$content = do_blocks( $content );

/*
* Most block themes omit the `core/query` and `core/post-template` blocks in their singular content templates.
* While this technically still works since singular content templates are always for only one post, it results in
* the main query loop never being entered which causes bugs in core and the plugin ecosystem.
*
* The workaround below ensures that the loop is started even for those singular templates. The while loop will by
* definition only go through a single iteration, i.e. `do_blocks()` is only called once. Additional safeguard
* checks are included to ensure the main query loop has not been tampered with and really only encompasses a
* single post.
*
* Even if the block template contained a `core/query` and `core/post-template` block referencing the main query
* loop, it would not cause errors since it would use a cloned instance and go through the same loop of a single
* post, within the actual main query loop.
*/
if ( is_singular() && 1 === $wp_query->post_count && have_posts() ) {
while ( have_posts() ) {
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
the_post();
$content = do_blocks( $content );
}
} else {
$content = do_blocks( $content );
}

$content = wptexturize( $content );
$content = convert_smilies( $content );
$content = wp_filter_content_tags( $content, 'template' );
Expand Down
114 changes: 114 additions & 0 deletions tests/phpunit/tests/block-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,118 @@ public function test_template_remains_unchanged_if_templates_array_is_empty() {
$resolved_template_path = locate_block_template( '', 'search', array() );
$this->assertSame( '', $resolved_template_path );
}

/**
* Tests that `get_the_block_template_html()` wraps block parsing into the query loop when on a singular template.
*
* This is necessary since block themes do not include the necessary blocks to trigger the main query loop, and
* since there is only a single post in the main query loop in such cases anyway.
*
* @ticket 58154
* @covers ::get_the_block_template_html
*/
public function test_get_the_block_template_html_enforces_singular_query_loop() {
global $_wp_current_template_content, $wp_query, $wp_the_query;

// Register test block to log `in_the_loop()` results.
$in_the_loop_logs = array();
$this->register_in_the_loop_logger_block( $in_the_loop_logs );

// Set main query to single post.
$post_id = self::factory()->post->create( array( 'post_title' => 'A single post' ) );
$wp_query = new WP_Query( array( 'p' => $post_id ) );
$wp_the_query = $wp_query;

// Use block template that just renders post title and the above test block.
$_wp_current_template_content = '<!-- wp:post-title /--><!-- wp:test/in-the-loop-logger /-->';

$expected = '<div class="wp-site-blocks">';
$expected .= '<h2 class="wp-block-post-title">A single post</h2>';
$expected .= '</div>';

$output = get_the_block_template_html();
$this->unregister_in_the_loop_logger_block();
$this->assertSame( $expected, $output, 'Unexpected block template output' );
$this->assertSame( array( true ), $in_the_loop_logs, 'Main query loop was not triggered' );
}

/**
* Tests that `get_the_block_template_html()` does not start the main query loop generally.
*
* @ticket 58154
* @covers ::get_the_block_template_html
*/
public function test_get_the_block_template_html_does_not_generally_enforce_loop() {
global $_wp_current_template_content, $wp_query, $wp_the_query;

// Register test block to log `in_the_loop()` results.
$in_the_loop_logs = array();
$this->register_in_the_loop_logger_block( $in_the_loop_logs );

// Set main query to a general post query (i.e. not for a specific post).
$post_id = self::factory()->post->create(
array(
'post_title' => 'A single post',
'post_content' => 'The content.',
)
);
$wp_query = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'publish',
)
);
$wp_the_query = $wp_query;

/*
* Use block template that renders the above test block, followed by a main query loop.
* `get_the_block_template_html()` should not start the loop, but the `core/query` and `core/post-template`
* blocks should.
*/
$_wp_current_template_content = '<!-- wp:test/in-the-loop-logger /-->';
$_wp_current_template_content .= '<!-- wp:query {"query":{"inherit":true}} -->';
$_wp_current_template_content .= '<!-- wp:post-template -->';
$_wp_current_template_content .= '<!-- wp:post-title /-->';
$_wp_current_template_content .= '<!-- wp:post-content /--><!-- wp:test/in-the-loop-logger /-->';
$_wp_current_template_content .= '<!-- /wp:post-template -->';
$_wp_current_template_content .= '<!-- /wp:query -->';

$expected = '<div class="wp-site-blocks">';
$expected .= '<ul class="wp-block-post-template is-layout-flow wp-block-post-template-is-layout-flow wp-block-query-is-layout-flow">';
$expected .= '<li class="wp-block-post post-' . $post_id . ' post type-post status-publish format-standard hentry category-uncategorized">';
$expected .= '<h2 class="wp-block-post-title">A single post</h2>';
$expected .= '<div class="entry-content wp-block-post-content is-layout-flow wp-block-post-content-is-layout-flow">' . wpautop( 'The content.' ) . '</div>';
$expected .= '</li>';
$expected .= '</ul>';
$expected .= '</div>';

$output = get_the_block_template_html();
$this->unregister_in_the_loop_logger_block();
$this->assertSame( $expected, $output, 'Unexpected block template output' );
$this->assertSame( array( false, true ), $in_the_loop_logs, 'Main query loop was triggered incorrectly' );
}

/**
* Registers a test block to log `in_the_loop()` results.
*
* @param array $in_the_loop_logs Array to log function results in. Passed by reference.
*/
private function register_in_the_loop_logger_block( array &$in_the_loop_logs ) {
register_block_type(
'test/in-the-loop-logger',
array(
'render_callback' => function() use ( &$in_the_loop_logs ) {
$in_the_loop_logs[] = in_the_loop();
return '';
},
)
);
}

/**
* Unregisters the test block registered by the `register_in_the_loop_logger_block()` method.
*/
private function unregister_in_the_loop_logger_block() {
unregister_block_type( 'test/in-the-loop-logger' );
}
}