diff --git a/src/wp-includes/class-walker-comment.php b/src/wp-includes/class-walker-comment.php
index 23c5af1bc69c0..fb8e0a9eb5dce 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.2.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()
@@ -204,7 +205,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();
@@ -260,6 +263,7 @@ public function end_el( &$output, $data_object, $depth = 0, $args = array() ) {
* Outputs a pingback comment.
*
* @since 3.6.0
+ * @since 7.2.0 A registered, non-built-in ping type is labeled with its singular name.
*
* @see wp_list_comments()
*
@@ -269,10 +273,32 @@ 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().
+ * 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 );
+ $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( _x( '%s:', 'comment type label' ), esc_html( $comment_type_object->labels->singular_name ) );
+ } else {
+ $label = __( 'Pingback:' );
+ }
?>
< id="comment-" >
- ', '' ); ?>
+ ', '' ); ?>
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.
*
@@ -448,14 +465,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'] );
-
- $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";
@@ -806,8 +816,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 +1017,68 @@ 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.2.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 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' );
+ }
+
+ $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.
@@ -1048,7 +1121,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/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php
index adf691bd6b475..a0d2a50baaaa8 100644
--- a/src/wp-includes/class-wp-comment-type.php
+++ b/src/wp-includes/class-wp-comment-type.php
@@ -141,6 +141,30 @@ 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 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.2.0
+ * @var bool
+ */
+ public $is_ping = false;
+
/**
* Whether the comment type is hierarchical.
*
@@ -230,6 +254,7 @@ public function set_props( $args ) {
'public' => true,
'internal' => false,
'render_callback' => null,
+ 'is_ping' => false,
'_builtin' => false,
);
diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php
index 77702035862dc..f830489af015d 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 7bf48e4913532..125bfda12295a 100644
--- a/src/wp-includes/comment.php
+++ b/src/wp-includes/comment.php
@@ -365,6 +365,7 @@ function create_initial_comment_types() {
'singular_name' => __( 'Pingback' ),
),
'public' => true,
+ 'is_ping' => true,
'_builtin' => true,
)
);
@@ -377,6 +378,7 @@ function create_initial_comment_types() {
'singular_name' => __( 'Trackback' ),
),
'public' => true,
+ 'is_ping' => true,
'_builtin' => true,
)
);
@@ -450,6 +452,17 @@ function create_initial_comment_types() {
* counts by default. Core does not currently act on this
* argument.
* 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(),
+ * 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
* of wp_list_comments() (the comment, the arguments, and the
@@ -1387,6 +1400,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.2.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.
@@ -1410,7 +1426,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 ];
}
}
@@ -1490,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
@@ -1590,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/query.php b/tests/phpunit/tests/comment/query.php
index dc870a78ae494..6cc7ba5d384c1 100644
--- a/tests/phpunit/tests/comment/query.php
+++ b/tests/phpunit/tests/comment/query.php
@@ -342,6 +342,141 @@ 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.' );
+ }
+
+ /**
+ * 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
*
diff --git a/tests/phpunit/tests/comment/separateComments.php b/tests/phpunit/tests/comment/separateComments.php
new file mode 100644
index 0000000000000..4bd1cce490673
--- /dev/null
+++ b/tests/phpunit/tests/comment/separateComments.php
@@ -0,0 +1,133 @@
+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'] );
+ }
+
+ /**
+ * 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'] );
+ }
+
+ /**
+ * 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 );
+
+ $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/types.php b/tests/phpunit/tests/comment/types.php
index 58fdb791127b0..a088710fa51ad 100644
--- a/tests/phpunit/tests/comment/types.php
+++ b/tests/phpunit/tests/comment/types.php
@@ -99,6 +99,26 @@ public function test_built_in_note_type_is_internal_and_non_public() {
$this->assertFalse( $note->public );
}
+ /**
+ * @ticket 35214
+ *
+ * @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 );
+ $this->assertTrue( get_comment_type_object( 'trackback' )->is_ping );
+ }
+
+ /**
+ * @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 );
+ $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 dfe915a0879f9..5cb68b774f940 100644
--- a/tests/phpunit/tests/comment/walker.php
+++ b/tests/phpunit/tests/comment/walker.php
@@ -217,6 +217,246 @@ public function test_explicit_callback_takes_precedence_over_render_callback() {
$this->assertStringNotContainsString( 'FROM_TYPE_CALLBACK', $output );
}
+ /**
+ * A registered ping type renders with the compact ping markup when short_ping is on:
+ * a label, the author link, and no comment body.
+ *
+ * @ticket 35214
+ */
+ public function test_registered_ping_type_renders_as_ping() {
+ 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_content' => 'A webmention body',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $output = $this->render_comments(
+ array( get_comment( $comment_id ) ),
+ array( 'short_ping' => true )
+ );
+
+ $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 );
+ }
+
+ /**
+ * 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.
+ *
+ * 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 );
+ }
+
+ /**
+ * 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 );
+ }
+
+ /**
+ * 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 );
+ },
+ )
+ );
+
+ $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 );
+ }
+
/**
* 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 2cdba49013f42..04c7312579a93 100644
--- a/tests/phpunit/tests/comment/wpCommentType.php
+++ b/tests/phpunit/tests/comment/wpCommentType.php
@@ -24,6 +24,18 @@ public function test_instance_defaults() {
$this->assertFalse( $comment_type->_builtin );
$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 );
}
/**