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 ); } /**