Force comment pagination on single posts.

Previously, the 'page_comments' toggle allowed users to disable comment
pagination. This toggle was only superficial, however. Even with
'page_comments' turned on, `comments_template()` loaded all of a post's
comments into memory, and passed them to `wp_list_comments()` and
`Walker_Comment`, the latter of which produced markup for only the
current page of comments. In other words, it was possible to enable
'page_comments', thereby showing only a subset of a post's comments on a given
page, but all comments continued to be loaded in the background. This technique
scaled poorly. Posts with hundreds or thousands of comments would load slowly,
or not at all, even when the 'comments_per_page' setting was set to a
reasonable number.

Recent changesets have addressed this problem through more efficient tree-
walking, better descendant caching, and more selective queries for top-level
post comments. The current changeset completes the project by addressing the
root issue: that loading a post causes all of its comments to be loaded too.

Here's the breakdown:

* Comment pagination is now forced. Setting 'page_comments' to false leads to evil things when you have many comments. If you want to avoid pagination, set 'comments_per_page' to something high.
* The 'page_comments' setting has been expunged from options-discussion.php, and from places in the codebase where it was referenced. For plugins relying on 'page_comments', we now force the value to `true` with a `pre_option` filter.
* `comments_template()` now queries for an appropriately small number of comments. Usually, this means the `comments_per_page` value.
* To preserve the current (odd) behavior for comment pagination links, some unholy hacks have been inserted into `comments_template()`. The ugliness is insulated in this function for backward compatibility and to minimize collateral damage. A side-effect is that, for certain settings of 'default_comments_page', up to 2x the value of `comments_per_page` might be fetched at a time.
* In support of these changes, a `$format` parameter has been added to `WP_Comment::get_children()`. This param allows you to request a flattened array of comment children, suitable for feeding into `Walker_Comment`.
* `WP_Query` loops are now informed about total available comment counts and comment pages by the `WP_Comment_Query` (`found_comments`, `max_num_pages`), instead of by `Walker_Comment`.

Aside from radical performance improvements in the case of a post with many
comments, this changeset fixes a bug that caused the first page of comments to
be partial (`found_comments` % `comments_per_page`), rather than the last, as
you'd expect.

Props boonebgorges, wonderboymusic.
Fixes #8071.
Built from https://develop.svn.wordpress.org/trunk@34561


git-svn-id: http://core.svn.wordpress.org/trunk@34525 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Boone Gorges 2015-09-25 20:40:25 +00:00
parent 66242cbc98
commit 18d6b3c8dc
11 changed files with 123 additions and 25 deletions

View File

@ -482,7 +482,7 @@ function populate_options() {
'close_comments_days_old' => 14, 'close_comments_days_old' => 14,
'thread_comments' => 1, 'thread_comments' => 1,
'thread_comments_depth' => 5, 'thread_comments_depth' => 5,
'page_comments' => 0, 'page_comments' => 1,
'comments_per_page' => 50, 'comments_per_page' => 50,
'default_comments_page' => 'newest', 'default_comments_page' => 'newest',
'comment_order' => 'asc', 'comment_order' => 'asc',

View File

@ -98,17 +98,14 @@ printf( __('Enable threaded (nested) comments %s levels deep'), $thread_comments
?></label> ?></label>
<br /> <br />
<label for="page_comments">
<input name="page_comments" type="checkbox" id="page_comments" value="1" <?php checked('1', get_option('page_comments')); ?> />
<?php <?php
$default_comments_page = '</label><label for="default_comments_page"><select name="default_comments_page" id="default_comments_page"><option value="newest"'; $default_comments_page = '</label><label for="default_comments_page"><select name="default_comments_page" id="default_comments_page"><option value="newest"';
if ( 'newest' == get_option('default_comments_page') ) $default_comments_page .= ' selected="selected"'; if ( 'newest' == get_option('default_comments_page') ) $default_comments_page .= ' selected="selected"';
$default_comments_page .= '>' . __('last') . '</option><option value="oldest"'; $default_comments_page .= '>' . __('last') . '</option><option value="oldest"';
if ( 'oldest' == get_option('default_comments_page') ) $default_comments_page .= ' selected="selected"'; if ( 'oldest' == get_option('default_comments_page') ) $default_comments_page .= ' selected="selected"';
$default_comments_page .= '>' . __('first') . '</option></select>'; $default_comments_page .= '>' . __('first') . '</option></select>';
printf( __('Break comments into pages with %1$s top level comments per page and the %2$s page displayed by default'), '</label><label for="comments_per_page"><input name="comments_per_page" type="number" step="1" min="0" id="comments_per_page" value="' . esc_attr(get_option('comments_per_page')) . '" class="small-text" />', $default_comments_page ); printf( __('Break comments into pages with %1$s top level comments per page and the %2$s page displayed by default'), '<label for="comments_per_page"><input name="comments_per_page" type="number" step="1" min="0" id="comments_per_page" value="' . esc_attr(get_option('comments_per_page')) . '" class="small-text" />', $default_comments_page );
?></label> ?></label>
<br /> <br />

View File

@ -83,7 +83,7 @@ if ( is_multisite() && ! is_super_admin() && 'update' != $action ) {
$whitelist_options = array( $whitelist_options = array(
'general' => array( 'blogname', 'blogdescription', 'gmt_offset', 'date_format', 'time_format', 'start_of_week', 'timezone_string', 'WPLANG' ), 'general' => array( 'blogname', 'blogdescription', 'gmt_offset', 'date_format', 'time_format', 'start_of_week', 'timezone_string', 'WPLANG' ),
'discussion' => array( 'default_pingback_flag', 'default_ping_status', 'default_comment_status', 'comments_notify', 'moderation_notify', 'comment_moderation', 'require_name_email', 'comment_whitelist', 'comment_max_links', 'moderation_keys', 'blacklist_keys', 'show_avatars', 'avatar_rating', 'avatar_default', 'close_comments_for_old_posts', 'close_comments_days_old', 'thread_comments', 'thread_comments_depth', 'page_comments', 'comments_per_page', 'default_comments_page', 'comment_order', 'comment_registration' ), 'discussion' => array( 'default_pingback_flag', 'default_ping_status', 'default_comment_status', 'comments_notify', 'moderation_notify', 'comment_moderation', 'require_name_email', 'comment_whitelist', 'comment_max_links', 'moderation_keys', 'blacklist_keys', 'show_avatars', 'avatar_rating', 'avatar_default', 'close_comments_for_old_posts', 'close_comments_days_old', 'thread_comments', 'thread_comments_depth', 'comments_per_page', 'default_comments_page', 'comment_order', 'comment_registration' ),
'media' => array( 'thumbnail_size_w', 'thumbnail_size_h', 'thumbnail_crop', 'medium_size_w', 'medium_size_h', 'large_size_w', 'large_size_h', 'image_default_size', 'image_default_align', 'image_default_link_type' ), 'media' => array( 'thumbnail_size_w', 'thumbnail_size_h', 'thumbnail_crop', 'medium_size_w', 'medium_size_h', 'large_size_w', 'large_size_h', 'image_default_size', 'image_default_align', 'image_default_link_type' ),
'reading' => array( 'posts_per_page', 'posts_per_rss', 'rss_use_excerpt', 'show_on_front', 'page_on_front', 'page_for_posts', 'blog_public' ), 'reading' => array( 'posts_per_page', 'posts_per_rss', 'rss_use_excerpt', 'show_on_front', 'page_on_front', 'page_for_posts', 'blog_public' ),
'writing' => array( 'default_category', 'default_email_category', 'default_link_category', 'default_post_format' ) 'writing' => array( 'default_category', 'default_email_category', 'default_link_category', 'default_post_format' )

View File

@ -321,7 +321,7 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) {
} }
} }
if ( get_option('page_comments') && ( ( 'newest' == get_option('default_comments_page') && get_query_var('cpage') > 0 ) || ( 'newest' != get_option('default_comments_page') && get_query_var('cpage') > 1 ) ) ) { if ( ( 'newest' == get_option('default_comments_page') && get_query_var('cpage') > 0 ) || ( 'newest' != get_option('default_comments_page') && get_query_var('cpage') > 1 ) ) {
$addl_path = ( !empty( $addl_path ) ? trailingslashit($addl_path) : '' ) . user_trailingslashit( $wp_rewrite->comments_pagination_base . '-' . get_query_var('cpage'), 'commentpaged' ); $addl_path = ( !empty( $addl_path ) ? trailingslashit($addl_path) : '' ) . user_trailingslashit( $wp_rewrite->comments_pagination_base . '-' . get_query_var('cpage'), 'commentpaged' );
$redirect['query'] = remove_query_arg( 'cpage', $redirect['query'] ); $redirect['query'] = remove_query_arg( 'cpage', $redirect['query'] );
} }

View File

@ -865,6 +865,10 @@ class WP_Comment_Query {
$level = 0; $level = 0;
do { do {
$parent_ids = $levels[ $level ]; $parent_ids = $levels[ $level ];
if ( ! $parent_ids ) {
break;
}
$where = 'WHERE ' . implode( ' AND ', $where_clauses ) . ' AND comment_parent IN (' . implode( ',', array_map( 'intval', $parent_ids ) ) . ')'; $where = 'WHERE ' . implode( ' AND ', $where_clauses ) . ' AND comment_parent IN (' . implode( ',', array_map( 'intval', $parent_ids ) ) . ')';
$comment_ids = $wpdb->get_col( "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']}" ); $comment_ids = $wpdb->get_col( "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']}" );

View File

@ -227,9 +227,11 @@ final class WP_Comment {
* @since 4.4.0 * @since 4.4.0
* @access public * @access public
* *
* @param string $format Return value format. 'tree' for a hierarchical tree, 'flat' for a flattened array.
* Default 'tree'.
* @return array Array of `WP_Comment` objects. * @return array Array of `WP_Comment` objects.
*/ */
public function get_children() { public function get_children( $format = 'tree' ) {
if ( is_null( $this->children ) ) { if ( is_null( $this->children ) ) {
$this->children = get_comments( array( $this->children = get_comments( array(
'parent' => $this->comment_ID, 'parent' => $this->comment_ID,
@ -237,7 +239,16 @@ final class WP_Comment {
) ); ) );
} }
return $this->children; if ( 'flat' === $format ) {
$children = array();
foreach ( $this->children as $child ) {
$children = array_merge( $children, array( $child ), $child->get_children( 'flat' ) );
}
} else {
$children = $this->children;
}
return $children;
} }
/** /**

View File

@ -807,9 +807,6 @@ function get_comment_pages_count( $comments = null, $per_page = null, $threaded
if ( empty($comments) ) if ( empty($comments) )
return 0; return 0;
if ( ! get_option( 'page_comments' ) )
return 1;
if ( !isset($per_page) ) if ( !isset($per_page) )
$per_page = (int) get_query_var('comments_per_page'); $per_page = (int) get_query_var('comments_per_page');
if ( 0 === $per_page ) if ( 0 === $per_page )
@ -858,7 +855,7 @@ function get_page_of_comment( $comment_ID, $args = array() ) {
$defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' ); $defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' );
$args = wp_parse_args( $args, $defaults ); $args = wp_parse_args( $args, $defaults );
if ( '' === $args['per_page'] && get_option('page_comments') ) if ( '' === $args['per_page'] )
$args['per_page'] = get_query_var('comments_per_page'); $args['per_page'] = get_query_var('comments_per_page');
if ( empty($args['per_page']) ) { if ( empty($args['per_page']) ) {
$args['per_page'] = 0; $args['per_page'] = 0;

View File

@ -683,7 +683,7 @@ function get_comment_link( $comment = null, $args = array() ) {
$defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' ); $defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' );
$args = wp_parse_args( $args, $defaults ); $args = wp_parse_args( $args, $defaults );
if ( '' === $args['per_page'] && get_option('page_comments') ) if ( '' === $args['per_page'] )
$args['per_page'] = get_option('comments_per_page'); $args['per_page'] = get_option('comments_per_page');
if ( empty($args['per_page']) ) { if ( empty($args['per_page']) ) {
@ -1214,10 +1214,11 @@ function comments_template( $file = '/comments.php', $separate_comments = false
$comment_author_url = esc_url($commenter['comment_author_url']); $comment_author_url = esc_url($commenter['comment_author_url']);
$comment_args = array( $comment_args = array(
'order' => 'ASC',
'orderby' => 'comment_date_gmt', 'orderby' => 'comment_date_gmt',
'status' => 'approve', 'status' => 'approve',
'post_id' => $post->ID, 'post_id' => $post->ID,
'hierarchical' => 'threaded',
'no_found_rows' => false,
'update_comment_meta_cache' => false, // We lazy-load comment meta for performance. 'update_comment_meta_cache' => false, // We lazy-load comment meta for performance.
); );
@ -1227,7 +1228,87 @@ function comments_template( $file = '/comments.php', $separate_comments = false
$comment_args['include_unapproved'] = array( $comment_author_email ); $comment_args['include_unapproved'] = array( $comment_author_email );
} }
$comments = get_comments( $comment_args ); $per_page = (int) get_query_var( 'comments_per_page' );
if ( 0 === $per_page ) {
$per_page = (int) get_option( 'comments_per_page' );
}
$flip_comment_order = $trim_comments_on_page = false;
if ( $post->comment_count > $per_page ) {
$comment_args['number'] = $per_page;
/*
* For legacy reasons, higher page numbers always mean more recent comments, regardless of sort order.
* Since we don't have full pagination info until after the query, we use some tricks to get the
* right comments for the current page.
*
* Abandon all hope, ye who enter here!
*/
$page = (int) get_query_var( 'cpage' );
if ( 'newest' === get_option( 'default_comments_page' ) ) {
if ( $page ) {
$comment_args['order'] = 'ASC';
/*
* We don't have enough data (namely, the total number of comments) to calculate an
* exact offset. We'll fetch too many comments, and trim them as needed
* after the query.
*/
$offset = ( $page - 2 ) * $per_page;
if ( 0 > $offset ) {
// `WP_Comment_Query` doesn't support negative offsets.
$comment_args['offset'] = 0;
} else {
$comment_args['offset'] = $offset;
}
// Fetch double the number of comments we need.
$comment_args['number'] += $per_page;
$trim_comments_on_page = true;
} else {
$comment_args['order'] = 'DESC';
$comment_args['offset'] = 0;
$flip_comment_order = true;
}
} else {
$comment_args['order'] = 'ASC';
if ( $page ) {
$comment_args['offset'] = ( $page - 1 ) * $per_page;
} else {
$comment_args['offset'] = 0;
}
}
}
$comment_query = new WP_Comment_Query( $comment_args );
$_comments = $comment_query->comments;
// Delightful pagination quirk #1: first page of results sometimes needs reordering.
if ( $flip_comment_order ) {
$_comments = array_reverse( $_comments );
}
// Delightful pagination quirk #2: reverse chronological order requires page shifting.
if ( $trim_comments_on_page ) {
// Correct the value of max_num_pages, which is wrong because we manipulated the per_page 'number'.
$comment_query->max_num_pages = ceil( $comment_query->found_comments / $per_page );
// Identify the number of comments that should appear on page 1.
$page_1_count = $comment_query->found_comments - ( ( $comment_query->max_num_pages - 1 ) * $per_page );
// Use that value to shift the matched comments.
if ( 1 === $page ) {
$_comments = array_slice( $_comments, 0, $page_1_count );
} else {
$_comments = array_slice( $_comments, $page_1_count, $per_page );
}
}
// Trees must be flattened before they're passed to the walker.
$comments_flat = array();
foreach ( $_comments as $_comment ) {
$comments_flat = array_merge( $comments_flat, array( $_comment ), $_comment->get_children( 'flat' ) );
}
/** /**
* Filter the comments array. * Filter the comments array.
@ -1237,9 +1318,10 @@ function comments_template( $file = '/comments.php', $separate_comments = false
* @param array $comments Array of comments supplied to the comments template. * @param array $comments Array of comments supplied to the comments template.
* @param int $post_ID Post ID. * @param int $post_ID Post ID.
*/ */
$wp_query->comments = apply_filters( 'comments_array', $comments, $post->ID ); $wp_query->comments = apply_filters( 'comments_array', $comments_flat, $post->ID );
$comments = &$wp_query->comments; $comments = &$wp_query->comments;
$wp_query->comment_count = count($wp_query->comments); $wp_query->comment_count = count($wp_query->comments);
$wp_query->max_num_comment_pages = $comment_query->max_num_pages;
if ( $separate_comments ) { if ( $separate_comments ) {
$wp_query->comments_by_type = separate_comments($comments); $wp_query->comments_by_type = separate_comments($comments);
@ -1249,7 +1331,7 @@ function comments_template( $file = '/comments.php', $separate_comments = false
} }
$overridden_cpage = false; $overridden_cpage = false;
if ( '' == get_query_var('cpage') && get_option('page_comments') ) { if ( '' == get_query_var('cpage') ) {
set_query_var( 'cpage', 'newest' == get_option('default_comments_page') ? get_comment_pages_count() : 1 ); set_query_var( 'cpage', 'newest' == get_option('default_comments_page') ? get_comment_pages_count() : 1 );
$overridden_cpage = true; $overridden_cpage = true;
} }
@ -1825,9 +1907,14 @@ function wp_list_comments( $args = array(), $comments = null ) {
} else { } else {
$_comments = $wp_query->comments; $_comments = $wp_query->comments;
} }
// Pagination is already handled by `WP_Comment_Query`, so we tell Walker not to bother.
if ( 1 < $wp_query->max_num_comment_pages ) {
$r['page'] = 1;
}
} }
if ( '' === $r['per_page'] && get_option('page_comments') ) if ( '' === $r['per_page'] )
$r['per_page'] = get_query_var('comments_per_page'); $r['per_page'] = get_query_var('comments_per_page');
if ( empty($r['per_page']) ) { if ( empty($r['per_page']) ) {
@ -1866,7 +1953,6 @@ function wp_list_comments( $args = array(), $comments = null ) {
} }
$output = $walker->paged_walk( $_comments, $r['max_depth'], $r['page'], $r['per_page'], $r ); $output = $walker->paged_walk( $_comments, $r['max_depth'], $r['page'], $r['per_page'], $r );
$wp_query->max_num_comment_pages = $walker->max_pages;
$in_comment_loop = false; $in_comment_loop = false;

View File

@ -316,6 +316,9 @@ add_filter( 'default_option_link_manager_enabled', '__return_true' );
// This option no longer exists; tell plugins we always support auto-embedding. // This option no longer exists; tell plugins we always support auto-embedding.
add_filter( 'default_option_embed_autourls', '__return_true' ); add_filter( 'default_option_embed_autourls', '__return_true' );
// This option no longer exists; tell plugins we want comment pagination.
add_filter( 'pre_option_page_comments', '__return_true' );
// Default settings for heartbeat // Default settings for heartbeat
add_filter( 'heartbeat_settings', 'wp_heartbeat_settings' ); add_filter( 'heartbeat_settings', 'wp_heartbeat_settings' );

View File

@ -2590,7 +2590,7 @@ function get_comments_pagenum_link( $pagenum = 1, $max_page = 0 ) {
function get_next_comments_link( $label = '', $max_page = 0 ) { function get_next_comments_link( $label = '', $max_page = 0 ) {
global $wp_query; global $wp_query;
if ( !is_singular() || !get_option('page_comments') ) if ( ! is_singular() )
return; return;
$page = get_query_var('cpage'); $page = get_query_var('cpage');
@ -2644,7 +2644,7 @@ function next_comments_link( $label = '', $max_page = 0 ) {
* @return string|void HTML-formatted link for the previous page of comments. * @return string|void HTML-formatted link for the previous page of comments.
*/ */
function get_previous_comments_link( $label = '' ) { function get_previous_comments_link( $label = '' ) {
if ( !is_singular() || !get_option('page_comments') ) if ( ! is_singular() )
return; return;
$page = get_query_var('cpage'); $page = get_query_var('cpage');
@ -2692,7 +2692,7 @@ function previous_comments_link( $label = '' ) {
function paginate_comments_links($args = array()) { function paginate_comments_links($args = array()) {
global $wp_rewrite; global $wp_rewrite;
if ( !is_singular() || !get_option('page_comments') ) if ( ! is_singular() )
return; return;
$page = get_query_var('cpage'); $page = get_query_var('cpage');
@ -2737,7 +2737,7 @@ function get_the_comments_navigation( $args = array() ) {
$navigation = ''; $navigation = '';
// Are there comments to navigate through? // Are there comments to navigate through?
if ( get_comment_pages_count() > 1 && get_option( 'page_comments' ) ) { if ( get_comment_pages_count() > 1 ) {
$args = wp_parse_args( $args, array( $args = wp_parse_args( $args, array(
'prev_text' => __( 'Older comments' ), 'prev_text' => __( 'Older comments' ),
'next_text' => __( 'Newer comments' ), 'next_text' => __( 'Newer comments' ),

View File

@ -4,7 +4,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '4.4-alpha-34560'; $wp_version = '4.4-alpha-34561';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.