diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 0f102d1ea80ee..178528926d909 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -2875,7 +2875,87 @@ function wp_update_comment_count_now( $post_id ) { $new = apply_filters( 'pre_wp_update_comment_count_now', null, $old, $post_id ); if ( is_null( $new ) ) { - $new = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type != 'note'", $post_id ) ); + $comments = (array) $wpdb->get_results( $wpdb->prepare( "SELECT comment_ID, comment_parent, comment_approved FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_type != 'note'", $post_id ) ); + $comments_by_id = array(); + + // Create a lookup array by comment ID. + foreach ( $comments as $comment ) { + $comments_by_id[ $comment->comment_ID ] = $comment; + } + + $comment_count = 0; + $ancestor_approval_cache = array(); + + foreach ( $comments as $comment ) { + if ( '1' !== $comment->comment_approved ) { + $ancestor_approval_cache[ (int) $comment->comment_ID ] = false; + } + } + + foreach ( $comments as $comment ) { + + if ( '1' !== $comment->comment_approved ) { + continue; + } + + $comment_id = (int) $comment->comment_ID; + + // Use cached result if this comment's ancestry was already resolved. + if ( isset( $ancestor_approval_cache[ $comment_id ] ) ) { + if ( $ancestor_approval_cache[ $comment_id ] ) { + ++$comment_count; + } + continue; + } + + // Walk the ancestor chain, collecting IDs to memoize afterwards. + $chain = array( $comment_id ); + $visited = array( + $comment_id => true, + ); + $parent_id = (int) $comment->comment_parent; + $has_unapproved = false; + + while ( 0 !== $parent_id ) { + if ( isset( $visited[ $parent_id ] ) ) { + $has_unapproved = true; + break; + } + + if ( isset( $ancestor_approval_cache[ $parent_id ] ) ) { + if ( ! $ancestor_approval_cache[ $parent_id ] ) { + $has_unapproved = true; + } + break; + } + + if ( ! isset( $comments_by_id[ $parent_id ] ) ) { + $has_unapproved = true; + break; + } + + $parent_comment = $comments_by_id[ $parent_id ]; + + if ( '1' !== $parent_comment->comment_approved ) { + $has_unapproved = true; + break; + } + + $visited[ $parent_id ] = true; + $chain[] = $parent_id; + $parent_id = (int) $parent_comment->comment_parent; + } + + foreach ( $chain as $chain_id ) { + $ancestor_approval_cache[ $chain_id ] = ! $has_unapproved; + } + + if ( ! $has_unapproved ) { + ++$comment_count; + } + } + + $new = $comment_count; } else { $new = (int) $new; } diff --git a/tests/phpunit/tests/comment/wpUpdateCommentCountNow.php b/tests/phpunit/tests/comment/wpUpdateCommentCountNow.php index 9dbb1f244ccf8..09fde6c37f06b 100644 --- a/tests/phpunit/tests/comment/wpUpdateCommentCountNow.php +++ b/tests/phpunit/tests/comment/wpUpdateCommentCountNow.php @@ -86,4 +86,126 @@ public function test_only_approved_regular_comments_are_counted() { public function _return_100() { return 100; } + + /** + * Test case where a trashed parent comment causes its child comments to be excluded from the comment count. + * + * @ticket 36409 + */ + public function test_trashed_parent_comment_excludes_child_comments_from_count() { + $post_id = self::factory()->post->create(); + + // Create 2 top-level comments, 2 child comments for the first top-level comment, and a grandchild of that first comment. + $parent_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + + $child_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_parent' => $parent_comment_id, + 'comment_approved' => 1, + ) + ); + + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_parent' => $parent_comment_id, + 'comment_approved' => 1, + ) + ); + + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_parent' => $child_comment_id, + 'comment_approved' => 1, + ) + ); + + $this->assertTrue( wp_update_comment_count_now( $post_id ) ); + $this->assertSame( '5', get_comments_number( $post_id ) ); + + wp_update_comment( + array( + 'comment_ID' => $parent_comment_id, + 'comment_approved' => 'trash', + ) + ); + + $this->assertTrue( wp_update_comment_count_now( $post_id ) ); + $this->assertSame( '1', get_comments_number( $post_id ) ); + } + + /** + * Test case where an unapproved parent comment causes its child comments to be excluded from the comment count. + * + * @ticket 36409 + */ + public function test_unapproved_parent_comment_excludes_child_comments_from_count() { + $post_id = self::factory()->post->create(); + + // Create 2 top-level comments, 2 child comments for the first top-level comment, and a grandchild of that first comment. + $parent_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + + $child_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_parent' => $parent_comment_id, + 'comment_approved' => 1, + ) + ); + + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_parent' => $parent_comment_id, + 'comment_approved' => 1, + ) + ); + + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_parent' => $child_comment_id, + 'comment_approved' => 1, + ) + ); + + $this->assertTrue( wp_update_comment_count_now( $post_id ) ); + $this->assertSame( '5', get_comments_number( $post_id ) ); + + wp_update_comment( + array( + 'comment_ID' => $parent_comment_id, + 'comment_approved' => '0', + ) + ); + + $this->assertTrue( wp_update_comment_count_now( $post_id ) ); + $this->assertSame( '1', get_comments_number( $post_id ) ); + } }