Skip to content
18 changes: 17 additions & 1 deletion src/wp-admin/includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ function get_comment_to_edit( $id ) {
*
* @since 2.3.0
* @since 6.9.0 Exclude the 'note' comment type from the count.
* @since 7.1.0 The excluded comment types are derived from the
* {@see 'default_excluded_comment_types'} filter.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
Expand All @@ -158,7 +160,21 @@ function get_pending_comments_num( $post_id ) {
$post_id_array = array_map( 'intval', $post_id_array );
$post_id_in = "'" . implode( "', '", $post_id_array ) . "'";

$pending = $wpdb->get_results( "SELECT comment_post_ID, COUNT(comment_ID) as num_comments FROM $wpdb->comments WHERE comment_post_ID IN ( $post_id_in ) AND comment_approved = '0' AND comment_type != 'note' GROUP BY comment_post_ID", ARRAY_A );
/** This filter is documented in wp-includes/class-wp-comment-query.php */
$excluded_types = apply_filters( 'default_excluded_comment_types', array( 'note' ), null );
$excluded_types = array_unique( array_filter( array_map( 'strval', (array) $excluded_types ) ) );

$type_not_in = '';
if ( $excluded_types ) {
$type_not_in = $wpdb->prepare(
sprintf( ' AND comment_type NOT IN ( %s )', implode( ', ', array_fill( 0, count( $excluded_types ), '%s' ) ) ),
$excluded_types
);
}

// $post_id_in is built from integers and $type_not_in is prepared above.
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$pending = $wpdb->get_results( "SELECT comment_post_ID, COUNT(comment_ID) as num_comments FROM $wpdb->comments WHERE comment_post_ID IN ( $post_id_in ) AND comment_approved = '0'$type_not_in GROUP BY comment_post_ID", ARRAY_A );

if ( $single ) {
if ( empty( $pending ) ) {
Expand Down
47 changes: 40 additions & 7 deletions src/wp-includes/class-wp-comment-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ public function get_comments() {
*
* @since 4.4.0
* @since 6.9.0 Excludes the 'note' comment type, unless 'all' or the 'note' types are requested.
* @since 7.1.0 The default-excluded comment types are filterable via {@see 'default_excluded_comment_types'}.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
Expand Down Expand Up @@ -771,13 +772,45 @@ protected function get_comment_ids() {
'NOT IN' => (array) $this->query_vars['type__not_in'],
);

// Exclude the 'note' comment type, unless 'all' types or the 'note' type explicitly are requested.
if (
! in_array( 'all', $raw_types['IN'], true ) &&
! in_array( 'note', $raw_types['IN'], true ) &&
! in_array( 'note', $raw_types['NOT IN'], true )
) {
$raw_types['NOT IN'][] = 'note';
/**
* Filters the comment types that are excluded from query results by default.
*
* Comment types in this list are omitted from `WP_Comment_Query` results
* unless the query explicitly requests the 'all' type, or requests the
* specific type via the 'type', 'type__in', or 'type__not_in' query
* variables.
*
* This allows plugins to keep comment types out of standard comment
* listings, counts, or feeds by default, without having to filter every
* query individually. The 'note' comment type, used by the editor, is
* excluded by default. The same set is applied when recalculating a
* post's stored comment count in wp_update_comment_count_now(), so an
* excluded type does not inflate get_comments_number().
*
* This exclusion is a default-visibility convenience, not an access-control
* mechanism: callers can still retrieve excluded types explicitly (for
* example with 'type' => 'all'), so do not rely on this filter to keep
* comment data private. Enforce capability checks wherever the data is
* displayed or exposed (for example over REST).
*
* @since 7.1.0
*
* @param string[] $excluded_types Comment types excluded from query results by default.
* Default array contains the 'note' type.
* @param WP_Comment_Query|null $query The WP_Comment_Query instance (passed by reference),
* or null when recalculating a post's comment count.
*/
$excluded_types = apply_filters_ref_array( 'default_excluded_comment_types', array( array( 'note' ), &$this ) );

// Exclude the default-excluded comment types, unless 'all' types or that type explicitly are requested.
foreach ( array_unique( (array) $excluded_types ) as $excluded_type ) {
if (
! in_array( 'all', $raw_types['IN'], true ) &&
! in_array( $excluded_type, $raw_types['IN'], true ) &&
! in_array( $excluded_type, $raw_types['NOT IN'], true )
) {
$raw_types['NOT IN'][] = $excluded_type;
}
}

$comment_types = array();
Expand Down
23 changes: 22 additions & 1 deletion src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -2876,7 +2876,28 @@ 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 ) );
/** This filter is documented in wp-includes/class-wp-comment-query.php */
$excluded_types = apply_filters( 'default_excluded_comment_types', array( 'note' ), null );
$excluded_types = array_unique( array_filter( array_map( 'strval', (array) $excluded_types ) ) );

if ( $excluded_types ) {
$new = (int) $wpdb->get_var(
$wpdb->prepare(
sprintf(
"SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %%d AND comment_approved = '1' AND comment_type NOT IN (%s)",
implode( ', ', array_fill( 0, count( $excluded_types ), '%s' ) )
),
array_merge( array( $post_id ), $excluded_types )
)
);
} else {
$new = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1'",
$post_id
)
);
}
} else {
$new = (int) $new;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

/**
* @group admin
* @group comment
*
* @covers ::get_pending_comments_num
*/
class Admin_Includes_Comment_GetPendingCommentsNum_Test extends WP_UnitTestCase {

/**
* Creates a pending (unapproved) comment of a given type on a post.
*
* @param int $post_id Post to attach the comment to.
* @param string $comment_type Comment type slug.
* @return int The new comment ID.
*/
private function make_pending( $post_id, $comment_type = 'comment' ) {
return self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => $comment_type,
'comment_approved' => '0',
)
);
}

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

$this->assertSame( 1, get_pending_comments_num( $post_id ) );
}

/**
* @ticket 65537
*/
public function test_excludes_note_type_by_default() {
$post_id = self::factory()->post->create();
$this->make_pending( $post_id );
$this->make_pending( $post_id, 'note' );

$this->assertSame( 1, get_pending_comments_num( $post_id ) );
}

/**
* A type added to the excluded set must drop out of the pending count.
*
* @ticket 65537
*/
public function test_excludes_a_filtered_type() {
$post_id = self::factory()->post->create();
$this->make_pending( $post_id );
$this->make_pending( $post_id, 'review' );

// 'review' is counted by default.
$this->assertSame( 2, get_pending_comments_num( $post_id ) );

$filter = static function ( $types ) {
$types[] = 'review';
return $types;
};
add_filter( 'default_excluded_comment_types', $filter );
$num = get_pending_comments_num( $post_id );
remove_filter( 'default_excluded_comment_types', $filter );

$this->assertSame( 1, $num );
}

/**
* The exclusion is filter-driven, not a hard-coded 'note' literal.
*
* @ticket 65537
*/
public function test_emptying_filter_counts_note_type() {
$post_id = self::factory()->post->create();
$this->make_pending( $post_id, 'note' );

$this->assertSame( 0, get_pending_comments_num( $post_id ) );

add_filter( 'default_excluded_comment_types', '__return_empty_array' );
$num = get_pending_comments_num( $post_id );
remove_filter( 'default_excluded_comment_types', '__return_empty_array' );

$this->assertSame( 1, $num );
}

/**
* @ticket 65537
*/
public function test_array_input_returns_counts_keyed_by_post() {
$post_a = self::factory()->post->create();
$post_b = self::factory()->post->create();
$this->make_pending( $post_a );
$this->make_pending( $post_a, 'note' );
$this->make_pending( $post_b );
$this->make_pending( $post_b );

$counts = get_pending_comments_num( array( $post_a, $post_b ) );

$this->assertSame(
array(
$post_a => 1,
$post_b => 2,
),
$counts
);
}
}
Loading
Loading