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
49 changes: 49 additions & 0 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -2926,6 +2926,55 @@ function wp_update_comment_count_now( $post_id ) {
return true;
}

/**
* Recalculates the stored comment count for one or more posts.
*
* Each post's `comment_count` is recomputed with wp_update_comment_count_now(),
* so the result honors the {@see 'default_excluded_comment_types'} filter.
*
* This is the recount counterpart to wp_update_comment_count(): the stored count
* is only refreshed for a post when its comments change, so an existing count can
* become stale after the set of excluded comment types changes (for example when
* a plugin registers a type that opts out of default listings). A plugin that
* changes that set should call this once, typically from its activation routine,
* the same way rewrite rules are flushed with flush_rewrite_rules().
*
* Recalculating every post is proportional to the number of posts that have
* comments and can be expensive on large sites. Pass a specific list of post IDs
* to limit the work, or run it from a maintenance context such as WP-CLI.
*
* @since 7.1.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param int[]|int|null $post_ids Optional. Post ID or array of post IDs to recalculate.
* Default null, which recalculates every post that has
* at least one comment.
* @return int Number of posts whose comment count was recalculated.
*/
function wp_update_comment_counts( $post_ids = null ) {
global $wpdb;

if ( null === $post_ids ) {
$post_ids = $wpdb->get_col( "SELECT DISTINCT comment_post_ID FROM $wpdb->comments" );
}

$post_ids = array_unique( array_filter( array_map( 'absint', (array) $post_ids ) ) );

if ( empty( $post_ids ) ) {
return 0;
}

$recalculated = 0;
foreach ( $post_ids as $post_id ) {
if ( wp_update_comment_count_now( $post_id ) ) {
++$recalculated;
}
}

return $recalculated;
}

//
// Ping and trackback functions.
//
Expand Down
143 changes: 143 additions & 0 deletions tests/phpunit/tests/comment/wpUpdateCommentCounts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

/**
* @group comment
*
* @covers ::wp_update_comment_counts
*/
class Tests_Comment_wpUpdateCommentCounts extends WP_UnitTestCase {

/**
* Overwrites a post's stored comment_count to simulate a stale value.
*
* @param int $post_id Post ID.
* @param int $count Stored comment count to write.
*/
private function set_stored_count( $post_id, $count ) {
global $wpdb;

$wpdb->update( $wpdb->posts, array( 'comment_count' => $count ), array( 'ID' => $post_id ) );
clean_post_cache( $post_id );
}

/**
* @ticket 65537
*/
public function test_returns_zero_when_there_is_nothing_to_recalculate() {
$this->assertSame( 0, wp_update_comment_counts( array() ) );
$this->assertSame( 0, wp_update_comment_counts( 0 ) );
}

/**
* @ticket 65537
*/
public function test_recalculates_only_the_given_posts() {
$post_a = self::factory()->post->create();
$post_b = self::factory()->post->create();
self::factory()->comment->create(
array(
'comment_post_ID' => $post_a,
'comment_approved' => 1,
)
);
self::factory()->comment->create(
array(
'comment_post_ID' => $post_b,
'comment_approved' => 1,
)
);

// Corrupt both stored counts, then recalculate only the first post.
$this->set_stored_count( $post_a, 99 );
$this->set_stored_count( $post_b, 99 );

$recalculated = wp_update_comment_counts( $post_a );

$this->assertSame( 1, $recalculated );
$this->assertSame( '1', get_comments_number( $post_a ) );
$this->assertSame( '99', get_comments_number( $post_b ) );
}

/**
* @ticket 65537
*/
public function test_deduplicates_repeated_post_ids() {
$post_id = self::factory()->post->create();
self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_approved' => 1,
)
);
$this->set_stored_count( $post_id, 99 );

$recalculated = wp_update_comment_counts( array( $post_id, $post_id ) );

$this->assertSame( 1, $recalculated );
$this->assertSame( '1', get_comments_number( $post_id ) );
}

/**
* @ticket 65537
*/
public function test_null_recalculates_all_posts_with_comments() {
$post_a = self::factory()->post->create();
$post_b = self::factory()->post->create();
self::factory()->comment->create(
array(
'comment_post_ID' => $post_a,
'comment_approved' => 1,
)
);
self::factory()->comment->create(
array(
'comment_post_ID' => $post_b,
'comment_approved' => 1,
)
);
$this->set_stored_count( $post_a, 99 );
$this->set_stored_count( $post_b, 99 );

wp_update_comment_counts();

$this->assertSame( '1', get_comments_number( $post_a ) );
$this->assertSame( '1', get_comments_number( $post_b ) );
}

/**
* The headline scenario: a type that joins the excluded set after comments
* already exist must not keep inflating a previously stored count.
*
* @ticket 65537
*/
public function test_recalculation_drops_a_newly_excluded_type() {
$post_id = self::factory()->post->create();
self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'review',
'comment_approved' => 1,
)
);

// Baseline: 'review' is counted, so the stored count is 1.
wp_update_comment_count_now( $post_id );
$this->assertSame( '1', get_comments_number( $post_id ) );

// A plugin now excludes 'review'. The stored count stays stale until a recount.
$filter = static function ( $types ) {
$types[] = 'review';
return $types;
};
add_filter( 'default_excluded_comment_types', $filter );

$this->assertSame( '1', get_comments_number( $post_id ) );

$recalculated = wp_update_comment_counts( $post_id );

remove_filter( 'default_excluded_comment_types', $filter );

$this->assertSame( 1, $recalculated );
$this->assertSame( '0', get_comments_number( $post_id ) );
}
}
Loading