From 6a0dfab714dc66bf42027c0e63915aa53824c37d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 17:15:54 -0700 Subject: [PATCH 1/7] 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/7] 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/7] 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/7] 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 ); } /** From 306bb484140c428707da8ea17032d2fd59fcd928 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 17:24:32 -0700 Subject: [PATCH 5/7] Comments: Label a registered ping type with its own name in the compact markup. `Walker_Comment::ping()` prints a literal "Pingback:". That has been imprecise since 3.6, when trackbacks started using the same markup, but it was at least never wrong by more than one word. Now that any type can opt into ping grouping, a webmention rendering as "Pingback: example.com" is plainly wrong output for exactly the audience the flag is for. Use the type's `singular_name` for registered, non-built-in types, matching what `comment_type()` already does on this branch, and leave the string alone for the built-ins so their markup is byte-identical. The test that asserted the literal "Pingback:" for a webmention was pinning the bug, so replace it: one test for the compact shape (label, author link, no comment body), one for the label itself. Also add the end-to-end case the suite was missing - `wp_list_comments()` with `type => 'pings'` listing a registered ping type - which is where a theme actually meets the grouping change. --- src/wp-includes/class-walker-comment.php | 17 ++++- tests/phpunit/tests/comment/walker.php | 80 ++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-walker-comment.php b/src/wp-includes/class-walker-comment.php index 4487339f0c010..23a921474a183 100644 --- a/src/wp-includes/class-walker-comment.php +++ b/src/wp-includes/class-walker-comment.php @@ -263,6 +263,7 @@ public function end_el( &$output, $data_object, $depth = 0, $args = array() ) { * Outputs a pingback comment. * * @since 3.6.0 + * @since 7.1.0 A registered, non-built-in ping type is labeled with its singular name. * * @see wp_list_comments() * @@ -272,10 +273,24 @@ public function end_el( &$output, $data_object, $depth = 0, $args = array() ) { */ protected function ping( $comment, $depth, $args ) { $tag = ( 'div' === $args['style'] ) ? 'div' : 'li'; + + /* + * The built-in ping types share the 'Pingback:' label - trackbacks have carried it + * since 3.6 - so their markup is unchanged. A registered ping type would be + * mislabeled by it, so use its own singular name instead, matching comment_type(). + */ + $comment_type_object = get_comment_type_object( $comment->comment_type ); + + if ( $comment_type_object && ! $comment_type_object->_builtin && isset( $comment_type_object->labels->singular_name ) ) { + /* translators: %s: Singular name of a registered comment type, e.g. "Webmention". */ + $label = sprintf( __( '%s:' ), esc_html( $comment_type_object->labels->singular_name ) ); + } else { + $label = __( 'Pingback:' ); + } ?> < id="comment-" >
    - ', '' ); ?> + ', '' ); ?>
    true ) ); + register_comment_type( + 'webmention', + array( + 'is_ping' => true, + 'labels' => array( 'singular_name' => 'Webmention' ), + ) + ); $comment_id = self::factory()->comment->create( array( @@ -239,9 +246,72 @@ public function test_registered_ping_type_renders_as_ping() { 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 ); + $this->assertStringContainsString( '
    ', $output, 'The compact ping markup should be used.' ); + $this->assertStringContainsString( 'class="url"', $output, 'The author link should be rendered.' ); + $this->assertStringNotContainsString( 'A webmention body', $output, 'The comment body should be omitted.' ); + } + + /** + * The user-visible end of the grouping change: a theme listing 'pings' gets the + * registered ping type alongside the built-ins, and nothing else. + * + * @ticket 35214 + */ + public function test_wp_list_comments_type_pings_lists_registered_ping_types() { + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $comments = array(); + + foreach ( array( 'comment', 'pingback', 'webmention' ) as $comment_type ) { + $comments[] = get_comment( + self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => $comment_type, + 'comment_author' => "Author of a $comment_type", + 'comment_approved' => '1', + ) + ) + ); + } + + $output = $this->render_comments( $comments, array( 'type' => 'pings' ) ); + + $this->assertStringContainsString( 'Author of a webmention', $output ); + $this->assertStringContainsString( 'Author of a pingback', $output ); + $this->assertStringNotContainsString( 'Author of a comment', $output ); + } + + /** + * The compact ping markup labels a registered type with its own singular name. The + * built-in "Pingback:" would be plainly wrong for anything else. + * + * @ticket 35214 + */ + public function test_registered_ping_type_renders_with_its_own_label() { + register_comment_type( + 'webmention', + array( + 'is_ping' => true, + 'labels' => array( 'singular_name' => 'Webmention' ), + ) + ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $this->post_id, + 'comment_type' => 'webmention', + 'comment_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => true ) + ); + + $this->assertStringContainsString( 'Webmention:', $output ); + $this->assertStringNotContainsString( 'Pingback:', $output ); } /** From 36dccee116a61fb5d527dfac8dc474a57b26bac9 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 17:24:54 -0700 Subject: [PATCH 6/7] Comments: Expand a 'pings' comment query to every registered ping type. `separate_comments()` and `wp_list_comments()` group pings by the `is_ping` flag, while `WP_Comment_Query` kept expanding the 'pings' token to 'pingback' and 'trackback'. A theme reading `$wp_query->comments_by_type['pings']` and a plugin calling `get_comments( array( 'type' => 'pings' ) )` would disagree about which comments are pings, so land the query side in the same release rather than ship that window. The resolved set goes into the comment query cache key. Which types are pings depends on what is registered, which changes when a plugin is activated, and the comment `last_changed` salt only moves when a comment does - so without this a 'pings' query would keep serving results cached before the type existed. Registration happens on 'init', so the built-in pair remains the fallback for callers that query before then. --- src/wp-includes/class-wp-comment-query.php | 63 +++++++++++++++- tests/phpunit/tests/comment/query.php | 84 ++++++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index f80864d31c8bc..f19d7fc8ab43a 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -119,6 +119,17 @@ class WP_Comment_Query { */ public $max_num_pages = 0; + /** + * Comment types a 'pings' type token expands to. + * + * Resolved once per query in get_comments(), so that the set folded into the cache + * key is the same one get_comment_ids() builds the SQL from. Null until resolved. + * + * @since 7.1.0 + * @var string[]|null + */ + protected $ping_comment_types = null; + /** * Make private/protected methods readable for backward compatibility. * @@ -149,6 +160,7 @@ public function __call( $name, $arguments ) { * @since 4.9.0 Introduced the `$paged` argument. * @since 5.1.0 Introduced the `$meta_compare_key` argument. * @since 5.3.0 Introduced the `$meta_type_key` argument. + * @since 7.1.0 A `$type` of 'pings' expands to every comment type registered with `is_ping`. * * @param string|array $query { * Optional. Array or query string of comment query parameters. Default empty. @@ -251,7 +263,8 @@ public function __call( $name, $arguments ) { * 'approve' (`comment_status=1`), 'all', or a custom * comment status. Default 'all'. * @type string|string[] $type Include comments of a given type, or array of types. - * Accepts 'comment', 'pings' (includes 'pingback' and + * Accepts 'comment', 'pings' (every comment type registered + * with `is_ping`, which includes 'pingback' and * 'trackback'), or any custom type string. Default empty. * @type string[] $type__in Include comments from a given array of comment types. * Default empty. @@ -455,6 +468,21 @@ public function get_comments() { $_args = wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ); unset( $_args['fields'], $_args['update_comment_meta_cache'], $_args['update_comment_post_cache'] ); + /* + * A 'pings' token expands to the registered ping types, which a plugin can change + * from one request to the next, so the resolved set belongs in the cache key. The + * comment last_changed salt only moves when a comment does, and would not catch it. + */ + $type_query_vars = array_merge( + (array) $this->query_vars['type'], + (array) $this->query_vars['type__in'], + (array) $this->query_vars['type__not_in'] + ); + + if ( in_array( 'pings', $type_query_vars, true ) ) { + $_args['ping_comment_types'] = $this->get_ping_comment_types(); + } + $key = md5( serialize( $_args ) ); $last_changed = wp_cache_get_last_changed( 'comment' ); @@ -806,8 +834,9 @@ protected function get_comment_ids() { break; case 'pings': - $comment_types[ $operator ][] = "'pingback'"; - $comment_types[ $operator ][] = "'trackback'"; + foreach ( $this->get_ping_comment_types() as $ping_type ) { + $comment_types[ $operator ][] = $wpdb->prepare( '%s', $ping_type ); + } break; default: @@ -1006,6 +1035,34 @@ protected function get_comment_ids() { } } + /** + * Resolves the comment types a 'pings' type token expands to. + * + * Matches how separate_comments() and wp_list_comments() group pings, so that a + * query for 'pings' returns the comments a theme would list under that heading. + * + * @since 7.1.0 + * + * @return string[] Comment type names. + */ + protected function get_ping_comment_types() { + if ( null === $this->ping_comment_types ) { + $ping_types = get_comment_types( array( 'is_ping' => true ), 'names' ); + + /* + * The built-in ping types, for the window before comment types are registered + * on 'init' and for any install running without the registry. + */ + if ( ! $ping_types ) { + $ping_types = array( 'pingback', 'trackback' ); + } + + $this->ping_comment_types = array_values( $ping_types ); + } + + return $this->ping_comment_types; + } + /** * Populates found_comments and max_num_pages properties for the current * query if the limit clause was used. diff --git a/tests/phpunit/tests/comment/query.php b/tests/phpunit/tests/comment/query.php index dc870a78ae494..c54d1d055d8e7 100644 --- a/tests/phpunit/tests/comment/query.php +++ b/tests/phpunit/tests/comment/query.php @@ -342,6 +342,90 @@ public function test_query_type_pings() { $this->assertSameSets( array( $c2, $c3 ), $found ); } + /** + * A 'pings' query returns every registered ping type, matching what + * separate_comments() groups under the same name. + * + * @ticket 35214 + * + * @covers WP_Comment_Query::query + */ + public function test_query_type_pings_includes_registered_ping_types() { + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $pingback = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'pingback', + ) + ); + $mention = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'webmention', + ) + ); + self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'review', + ) + ); + + $q = new WP_Comment_Query(); + $found = $q->query( + array( + 'type' => 'pings', + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( $pingback, $mention ), $found ); + } + + /** + * The registered ping types are part of the 'pings' cache key. Otherwise a plugin + * registering a ping type would keep serving results cached before it existed, since + * the comment last_changed salt only moves when a comment does. + * + * @ticket 35214 + * + * @covers WP_Comment_Query::get_comments + */ + public function test_query_type_pings_is_not_served_from_a_stale_cache() { + $pingback = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'pingback', + ) + ); + $mention = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'webmention', + ) + ); + + $args = array( + 'type' => 'pings', + 'fields' => 'ids', + ); + + $before = ( new WP_Comment_Query() )->query( $args ); + + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $after = ( new WP_Comment_Query() )->query( $args ); + + $this->assertSameSets( array( $pingback ), $before, 'Before registration the type is not a ping.' ); + $this->assertSameSets( array( $pingback, $mention ), $after, 'After registration it is.' ); + } + /** * Comments and custom * From 9aea3a5e5aeecf7b670f21e14208c0d3fd47920a Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 17:24:54 -0700 Subject: [PATCH 7/7] Docs: Describe what the `is_ping` flag does and does not reach. The flag's docs covered grouping and rendering but not the two things a plugin author most needs to know before using it: that the compact ping markup now labels the comment with the type's `singular_name`, so a ping type ought to register one, and that the flag stops at display and grouping. Validation, default comment status, and notification emails still key on the 'pingback' and 'trackback' type names, so a custom ping type gains none of their handling - and, just as importantly, none of their bypasses. --- src/wp-includes/class-wp-comment-type.php | 18 +++++++++++++----- src/wp-includes/comment.php | 12 ++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 79ab410b81589..4f8b6095e796d 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -146,11 +146,19 @@ 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, 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. + * {@see separate_comments()} and returned by a `'pings'` type query in + * {@see WP_Comment_Query}. When the `short_ping` argument of wp_list_comments() + * is true, they are rendered with the compact ping markup by + * {@see Walker_Comment}, which labels the comment with the type's + * `singular_name`, so a ping type should register one. 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. + * + * The flag drives grouping and display only. It does not change how a comment of + * this type is validated, moderated, or notified about: the pingback and trackback + * paths in {@see check_comment()}, {@see get_default_comment_status()}, and the + * notification emails are still keyed to those two type names. Default false. * * @since 7.1.0 * @var bool diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 9044c3e0e2cd5..2a1b8e09890d3 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -453,10 +453,14 @@ function create_initial_comment_types() { * 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, - * 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. + * Ping types are grouped together by separate_comments(), + * returned by a 'pings' type query, and, when + * wp_list_comments() is called with 'short_ping', rendered + * with compact ping markup by Walker_Comment, labeled with the + * type's singular name. A registered 'render_callback' takes + * precedence over the ping markup. The flag drives grouping + * and display only; validation, moderation, and notification + * still key on the 'pingback' and 'trackback' type names. * 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