From 6a0dfab714dc66bf42027c0e63915aa53824c37d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 17:15:54 -0700 Subject: [PATCH 1/4] Comments: Generalize ping handling with an `is_ping` comment type flag. Pingbacks and trackbacks were singled out by hard-coded `comment_type` string comparisons in `separate_comments()` and `Walker_Comment`, so the "this is a ping, not a human comment" distinction could not be expressed by a registered comment type. Add an `is_ping` property to `WP_Comment_Type` (default false) and mark the built-in `pingback` and `trackback` types with it. `separate_comments()` now groups any registered ping type into the `pings` bucket, and `Walker_Comment::start_el()` renders any ping type with the compact ping markup. Built-in behaviour is unchanged. See #35214. --- src/wp-includes/class-walker-comment.php | 4 +++- src/wp-includes/class-wp-comment-type.php | 14 ++++++++++++++ src/wp-includes/comment.php | 10 +++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-walker-comment.php b/src/wp-includes/class-walker-comment.php index 1c0c98795500e..1af31a2003316 100644 --- a/src/wp-includes/class-walker-comment.php +++ b/src/wp-includes/class-walker-comment.php @@ -202,7 +202,9 @@ public function start_el( &$output, $data_object, $depth = 0, $args = array(), $ add_filter( 'comment_text', array( $this, 'filter_comment_text' ), 40, 2 ); } - if ( ( 'pingback' === $comment->comment_type || 'trackback' === $comment->comment_type ) && $args['short_ping'] ) { + $is_ping = $comment_type_object && $comment_type_object->is_ping; + + if ( $is_ping && $args['short_ping'] ) { ob_start(); $this->ping( $comment, $depth, $args ); $output .= ob_get_clean(); diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index a8c0463eb16d7..ece67efc0e49e 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -119,6 +119,19 @@ final class WP_Comment_Type { */ public $render_callback = null; + /** + * Whether the comment type represents a ping (a notification from another site) + * rather than a human-authored comment. + * + * Ping types (such as `pingback` and `trackback`) are grouped together by + * {@see separate_comments()} and rendered with the compact ping markup by + * {@see Walker_Comment}. Default false. + * + * @since 7.1.0 + * @var bool + */ + public $is_ping = false; + /** * Whether the comment type is hierarchical. * @@ -208,6 +221,7 @@ public function set_props( $args ) { 'internal' => false, 'show_ui' => null, 'render_callback' => null, + 'is_ping' => false, '_builtin' => false, ); diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 4e3f6f58f0996..3ea28aae46939 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -303,6 +303,7 @@ function create_initial_comment_types() { 'singular_name' => __( 'Pingback' ), ), 'public' => true, + 'is_ping' => true, '_builtin' => true, ) ); @@ -315,6 +316,7 @@ function create_initial_comment_types() { 'singular_name' => __( 'Trackback' ), ), 'public' => true, + 'is_ping' => true, '_builtin' => true, ) ); @@ -367,6 +369,10 @@ function create_initial_comment_types() { * excluded from default public-facing contexts. Default false. * @type bool $show_ui Whether to generate and allow a UI for managing this comment * type in the admin. Default is value of $public. + * @type bool $is_ping Whether the comment type represents a ping (a notification from + * another site) rather than a human-authored comment. Ping types are + * grouped together by separate_comments() and rendered with compact + * ping markup by Walker_Comment. Default false. * @type callable $render_callback Callback used to render a comment of this type in comment * lists. Receives the same arguments as the `callback` argument * of wp_list_comments() (the comment, the arguments, and the @@ -1256,7 +1262,9 @@ function separate_comments( &$comments ) { $comments_by_type[ $type ][] = &$comments[ $i ]; - if ( 'trackback' === $type || 'pingback' === $type ) { + $comment_type_object = get_comment_type_object( $type ); + + if ( $comment_type_object && $comment_type_object->is_ping ) { $comments_by_type['pings'][] = &$comments[ $i ]; } } From 4bbd85ca66acb6dc3c668b92b19908801cd0dc83 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 17:15:59 -0700 Subject: [PATCH 2/4] Comments: Add tests for `is_ping` comment type grouping. Cover the new flag end to end: the `WP_Comment_Type` default and storage, the built-in pingback/trackback types being marked as pings (and comment/note not), `separate_comments()` grouping a registered ping type into the `pings` bucket while leaving non-ping types out, and `Walker_Comment` rendering a registered ping type with the compact ping markup. See #35214. --- .../tests/comment/separateComments.php | 108 ++++++++++++++++++ tests/phpunit/tests/comment/types.php | 16 +++ tests/phpunit/tests/comment/walker.php | 29 +++++ tests/phpunit/tests/comment/wpCommentType.php | 12 ++ 4 files changed, 165 insertions(+) create mode 100644 tests/phpunit/tests/comment/separateComments.php diff --git a/tests/phpunit/tests/comment/separateComments.php b/tests/phpunit/tests/comment/separateComments.php new file mode 100644 index 0000000000000..b9145f88a836d --- /dev/null +++ b/tests/phpunit/tests/comment/separateComments.php @@ -0,0 +1,108 @@ +post->create(); + } + + /** + * Builds a comment of the given type on the shared post. + * + * @param string $comment_type Comment type slug. + * @return WP_Comment The created comment object. + */ + private function make_comment( $comment_type ) { + return get_comment( + self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_type' => $comment_type, + ) + ) + ); + } + + /** + * The standard buckets are always present, even when empty. + * + * @ticket 35214 + */ + public function test_default_buckets_are_always_present() { + $comments = array(); + $separated = separate_comments( $comments ); + + $this->assertArrayHasKey( 'comment', $separated ); + $this->assertArrayHasKey( 'trackback', $separated ); + $this->assertArrayHasKey( 'pingback', $separated ); + $this->assertArrayHasKey( 'pings', $separated ); + } + + /** + * Built-in pingbacks and trackbacks are grouped into the 'pings' bucket. + * + * @ticket 35214 + */ + public function test_built_in_pings_are_grouped_into_pings_bucket() { + $comments = array( + $this->make_comment( 'comment' ), + $this->make_comment( 'pingback' ), + $this->make_comment( 'trackback' ), + ); + + $separated = separate_comments( $comments ); + + $this->assertCount( 1, $separated['comment'] ); + $this->assertCount( 1, $separated['pingback'] ); + $this->assertCount( 1, $separated['trackback'] ); + $this->assertCount( 2, $separated['pings'] ); + } + + /** + * A registered comment type marked as a ping is grouped into 'pings'. + * + * @ticket 35214 + */ + public function test_registered_ping_type_is_grouped_into_pings_bucket() { + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $comments = array( $this->make_comment( 'webmention' ) ); + + $separated = separate_comments( $comments ); + + $this->assertCount( 1, $separated['webmention'] ); + $this->assertCount( 1, $separated['pings'] ); + + unregister_comment_type( 'webmention' ); + } + + /** + * A registered comment type that is not a ping stays out of the 'pings' bucket. + * + * @ticket 35214 + */ + public function test_non_ping_type_is_not_grouped_into_pings_bucket() { + register_comment_type( 'review' ); + + $comments = array( $this->make_comment( 'review' ) ); + + $separated = separate_comments( $comments ); + + $this->assertCount( 1, $separated['review'] ); + $this->assertCount( 0, $separated['pings'] ); + + unregister_comment_type( 'review' ); + } +} diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index 54f8e2aa9ad33..5b83bb57ba63b 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -118,6 +118,22 @@ public function test_built_in_note_type_is_internal_and_non_public() { $this->assertFalse( $note->public ); } + /** + * @ticket 35214 + */ + public function test_built_in_ping_types_are_marked_as_pings() { + $this->assertTrue( get_comment_type_object( 'pingback' )->is_ping ); + $this->assertTrue( get_comment_type_object( 'trackback' )->is_ping ); + } + + /** + * @ticket 35214 + */ + public function test_built_in_non_ping_types_are_not_marked_as_pings() { + $this->assertFalse( get_comment_type_object( 'comment' )->is_ping ); + $this->assertFalse( get_comment_type_object( 'note' )->is_ping ); + } + /** * @ticket 35214 */ diff --git a/tests/phpunit/tests/comment/walker.php b/tests/phpunit/tests/comment/walker.php index 434b2c12b3d38..c012ef4465ec0 100644 --- a/tests/phpunit/tests/comment/walker.php +++ b/tests/phpunit/tests/comment/walker.php @@ -144,6 +144,35 @@ public function test_explicit_callback_takes_precedence_over_render_callback() { unregister_comment_type( 'review' ); } + /** + * A registered ping type renders with the compact ping markup when short_ping is on. + * + * @ticket 35214 + */ + public function test_registered_ping_type_renders_as_ping() { + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => 'webmention', + 'comment_content' => 'A webmention body', + 'comment_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => true ) + ); + + // The compact ping markup prints the "Pingback:" label and omits the comment body. + $this->assertStringContainsString( 'Pingback:', $output ); + $this->assertStringNotContainsString( 'A webmention body', $output ); + + unregister_comment_type( 'webmention' ); + } + /** * Built-in comment types without a render_callback render normally. * diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php index 9dd8c3a37187b..1b8327a9836e8 100644 --- a/tests/phpunit/tests/comment/wpCommentType.php +++ b/tests/phpunit/tests/comment/wpCommentType.php @@ -25,6 +25,18 @@ public function test_instance_defaults() { $this->assertTrue( $comment_type->show_ui ); $this->assertFalse( $comment_type->hierarchical ); $this->assertNull( $comment_type->render_callback ); + $this->assertFalse( $comment_type->is_ping ); + } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_is_ping_is_stored() { + $comment_type = new WP_Comment_Type( 'foo', array( 'is_ping' => true ) ); + + $this->assertTrue( $comment_type->is_ping ); } /** From 3a5a4019ae7bce569d31a1d1e14752d66fe3355b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 20:30:10 -0700 Subject: [PATCH 3/4] Comments: Cover both branches of is_ping rendering in Walker_Comment. Add the complementary cases for the is_ping && short_ping guard: the built-in pingback still renders as a compact ping (regression guard for the move from hard-coded type strings to the is_ping flag), a ping type renders its full markup when short_ping is off, and a non-ping type is never rendered as a ping even with short_ping enabled. See #35214. --- tests/phpunit/tests/comment/walker.php | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/phpunit/tests/comment/walker.php b/tests/phpunit/tests/comment/walker.php index c012ef4465ec0..5259d2495fba9 100644 --- a/tests/phpunit/tests/comment/walker.php +++ b/tests/phpunit/tests/comment/walker.php @@ -173,6 +173,89 @@ public function test_registered_ping_type_renders_as_ping() { unregister_comment_type( 'webmention' ); } + /** + * The built-in pingback type still renders with the compact ping markup. + * + * Guards the refactor from hard-coded `pingback`/`trackback` string checks to + * the `is_ping` flag: built-in ping rendering must remain unchanged. + * + * @ticket 35214 + */ + public function test_built_in_pingback_still_renders_as_ping() { + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => 'pingback', + 'comment_content' => 'A pingback body', + 'comment_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => true ) + ); + + $this->assertStringContainsString( 'Pingback:', $output ); + $this->assertStringNotContainsString( 'A pingback body', $output ); + } + + /** + * A ping type renders its full markup (not the compact ping) when short_ping is off. + * + * @ticket 35214 + */ + public function test_ping_type_renders_full_markup_when_short_ping_disabled() { + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => 'webmention', + 'comment_content' => 'A webmention body', + 'comment_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => false ) + ); + + // With short_ping off the full markup is rendered, including the comment body. + $this->assertStringContainsString( 'A webmention body', $output ); + + unregister_comment_type( 'webmention' ); + } + + /** + * A non-ping type is never rendered as a ping, even with short_ping enabled. + * + * @ticket 35214 + */ + public function test_non_ping_type_is_not_rendered_as_ping_with_short_ping() { + register_comment_type( 'review' ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => 'review', + 'comment_content' => 'A review body', + 'comment_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => true ) + ); + + $this->assertStringContainsString( 'A review body', $output ); + $this->assertStringNotContainsString( 'Pingback:', $output ); + + unregister_comment_type( 'review' ); + } + /** * Built-in comment types without a render_callback render normally. * From c803649d4dad097bb7c3ddda2e11ffe222c2ccce Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 11 Jul 2026 10:27:49 -0700 Subject: [PATCH 4/4] Comments: Document is_ping interactions and pin grouping edge cases. - The is_ping docs now state the 'short_ping' condition, that a registered 'render_callback' takes precedence over the compact ping markup, and the classic-theme-only scope. - Add the missing @since 7.1.0 changelog entries to separate_comments() and extend Walker_Comment::start_el()'s. - Update the wp_list_comments() 'type' argument doc: 'pings' now means every registered comment type with 'is_ping'. - Tests: pin the render_callback-beats-short_ping precedence, the unregistered-type bucket (no 'pings' membership, matching the old hard-coded behavior), and the legacy empty-string 'comment' bucket. Drop cleanup-only unregister calls now handled by the test framework. --- src/wp-includes/class-walker-comment.php | 3 +- src/wp-includes/class-wp-comment-type.php | 7 +++- src/wp-includes/comment-template.php | 6 ++- src/wp-includes/comment.php | 9 +++- .../tests/comment/separateComments.php | 31 ++++++++++++-- tests/phpunit/tests/comment/walker.php | 41 ++++++++++++++++--- 6 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/class-walker-comment.php b/src/wp-includes/class-walker-comment.php index 1f3c6ddfa7ce0..4487339f0c010 100644 --- a/src/wp-includes/class-walker-comment.php +++ b/src/wp-includes/class-walker-comment.php @@ -159,7 +159,8 @@ public function display_element( $element, &$children_elements, $max_depth, $dep * @since 5.9.0 Renamed `$comment` to `$data_object` and `$id` to `$current_object_id` * to match parent class for PHP 8 named parameter support. * @since 7.1.0 Comments of a registered comment type with a `render_callback` - * are rendered via that callback. + * are rendered via that callback, and short-ping rendering is + * driven by the comment type's `is_ping` property. * * @see Walker::start_el() * @see wp_list_comments() diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 4f5eee3e935aa..9ae1e56222899 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -133,8 +133,11 @@ final class WP_Comment_Type { * rather than a human-authored comment. * * Ping types (such as `pingback` and `trackback`) are grouped together by - * {@see separate_comments()} and rendered with the compact ping markup by - * {@see Walker_Comment}. Default false. + * {@see separate_comments()} and, when the `short_ping` argument of + * wp_list_comments() is true, rendered with the compact ping markup by + * {@see Walker_Comment}. A registered `render_callback` takes precedence over + * the ping markup. Like `render_callback`, the rendering effects apply only to + * classic themes; block themes do not use Walker_Comment. Default false. * * @since 7.1.0 * @var bool diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index ce40ee014ac34..5c91c32042c75 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -2230,8 +2230,10 @@ function _get_comment_reply_id( $post = null ) { * 'div' will result in no additional list markup. Default 'ul'. * @type callable $callback Callback function to use. Default null. * @type callable $end-callback Callback function to use at the end. Default null. - * @type string $type Type of comments to list. Accepts 'all', 'comment', - * 'pingback', 'trackback', 'pings'. Default 'all'. + * @type string $type Type of comments to list. Accepts 'all', any comment type + * slug, or 'pings' (the comments of every registered comment + * type with the 'is_ping' property, which includes pingbacks + * and trackbacks). Default 'all'. * @type int $page Page ID to list comments for. Default empty. * @type int $per_page Number of comments to list per page. Default empty. * @type int $avatar_size Height and width dimensions of the avatar size. Default 32. diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index e7fd684869c2e..bf441a27e8a92 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -378,8 +378,10 @@ function create_initial_comment_types() { * default query exclusions in the future. Default false. * @type bool $is_ping Whether the comment type represents a ping (a notification * from another site) rather than a human-authored comment. - * Ping types are grouped together by separate_comments() and - * rendered with compact ping markup by Walker_Comment. + * Ping types are grouped together by separate_comments() and, + * when wp_list_comments() is called with 'short_ping', rendered + * with compact ping markup by Walker_Comment. A registered + * 'render_callback' takes precedence over the ping markup. * Default false. * @type callable $render_callback Callback used to render a comment of this type in comment * lists. Receives the same arguments as the `callback` argument @@ -1288,6 +1290,9 @@ function wp_check_comment_flood( $is_flood, $ip, $email, $date, $avoid_die = fal * Separates an array of comments into an array keyed by comment_type. * * @since 2.7.0 + * @since 7.1.0 The 'pings' group contains the comments of every registered + * comment type with the `is_ping` property, rather than only + * pingbacks and trackbacks. * * @param WP_Comment[] $comments Array of comments. * @return array Array of comments keyed by comment type. diff --git a/tests/phpunit/tests/comment/separateComments.php b/tests/phpunit/tests/comment/separateComments.php index b9145f88a836d..4bd1cce490673 100644 --- a/tests/phpunit/tests/comment/separateComments.php +++ b/tests/phpunit/tests/comment/separateComments.php @@ -84,8 +84,6 @@ public function test_registered_ping_type_is_grouped_into_pings_bucket() { $this->assertCount( 1, $separated['webmention'] ); $this->assertCount( 1, $separated['pings'] ); - - unregister_comment_type( 'webmention' ); } /** @@ -102,7 +100,34 @@ public function test_non_ping_type_is_not_grouped_into_pings_bucket() { $this->assertCount( 1, $separated['review'] ); $this->assertCount( 0, $separated['pings'] ); + } + + /** + * An unregistered comment type gets its own bucket and stays out of 'pings', + * matching the previous hard-coded behavior. + * + * @ticket 35214 + */ + public function test_unregistered_type_gets_own_bucket_and_is_not_a_ping() { + $comments = array( $this->make_comment( 'webmention' ) ); + + $separated = separate_comments( $comments ); - unregister_comment_type( 'review' ); + $this->assertCount( 1, $separated['webmention'] ); + $this->assertCount( 0, $separated['pings'] ); + } + + /** + * A comment stored with the legacy empty string type lands in the 'comment' bucket. + * + * @ticket 35214 + */ + public function test_legacy_empty_type_lands_in_comment_bucket() { + $comments = array( $this->make_comment( '' ) ); + + $separated = separate_comments( $comments ); + + $this->assertCount( 1, $separated['comment'] ); + $this->assertCount( 0, $separated['pings'] ); } } diff --git a/tests/phpunit/tests/comment/walker.php b/tests/phpunit/tests/comment/walker.php index 05ff57a607ae0..39a4f9e7968fc 100644 --- a/tests/phpunit/tests/comment/walker.php +++ b/tests/phpunit/tests/comment/walker.php @@ -168,8 +168,6 @@ public function test_registered_ping_type_renders_as_ping() { // The compact ping markup prints the "Pingback:" label and omits the comment body. $this->assertStringContainsString( 'Pingback:', $output ); $this->assertStringNotContainsString( 'A webmention body', $output ); - - unregister_comment_type( 'webmention' ); } /** @@ -223,8 +221,6 @@ public function test_ping_type_renders_full_markup_when_short_ping_disabled() { // With short_ping off the full markup is rendered, including the comment body. $this->assertStringContainsString( 'A webmention body', $output ); - - unregister_comment_type( 'webmention' ); } /** @@ -251,8 +247,43 @@ public function test_non_ping_type_is_not_rendered_as_ping_with_short_ping() { $this->assertStringContainsString( 'A review body', $output ); $this->assertStringNotContainsString( 'Pingback:', $output ); + } + + /** + * A render_callback wins over the compact ping markup for ping types. + * + * This pins the precedence chain: explicit wp_list_comments() 'callback', + * then 'render_callback', then is_ping short-ping markup, then default markup. + * + * @ticket 35214 + */ + public function test_render_callback_takes_precedence_over_short_ping_markup() { + register_comment_type( + 'webmention', + array( + 'is_ping' => true, + 'render_callback' => static function ( $comment ) { + echo '
  • ' . esc_html( $comment->comment_content ); + }, + ) + ); - unregister_comment_type( 'review' ); + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => 'webmention', + 'comment_content' => 'A webmention body', + 'comment_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => true ) + ); + + $this->assertStringContainsString( '
  • A webmention body', $output ); + $this->assertStringNotContainsString( 'Pingback:', $output ); } /**