From 6a0dfab714dc66bf42027c0e63915aa53824c37d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 17:15:54 -0700 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 From d72ec04c8990f4b042dae0edf8da95793eff7781 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sun, 23 Aug 2026 08:49:22 -0700 Subject: [PATCH 08/10] Comments: Fold the resolved ping types into the descendant cache keys. get_comments() folds the registered ping types into its cache key so a plugin registering a ping type invalidates 'pings' results, but fill_descendants() built its per-parent child ID keys from the raw query vars alone. The cached child lists are filtered by the resolved ping set, so after registering a new ping type a threaded 'pings' query returned the updated top-level comments while still hiding the new type's children until something bumped the comment last_changed salt - indefinitely, under a persistent object cache. Extract the key construction into a shared get_cache_key_args() helper used by both paths so they cannot drift apart again. As a side effect the descendant keys now also drop 'fields' and the two update_*_cache flags, which never affected which children exist. See #35214. --- src/wp-includes/class-wp-comment-query.php | 62 +++++++++++++--------- tests/phpunit/tests/comment/query.php | 51 ++++++++++++++++++ 2 files changed, 88 insertions(+), 25 deletions(-) diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index f19d7fc8ab43a..1496f6a0955aa 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -461,29 +461,7 @@ public function get_comments() { return $comment_data; } - /* - * Only use the args defined in the query_var_defaults to compute the key, - * but ignore 'fields', 'update_comment_meta_cache', 'update_comment_post_cache' which does not affect query results. - */ - $_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 ) ); + $key = md5( serialize( $this->get_cache_key_args() ) ); $last_changed = wp_cache_get_last_changed( 'comment' ); $cache_key = "get_comments:$key"; @@ -1035,13 +1013,47 @@ protected function get_comment_ids() { } } + /** + * Builds the normalized set of query vars that comment query cache keys hash. + * + * Only uses the args defined in the query_var_defaults, ignoring 'fields', + * 'update_comment_meta_cache', and 'update_comment_post_cache', which do not + * affect query results. + * + * 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. Both the main query cache in get_comments() and the per-parent descendant + * caches in fill_descendants() hash these args, so the two cannot disagree. + * + * @since 7.2.0 + * + * @return array Query vars to hash into a cache key. + */ + protected function get_cache_key_args() { + $_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'] ); + + $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(); + } + + return $_args; + } + /** * 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 + * @since 7.2.0 * * @return string[] Comment type names. */ @@ -1105,7 +1117,7 @@ protected function fill_descendants( $comments ) { 0 => wp_list_pluck( $comments, 'comment_ID' ), ); - $key = md5( serialize( wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ) ) ); + $key = md5( serialize( $this->get_cache_key_args() ) ); $last_changed = wp_cache_get_last_changed( 'comment' ); // Fetch an entire level of the descendant tree at a time. diff --git a/tests/phpunit/tests/comment/query.php b/tests/phpunit/tests/comment/query.php index c54d1d055d8e7..6cc7ba5d384c1 100644 --- a/tests/phpunit/tests/comment/query.php +++ b/tests/phpunit/tests/comment/query.php @@ -426,6 +426,57 @@ public function test_query_type_pings_is_not_served_from_a_stale_cache() { $this->assertSameSets( array( $pingback, $mention ), $after, 'After registration it is.' ); } + /** + * The resolved ping set is part of the per-parent descendant cache keys too. + * Otherwise a threaded 'pings' query run before a ping type existed would keep + * serving that parent's children from the stale child ID cache even though the + * top-level results update. + * + * @ticket 35214 + * + * @covers WP_Comment_Query::fill_descendants + */ + public function test_threaded_pings_descendants_are_not_served_from_a_stale_cache() { + $pingback = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'pingback', + ) + ); + $child = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => '1', + 'comment_type' => 'webmention', + 'comment_parent' => $pingback, + ) + ); + + $args = array( + 'type' => 'pings', + 'hierarchical' => 'threaded', + 'post_id' => self::$post_id, + ); + + $before = ( new WP_Comment_Query() )->query( $args ); + + register_comment_type( 'webmention', array( 'is_ping' => true ) ); + + $after = ( new WP_Comment_Query() )->query( $args ); + + $this->assertSame( + array(), + $before[ $pingback ]->get_children(), + 'Before registration the child type is not a ping, so the parent has no children.' + ); + $this->assertSame( + array( $child ), + array_values( array_map( 'intval', wp_list_pluck( $after[ $pingback ]->get_children(), 'comment_ID' ) ) ), + 'After registration the child should not be hidden by a stale descendant cache.' + ); + } + /** * Comments and custom * From 32661c63f3f473f44869decb8460d85dfda83649 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sun, 23 Aug 2026 08:50:23 -0700 Subject: [PATCH 09/10] Comments: Keep 'Pingback:' for ping types registered without labels. Every registered comment type gets the default 'Comment' singular name from set_props(), so the singular-name guard in Walker_Comment::ping() could never fail and a ping type registered without labels rendered as 'Comment:' - a worse mislabel than the 'Pingback:' it replaced. Fall back to 'Pingback:' whenever the type inherited the default singular name, and pin it with a test (the existing suite only registered ping types that supplied a label). Also give the '%s:' string a translator context, matching the existing '%s:' prefixes in post-template.php and general-template.php so it does not merge with unrelated uses in the POT. See #35214. --- src/wp-includes/class-walker-comment.php | 14 +++++++++--- tests/phpunit/tests/comment/walker.php | 27 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-walker-comment.php b/src/wp-includes/class-walker-comment.php index 3c40f6afe285f..bd49ec3872c66 100644 --- a/src/wp-includes/class-walker-comment.php +++ b/src/wp-includes/class-walker-comment.php @@ -278,12 +278,20 @@ protected function ping( $comment, $depth, $args ) { * 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(). + * A ping type registered without labels inherits the default 'Comment' singular + * name, which is even more wrong for a ping, so such types keep 'Pingback:' too. */ $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 ) ) { + $default_labels = WP_Comment_Type::get_default_labels(); + + if ( + $comment_type_object + && ! $comment_type_object->_builtin + && isset( $comment_type_object->labels->singular_name ) + && $default_labels['singular_name'][0] !== $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 ) ); + $label = sprintf( _x( '%s:', 'comment type label' ), esc_html( $comment_type_object->labels->singular_name ) ); } else { $label = __( 'Pingback:' ); } diff --git a/tests/phpunit/tests/comment/walker.php b/tests/phpunit/tests/comment/walker.php index 2f18dabff5658..5cb68b774f940 100644 --- a/tests/phpunit/tests/comment/walker.php +++ b/tests/phpunit/tests/comment/walker.php @@ -314,6 +314,33 @@ public function test_registered_ping_type_renders_with_its_own_label() { $this->assertStringNotContainsString( 'Pingback:', $output ); } + /** + * A ping type registered without labels inherits the default 'Comment' singular name, + * which is even more wrong for a ping than 'Pingback:'. The compact markup falls back + * to 'Pingback:' rather than labeling a ping 'Comment:'. + * + * @ticket 35214 + */ + public function test_ping_type_without_labels_falls_back_to_pingback_label() { + 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_approved' => '1', + ) + ); + + $output = $this->render_comments( + array( get_comment( $comment_id ) ), + array( 'short_ping' => true ) + ); + + $this->assertStringContainsString( 'Pingback:', $output ); + $this->assertStringNotContainsString( 'Comment:', $output ); + } + /** * The built-in pingback type still renders with the compact ping markup. * From c52070c03dc993a27a2080b442194d6190e5cd0f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sun, 23 Aug 2026 08:52:25 -0700 Subject: [PATCH 10/10] Comments: Stamp the is_ping work for 7.2.0 and settle its edges. The branch was written while trunk was 7.1-alpha; trunk is now 7.2-alpha, so the six @since tags this branch adds pointed at a release that will never contain them. Beyond the stamps, resolve the smaller review findings around the 'pings' expansion: reset the memoized ping type set in parse_query() so reusing one WP_Comment_Query instance cannot build SQL and cache keys from a set resolved before a registration change; correct the empty-registry fallback rationale (comment types are registered directly in wp-settings.php, not on 'init', so only a partial bootstrap hits it); update the two get_page_of_comment() docs that still described 'pings' as trackbacks and pingbacks; and repair the test docblocks displaced when the ping tests were inserted between test_comment_type_exists() and its annotations. See #35214. --- src/wp-includes/class-walker-comment.php | 2 +- src/wp-includes/class-wp-comment-query.php | 16 ++++++++++------ src/wp-includes/class-wp-comment-type.php | 2 +- src/wp-includes/comment.php | 8 +++++--- tests/phpunit/tests/comment/types.php | 6 +++++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/class-walker-comment.php b/src/wp-includes/class-walker-comment.php index bd49ec3872c66..fb8e0a9eb5dce 100644 --- a/src/wp-includes/class-walker-comment.php +++ b/src/wp-includes/class-walker-comment.php @@ -263,7 +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. + * @since 7.2.0 A registered, non-built-in ping type is labeled with its singular name. * * @see wp_list_comments() * diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index 1496f6a0955aa..32f0a2e5a1b15 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -122,10 +122,11 @@ class WP_Comment_Query { /** * 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. + * Resolved lazily, once per parsed query, so that the set folded into the cache + * keys is the same one get_comment_ids() builds the SQL from. Reset by + * parse_query(); null until resolved. * - * @since 7.1.0 + * @since 7.2.0 * @var string[]|null */ protected $ping_comment_types = null; @@ -160,7 +161,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`. + * @since 7.2.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. @@ -356,6 +357,9 @@ public function parse_query( $query = '' ) { $this->query_vars = wp_parse_args( $query, $this->query_var_defaults ); + // Re-resolve the ping types per query, in case the registry changed in between. + $this->ping_comment_types = null; + /** * Fires after the comment query vars have been parsed. * @@ -1062,8 +1066,8 @@ protected function get_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. + * The built-in ping types, for queries that run before create_initial_comment_types() + * in a partial bootstrap and for any install running without the registry. */ if ( ! $ping_types ) { $ping_types = array( 'pingback', 'trackback' ); diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 7394aee541ed5..a0d2a50baaaa8 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -160,7 +160,7 @@ final class WP_Comment_Type { * 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 + * @since 7.2.0 * @var bool */ public $is_ping = false; diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index f10804c818a5c..125bfda12295a 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -1400,7 +1400,7 @@ 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 + * @since 7.2.0 The 'pings' group contains the comments of every registered * comment type with the `is_ping` property, rather than only * pingbacks and trackbacks. * @@ -1508,7 +1508,8 @@ function get_comment_pages_count( $comments = null, $per_page = null, $threaded * * @type string $type Limit paginated comments to those matching a given type. * Accepts 'comment', 'trackback', 'pingback', 'pings' - * (trackbacks and pingbacks), or 'all'. Default 'all'. + * (the comments of every registered comment type with + * `is_ping`), or 'all'. Default 'all'. * @type int $per_page Per-page count to use when calculating pagination. * Defaults to the value of the 'comments_per_page' option. * @type int|string $max_depth If greater than 1, comment page will be determined @@ -1608,7 +1609,8 @@ function get_page_of_comment( $comment_id, $args = array() ) { * * @type string $type Limit paginated comments to those matching a given type. * Accepts 'comment', 'trackback', 'pingback', 'pings' - * (trackbacks and pingbacks), or 'all'. Default 'all'. + * (the comments of every registered comment type with + * `is_ping`), or 'all'. Default 'all'. * @type int $post_id ID of the post. * @type string $fields Comment fields to return. * @type bool $count Whether to return a comment count (true) or array diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index d104f5a90b56d..a088710fa51ad 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -102,7 +102,7 @@ public function test_built_in_note_type_is_internal_and_non_public() { /** * @ticket 35214 * - * @covers ::comment_type_exists + * @covers ::create_initial_comment_types */ public function test_built_in_ping_types_are_marked_as_pings() { $this->assertTrue( get_comment_type_object( 'pingback' )->is_ping ); @@ -111,6 +111,8 @@ public function test_built_in_ping_types_are_marked_as_pings() { /** * @ticket 35214 + * + * @covers ::create_initial_comment_types */ public function test_built_in_non_ping_types_are_not_marked_as_pings() { $this->assertFalse( get_comment_type_object( 'comment' )->is_ping ); @@ -119,6 +121,8 @@ public function test_built_in_non_ping_types_are_not_marked_as_pings() { /** * @ticket 35214 + * + * @covers ::comment_type_exists */ public function test_comment_type_exists() { $this->assertFalse( comment_type_exists( 'foo' ) );