Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b7cb7fc
Comments: Add filter for comment types excluded from queries by default
adamsilverstein Jun 24, 2026
4b57b1a
Apply suggestion from @adamsilverstein
adamsilverstein Jun 24, 2026
9e9fb0b
Apply suggestion from @adamsilverstein
adamsilverstein Jun 24, 2026
6e67226
Apply suggestion from @adamsilverstein
adamsilverstein Jun 24, 2026
237d916
Apply suggestion from @adamsilverstein
adamsilverstein Jun 24, 2026
bb238e9
Comments: Clarify the default_excluded_comment_types filter is not ac…
adamsilverstein Jun 25, 2026
1a4bf12
Comments: Feed default_excluded_comment_types into the comment counter.
adamsilverstein Jun 25, 2026
2f75e55
Comments: Honor default_excluded_comment_types in pending counts.
adamsilverstein Jun 25, 2026
e656cbd
Comments: Extract wp_get_default_excluded_comment_types().
adamsilverstein Jul 11, 2026
9355bd4
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 17, 2026
e0d3cf9
Comments: Treat aliased type requests as explicit against excluded ty…
adamsilverstein Jul 17, 2026
4f69762
Comments: Keep a comment type named '0' in the excluded types list.
adamsilverstein Jul 17, 2026
0ded145
Comments: Drop the unsupported "feeds" claim from the filter docblock.
adamsilverstein Jul 17, 2026
c082ed3
Comments: Apply the excluded types filter in the comments list table.
adamsilverstein Jul 17, 2026
bebcf94
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 23, 2026
91e2876
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 24, 2026
b3fc3ef
Comments: Drop non-scalar values from the excluded comment types filter.
adamsilverstein Jul 24, 2026
e658376
Potential fix for pull request finding
adamsilverstein Jul 24, 2026
3e371d0
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 24, 2026
3d507a5
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 24, 2026
29af50b
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 25, 2026
e0e1c5e
Comments: Let the list table show an explicitly requested excluded type.
adamsilverstein Jul 25, 2026
106def5
Merge branch 'trunk' into feature/65537-excluded-comment-types-filter
adamsilverstein Jul 26, 2026
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
30 changes: 29 additions & 1 deletion src/wp-admin/includes/class-wp-comments-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,40 @@ public function prepare_items() {
$comment_status = 'all';
}

// The 'note' type is never listed here, so it is dropped from the request.
$comment_type = '';

if ( ! empty( $_REQUEST['comment_type'] ) && 'note' !== $_REQUEST['comment_type'] ) {
$comment_type = $_REQUEST['comment_type'];
}

/*
* WP_Comment_Query drops the default exclusions when 'all' types are
* requested, so they are also passed as 'type__not_in' to keep excluded
* types out of the list table in that case.
*
* The requested type is removed from that list, so a plugin that adds
* its own default-excluded type to the type dropdown via
* 'admin_comment_types_dropdown' can still list it. Type aliases are
* expanded first, matching how WP_Comment_Query resolves them.
*/
switch ( $comment_type ) {
case 'comment':
case 'comments':
$requested_types = array( '', 'comment' );
break;

case 'pings':
$requested_types = array( 'pingback', 'trackback' );
break;

default:
$requested_types = array( $comment_type );
break;
}

$excluded_types = array_values( array_diff( wp_get_default_excluded_comment_types(), $requested_types ) );

$search = $_REQUEST['s'] ?? '';

$post_type = ( isset( $_REQUEST['post_type'] ) ) ? sanitize_key( $_REQUEST['post_type'] ) : '';
Expand Down Expand Up @@ -155,7 +183,7 @@ public function prepare_items() {
'number' => $number,
'post_id' => $post_id,
'type' => $comment_type,
'type__not_in' => array( 'note' ),
'type__not_in' => $excluded_types,
'orderby' => $orderby,
'order' => $order,
Comment on lines 184 to 188

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in e0e1c5e.

I didn't drop type__not_in entirely though - the list table still has to force the exclusions for a type=all request. WP_Comment_Query skips its default exclusions when 'all' is requested, and there is an existing test (test_comments_list_table_does_not_show_note_comment_type, added for #64198/#64474) asserting notes stay hidden on edit-comments.php?comment_type=all. Removing the arg outright regresses that.

Instead the requested type is now subtracted from the exclusion list before the query runs, with the 'comment'/'comments'/'pings' aliases expanded the same way WP_Comment_Query expands them:

$excluded_types = array_values( array_diff( wp_get_default_excluded_comment_types(), $requested_types ) );

The 'note' type is still stripped from the request a few lines above, so that one remains unlistable in the admin regardless.

Added two tests in tests/phpunit/tests/admin/wpCommentsListTable.php: a filtered-in 'private' type stays hidden for both an empty and an all request, and it is listed when explicitly selected. The second one fails against the old code (0 items) and passes now.

'post_type' => $post_type,
Expand Down
16 changes: 15 additions & 1 deletion src/wp-admin/includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,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 @@ -242,7 +244,19 @@ 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 );
$excluded_types = wp_get_default_excluded_comment_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
42 changes: 35 additions & 7 deletions src/wp-includes/class-wp-comment-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,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 @@ -779,13 +780,40 @@ 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';
$excluded_types = wp_get_default_excluded_comment_types( $this );

// Unless all types are requested, exclude each default-excluded type
// that the query does not explicitly request. The special type tokens
// in the request ('comment', 'comments', 'pings') are first expanded to
// the literal comment_type values they represent, so a type requested
// via an alias (for example 'pings' for 'pingback' and 'trackback') is
// still treated as explicitly requested and is not excluded.
if ( ! in_array( 'all', $raw_types['IN'], true ) ) {
$requested_types = array();
foreach ( $raw_types['IN'] as $requested_type ) {
switch ( $requested_type ) {
case 'comment':
case 'comments':
$requested_types[] = '';
$requested_types[] = 'comment';
break;

case 'pings':
$requested_types[] = 'pingback';
$requested_types[] = 'trackback';
break;

default:
$requested_types[] = $requested_type;
break;
}
}

foreach ( $excluded_types as $excluded_type ) {
if ( ! in_array( $excluded_type, $requested_types, true ) ) {
$raw_types['NOT IN'][] = $excluded_type;
}
}
}

$comment_types = array();
Expand Down
90 changes: 89 additions & 1 deletion src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -2872,10 +2872,79 @@ function wp_update_comment_count( $post_id, $do_deferred = false ) {
return null;
}

/**
* Retrieves the comment types that are excluded from queries and counts by default.
*
* Applies the {@see 'default_excluded_comment_types'} filter and normalizes the
* result: non-scalar values are discarded, the rest are cast to strings, empties
* and duplicates are removed, and the special type tokens understood by
* WP_Comment_Query ('all', 'comment', 'comments', 'pings') are stripped - the
* filter deals in literal `comment_type` values only.
*
* @since 7.1.0
*
* @param WP_Comment_Query|null $query Optional. The current query instance when called
* from WP_Comment_Query, or null in counting
* contexts. Default null.
* @return string[] Comment types excluded by default.
*/
function wp_get_default_excluded_comment_types( $query = null ) {
/**
* 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 explicitly
Comment on lines +2891 to +2896
* includes the specific type via the 'type' or 'type__in' query variables.
*
* This allows plugins to keep comment types out of standard comment
* listings and counts 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() and when
* counting pending comments, so an excluded type does not inflate
* get_comments_number().
*
* Values must be literal `comment_type` values as stored in the database;
* the special type tokens understood by WP_Comment_Query ('all', 'comment',
* 'comments', 'pings') are ignored, as are values that are not scalar.
*
* Register callbacks for this filter unconditionally (for example on
* 'plugins_loaded' or 'init') rather than toggling them per call: query
* results are cached against the comment `last_changed` key, which does not
* account for this filter's output, so per-call toggling can serve stale
* results from the cache.
*
* 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, or null in counting
* contexts (recalculating a post's stored comment
* count, counting pending comments).
*/
$excluded_types = apply_filters( 'default_excluded_comment_types', array( 'note' ), $query );

// Drop values that cannot be cast to a string, so a stray object or array cannot error out.
$excluded_types = array_filter( (array) $excluded_types, 'is_scalar' );

$excluded_types = array_unique( array_filter( array_map( 'strval', $excluded_types ), 'strlen' ) );

// Strip the special type tokens so an alias cannot poison explicit-type queries.
return array_values( array_diff( $excluded_types, array( 'all', 'comment', 'comments', 'pings' ) ) );
}

/**
* Updates the comment count for the post.
*
* @since 2.5.0
* @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 Down Expand Up @@ -2914,7 +2983,26 @@ 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 ) );
$excluded_types = wp_get_default_excluded_comment_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,137 @@
<?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 );
}

/**
* A filter callback returning false degrades gracefully to no exclusions.
*
* Scalar returns are cast to an array and treated as a single excluded type;
* only values that normalize to an empty set disable the exclusions.
*
* @ticket 65537
*/
public function test_false_filter_return_counts_all_types() {
$post_id = self::factory()->post->create();
$this->make_pending( $post_id, 'note' );

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

$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