diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php
new file mode 100644
index 0000000000000..6f88c5e7b499d
--- /dev/null
+++ b/src/wp-includes/class-wp-comment-type.php
@@ -0,0 +1,243 @@
+name = $comment_type;
+
+ $this->set_props( $args );
+ }
+
+ /**
+ * Sets comment type properties.
+ *
+ * See the register_comment_type() function for accepted arguments for `$args`.
+ *
+ * @since 7.1.0
+ *
+ * @param array|string $args Array or string of arguments for registering a comment type.
+ */
+ public function set_props( $args ) {
+ $args = wp_parse_args( $args );
+
+ /**
+ * Filters the arguments for registering a comment type.
+ *
+ * @since 7.1.0
+ *
+ * @param array $args Array of arguments for registering a comment type.
+ * See the register_comment_type() function for accepted arguments.
+ * @param string $comment_type Comment type key.
+ */
+ $args = apply_filters( 'register_comment_type_args', $args, $this->name );
+
+ $comment_type = $this->name;
+
+ /**
+ * Filters the arguments for registering a specific comment type.
+ *
+ * The dynamic portion of the filter name, `$comment_type`, refers to the comment type key.
+ *
+ * Possible hook names include:
+ *
+ * - `register_comment_comment_type_args`
+ * - `register_pingback_comment_type_args`
+ *
+ * @since 7.1.0
+ *
+ * @param array $args Array of arguments for registering a comment type.
+ * See the register_comment_type() function for accepted arguments.
+ * @param string $comment_type Comment type key.
+ */
+ $args = apply_filters( "register_{$comment_type}_comment_type_args", $args, $this->name );
+
+ /*
+ * Note: 'label' is intentionally omitted from the defaults. Leaving the property
+ * unset (null) lets get_comment_type_labels() fall back to the default labels, the
+ * same way WP_Post_Type and WP_Taxonomy behave. A 'label' default of false would be
+ * treated as a provided value and overwrite the default name with false.
+ */
+ $defaults = array(
+ 'labels' => array(),
+ 'description' => '',
+ 'public' => true,
+ 'internal' => false,
+ 'show_ui' => null,
+ '_builtin' => false,
+ );
+
+ $args = array_merge( $defaults, $args );
+
+ // If not set, default to the setting for 'public'.
+ if ( null === $args['show_ui'] ) {
+ $args['show_ui'] = $args['public'];
+ }
+
+ $args['name'] = $this->name;
+
+ foreach ( $args as $property_name => $property_value ) {
+ $this->$property_name = $property_value;
+ }
+
+ $this->labels = get_comment_type_labels( $this );
+ $this->label = $this->labels->name;
+ }
+
+ /**
+ * Returns the default labels for comment types.
+ *
+ * @since 7.1.0
+ *
+ * @return (string|null)[][] The default labels for comment types.
+ */
+ public static function get_default_labels() {
+ if ( ! empty( self::$default_labels ) ) {
+ return self::$default_labels;
+ }
+
+ self::$default_labels = array(
+ 'name' => array( _x( 'Comments', 'comment type general name' ), null ),
+ 'singular_name' => array( _x( 'Comment', 'comment type singular name' ), null ),
+ );
+
+ return self::$default_labels;
+ }
+
+ /**
+ * Resets the cache for the default labels.
+ *
+ * @since 7.1.0
+ */
+ public static function reset_default_labels() {
+ self::$default_labels = array();
+ }
+}
diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php
index 43bd68ff972a4..5dc0c76ac3b21 100644
--- a/src/wp-includes/comment-template.php
+++ b/src/wp-includes/comment-template.php
@@ -1185,6 +1185,9 @@ function get_comment_type( $comment_id = 0 ) {
* @param string|false $pingback_text Optional. String to display for pingback type. Default false.
*/
function comment_type( $comment_text = false, $trackback_text = false, $pingback_text = false ) {
+ // Whether the caller supplied custom text for the default comment label.
+ $comment_text_overridden = ( false !== $comment_text );
+
if ( false === $comment_text ) {
$comment_text = _x( 'Comment', 'noun' );
}
@@ -1203,7 +1206,18 @@ function comment_type( $comment_text = false, $trackback_text = false, $pingback
echo $pingback_text;
break;
default:
- echo $comment_text;
+ /*
+ * For a registered, non-built-in comment type, fall back to its singular label
+ * when the caller did not supply custom text. Built-in types and explicit
+ * overrides keep their existing output.
+ */
+ $comment_type_object = $comment_text_overridden ? null : get_comment_type_object( $type );
+
+ if ( $comment_type_object && ! $comment_type_object->_builtin && isset( $comment_type_object->labels->singular_name ) ) {
+ echo esc_html( $comment_type_object->labels->singular_name );
+ } else {
+ echo $comment_text;
+ }
}
}
diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php
index b93908adc0519..e8b4f28fe8ed9 100644
--- a/src/wp-includes/comment.php
+++ b/src/wp-includes/comment.php
@@ -273,6 +273,302 @@ function get_comments( $args = '' ) {
return $query->query( $args );
}
+/**
+ * Creates the initial comment types when 'init' action is fired.
+ *
+ * See register_comment_type() for accepted arguments.
+ *
+ * @since 7.1.0
+ */
+function create_initial_comment_types() {
+ WP_Comment_Type::reset_default_labels();
+
+ register_comment_type(
+ 'comment',
+ array(
+ 'label' => __( 'Comments' ),
+ 'labels' => array(
+ 'singular_name' => _x( 'Comment', 'noun' ),
+ ),
+ 'public' => true,
+ '_builtin' => true,
+ )
+ );
+
+ register_comment_type(
+ 'pingback',
+ array(
+ 'label' => __( 'Pingbacks' ),
+ 'labels' => array(
+ 'singular_name' => __( 'Pingback' ),
+ ),
+ 'public' => true,
+ '_builtin' => true,
+ )
+ );
+
+ register_comment_type(
+ 'trackback',
+ array(
+ 'label' => __( 'Trackbacks' ),
+ 'labels' => array(
+ 'singular_name' => __( 'Trackback' ),
+ ),
+ 'public' => true,
+ '_builtin' => true,
+ )
+ );
+
+ register_comment_type(
+ 'note',
+ array(
+ 'label' => _x( 'Notes', 'comment type general name' ),
+ 'labels' => array(
+ 'singular_name' => _x( 'Note', 'comment type singular name' ),
+ ),
+ 'public' => false,
+ 'internal' => true,
+ '_builtin' => true,
+ )
+ );
+}
+
+/**
+ * Registers a comment type.
+ *
+ * Note: Comment type registrations should not be hooked before the {@see 'init'} action.
+ * This is because comment type slugs need to be reserved as part of the upgrade routine
+ * and global variables need to be available for the comment type to register itself.
+ *
+ * Comment types are stored verbatim in the `comment_type` column of the comments table.
+ * Registration provides labels and metadata for a type; it does not constrain which values
+ * may be stored.
+ *
+ * @since 7.1.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param string $comment_type Comment type key. Must not exceed 20 characters and may only
+ * contain lowercase alphanumeric characters, dashes, and underscores.
+ * See sanitize_key().
+ * @param array|string $args {
+ * Optional. Array or string of arguments for registering a comment type. Default empty array.
+ *
+ * @type string $label Name of the comment type shown in the menu. Usually plural.
+ * Default is value of $labels['name'].
+ * @type string[] $labels An array of labels for this comment type. If not set, comment
+ * labels are inherited. See get_comment_type_labels() for a full
+ * list of supported labels.
+ * @type string $description A short descriptive summary of what the comment type is.
+ * Default empty.
+ * @type bool $public Whether the comment type is intended for use publicly either via
+ * the admin interface or by front-end users. Default true.
+ * @type bool $internal Whether the comment type is for internal use only and should be
+ * 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.
+ * }
+ * @return WP_Comment_Type|WP_Error The registered comment type object on success,
+ * WP_Error object on failure.
+ */
+function register_comment_type( $comment_type, $args = array() ) {
+ global $wp_comment_types;
+
+ if ( ! is_array( $wp_comment_types ) ) {
+ $wp_comment_types = array();
+ }
+
+ // Sanitize comment type name.
+ $comment_type = sanitize_key( $comment_type );
+
+ if ( empty( $comment_type ) || strlen( $comment_type ) > 20 ) {
+ _doing_it_wrong( __FUNCTION__, __( 'Comment type names must be between 1 and 20 characters in length.' ), '7.1.0' );
+ return new WP_Error( 'comment_type_length_invalid', __( 'Comment type names must be between 1 and 20 characters in length.' ) );
+ }
+
+ $comment_type_object = new WP_Comment_Type( $comment_type, $args );
+
+ $wp_comment_types[ $comment_type ] = $comment_type_object;
+
+ /**
+ * Fires after a comment type is registered.
+ *
+ * @since 7.1.0
+ *
+ * @param string $comment_type Comment type key.
+ * @param WP_Comment_Type $comment_type_object Comment type object.
+ */
+ do_action( 'registered_comment_type', $comment_type, $comment_type_object );
+
+ /**
+ * Fires after a specific comment type is registered.
+ *
+ * The dynamic portion of the filter name, `$comment_type`, refers to the comment type key.
+ *
+ * Possible hook names include:
+ *
+ * - `registered_comment_type_comment`
+ * - `registered_comment_type_pingback`
+ *
+ * @since 7.1.0
+ *
+ * @param string $comment_type Comment type key.
+ * @param WP_Comment_Type $comment_type_object Comment type object.
+ */
+ do_action( "registered_comment_type_{$comment_type}", $comment_type, $comment_type_object );
+
+ return $comment_type_object;
+}
+
+/**
+ * Unregisters a comment type.
+ *
+ * Cannot be used to unregister built-in comment types.
+ *
+ * @since 7.1.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param string $comment_type Comment type key.
+ * @return true|WP_Error True on success, WP_Error on failure or if the comment type doesn't exist.
+ */
+function unregister_comment_type( $comment_type ) {
+ global $wp_comment_types;
+
+ if ( ! comment_type_exists( $comment_type ) ) {
+ return new WP_Error( 'invalid_comment_type', __( 'Invalid comment type.' ) );
+ }
+
+ $comment_type_object = get_comment_type_object( $comment_type );
+
+ // Do not allow unregistering built-in comment types.
+ if ( $comment_type_object->_builtin ) {
+ return new WP_Error( 'invalid_comment_type', __( 'Unregistering a built-in comment type is not allowed.' ) );
+ }
+
+ unset( $wp_comment_types[ $comment_type ] );
+
+ /**
+ * Fires after a comment type is unregistered.
+ *
+ * @since 7.1.0
+ *
+ * @param string $comment_type Comment type key.
+ */
+ do_action( 'unregistered_comment_type', $comment_type );
+
+ return true;
+}
+
+/**
+ * Retrieves a comment type object by name.
+ *
+ * @since 7.1.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param string $comment_type The name of a registered comment type.
+ * @return WP_Comment_Type|null WP_Comment_Type object if it exists, null otherwise.
+ */
+function get_comment_type_object( $comment_type ) {
+ global $wp_comment_types;
+
+ if ( ! is_scalar( $comment_type ) || empty( $wp_comment_types[ $comment_type ] ) ) {
+ return null;
+ }
+
+ return $wp_comment_types[ $comment_type ];
+}
+
+/**
+ * Retrieves a list of registered comment type names or objects.
+ *
+ * @since 7.1.0
+ *
+ * @global WP_Comment_Type[] $wp_comment_types List of comment types.
+ *
+ * @param array|string $args Optional. An array of key => value arguments to match against
+ * the comment type objects. Default empty array.
+ * @param string $output Optional. The type of output to return. Either comment type 'names'
+ * or 'objects'. Default 'names'.
+ * @param string $operator Optional. The logical operation to perform. 'or' means only one
+ * element from the array needs to match; 'and' means all elements
+ * must match; 'not' means no elements may match. Default 'and'.
+ * @return string[]|WP_Comment_Type[] An array of comment type names or objects.
+ */
+function get_comment_types( $args = array(), $output = 'names', $operator = 'and' ) {
+ global $wp_comment_types;
+
+ $field = ( 'names' === $output ) ? 'name' : false;
+
+ return wp_filter_object_list( $wp_comment_types, $args, $operator, $field );
+}
+
+/**
+ * Determines whether a comment type is registered.
+ *
+ * @since 7.1.0
+ *
+ * @param string $comment_type Comment type name.
+ * @return bool Whether the comment type is registered.
+ */
+function comment_type_exists( $comment_type ) {
+ return (bool) get_comment_type_object( $comment_type );
+}
+
+/**
+ * Builds an object with all comment type labels out of a comment type object.
+ *
+ * @since 7.1.0
+ *
+ * @param WP_Comment_Type $comment_type_object Comment type object.
+ * @return object {
+ * Comment type labels object.
+ *
+ * @type string $name General name for the comment type, usually plural. The same and
+ * overridden by `$comment_type_object->label`. Default 'Comments'.
+ * @type string $singular_name Name for one object of this comment type. Default 'Comment'.
+ * @type string $menu_name Label for the menu name. Default is the same as `name`.
+ * }
+ */
+function get_comment_type_labels( $comment_type_object ) {
+ $nohier_vs_hier_defaults = WP_Comment_Type::get_default_labels();
+
+ $nohier_vs_hier_defaults['menu_name'] = $nohier_vs_hier_defaults['name'];
+
+ $labels = _get_custom_object_labels( $comment_type_object, $nohier_vs_hier_defaults );
+
+ $comment_type = $comment_type_object->name;
+
+ $default_labels = clone $labels;
+
+ /**
+ * Filters the labels of a specific comment type.
+ *
+ * The dynamic portion of the hook name, `$comment_type`, refers to the comment type slug.
+ *
+ * Possible hook names include:
+ *
+ * - `comment_type_labels_comment`
+ * - `comment_type_labels_pingback`
+ *
+ * @since 7.1.0
+ *
+ * @see get_comment_type_labels() for the full list of comment type labels.
+ *
+ * Labels are stored unescaped, mirroring the post type and taxonomy label
+ * contract; callers must escape them on output (for example with esc_html()).
+ *
+ * @param object $labels Object with labels for the comment type as member variables.
+ */
+ $labels = apply_filters( "comment_type_labels_{$comment_type}", $labels );
+
+ // Ensure that the filtered labels contain all required default values.
+ $labels = (object) array_merge( (array) $default_labels, (array) $labels );
+
+ return $labels;
+}
+
/**
* Retrieves all of the WordPress supported comment statuses.
*
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 5581828a10b61..966c69cca5cf9 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -532,6 +532,10 @@
add_action( 'split_shared_term', '_wp_check_split_nav_menu_terms', 10, 4 );
add_action( 'wp_split_shared_term_batch', '_wp_batch_split_terms' );
+// Comment types.
+add_action( 'init', 'create_initial_comment_types', 0 ); // Highest priority.
+add_action( 'change_locale', 'create_initial_comment_types' );
+
// Comment type updates.
add_action( 'admin_init', '_wp_check_for_scheduled_update_comment_type' );
add_action( 'wp_update_comment_type_batch', '_wp_batch_update_comment_type' );
diff --git a/src/wp-settings.php b/src/wp-settings.php
index ef5c7784ee561..9ec2c3607271d 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -232,6 +232,7 @@
require ABSPATH . WPINC . '/comment.php';
require ABSPATH . WPINC . '/class-wp-comment.php';
require ABSPATH . WPINC . '/class-wp-comment-query.php';
+require ABSPATH . WPINC . '/class-wp-comment-type.php';
require ABSPATH . WPINC . '/class-walker-comment.php';
require ABSPATH . WPINC . '/comment-template.php';
require ABSPATH . WPINC . '/rewrite.php';
@@ -554,10 +555,11 @@
// Create common globals.
require ABSPATH . WPINC . '/vars.php';
-// Make taxonomies and posts available to plugins and themes.
+// Make taxonomies, posts, and comment types available to plugins and themes.
// @plugin authors: warning: these get registered again on the init hook.
create_initial_taxonomies();
create_initial_post_types();
+create_initial_comment_types();
wp_start_scraping_edited_file_errors();
diff --git a/tests/phpunit/tests/comment/commentType.php b/tests/phpunit/tests/comment/commentType.php
new file mode 100644
index 0000000000000..0f0010001204b
--- /dev/null
+++ b/tests/phpunit/tests/comment/commentType.php
@@ -0,0 +1,138 @@
+post->create();
+ }
+
+ public function tear_down() {
+ global $wp_comment_types;
+
+ foreach ( array_keys( $wp_comment_types ) as $comment_type ) {
+ if ( ! $wp_comment_types[ $comment_type ]->_builtin ) {
+ unset( $wp_comment_types[ $comment_type ] );
+ }
+ }
+
+ parent::tear_down();
+ }
+
+ /**
+ * Returns the output of comment_type() for a comment of the given type.
+ *
+ * @param string $type Comment type stored on the comment.
+ * @param mixed ...$args Optional arguments passed through to comment_type().
+ * @return string Captured output.
+ */
+ private function get_comment_type_output( $type, ...$args ) {
+ $comment_id = self::factory()->comment->create(
+ array(
+ 'comment_post_ID' => self::$post_id,
+ 'comment_type' => $type,
+ )
+ );
+
+ $GLOBALS['comment'] = get_comment( $comment_id );
+
+ ob_start();
+ comment_type( ...$args );
+ $output = ob_get_clean();
+
+ unset( $GLOBALS['comment'] );
+
+ return $output;
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_built_in_types_output_is_unchanged() {
+ $this->assertSame( 'Comment', $this->get_comment_type_output( 'comment' ) );
+ $this->assertSame( 'Trackback', $this->get_comment_type_output( 'trackback' ) );
+ $this->assertSame( 'Pingback', $this->get_comment_type_output( 'pingback' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_custom_text_overrides_are_respected() {
+ $this->assertSame( 'C', $this->get_comment_type_output( 'comment', 'C', 'T', 'P' ) );
+ $this->assertSame( 'T', $this->get_comment_type_output( 'trackback', 'C', 'T', 'P' ) );
+ $this->assertSame( 'P', $this->get_comment_type_output( 'pingback', 'C', 'T', 'P' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_registered_custom_type_outputs_its_label() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'labels' => array(
+ 'singular_name' => 'Foo',
+ ),
+ )
+ );
+
+ $this->assertSame( 'Foo', $this->get_comment_type_output( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_unregistered_custom_type_falls_back_to_default_label() {
+ $this->assertSame( _x( 'Comment', 'noun' ), $this->get_comment_type_output( 'bar' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_custom_text_override_wins_over_registered_label() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'labels' => array(
+ 'singular_name' => 'Foo',
+ ),
+ )
+ );
+
+ $this->assertSame( 'Custom', $this->get_comment_type_output( 'foo', 'Custom' ) );
+ }
+
+ /**
+ * The registered label is escaped on output to guard against HTML/script injection.
+ *
+ * @ticket 35214
+ */
+ public function test_registered_label_is_escaped_on_output() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'labels' => array(
+ 'singular_name' => '
Foo',
+ ),
+ )
+ );
+
+ $this->assertSame(
+ esc_html( '
Foo' ),
+ $this->get_comment_type_output( 'foo' )
+ );
+ }
+}
diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php
new file mode 100644
index 0000000000000..54f8e2aa9ad33
--- /dev/null
+++ b/tests/phpunit/tests/comment/types.php
@@ -0,0 +1,294 @@
+_builtin ) {
+ unset( $wp_comment_types[ $comment_type ] );
+ }
+ }
+
+ parent::tear_down();
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_register_comment_type() {
+ $this->assertNull( get_comment_type_object( 'foo' ) );
+
+ register_comment_type( 'foo' );
+
+ $cobj = get_comment_type_object( 'foo' );
+ $this->assertInstanceOf( 'WP_Comment_Type', $cobj );
+ $this->assertSame( 'foo', $cobj->name );
+
+ // Test some defaults.
+ $this->assertTrue( $cobj->public );
+ $this->assertFalse( $cobj->internal );
+ $this->assertFalse( $cobj->_builtin );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_register_comment_type_without_labels_uses_default_labels() {
+ register_comment_type( 'foo' );
+
+ $cobj = get_comment_type_object( 'foo' );
+
+ $this->assertSame( 'Comments', $cobj->label );
+ $this->assertSame( 'Comments', $cobj->labels->name );
+ $this->assertSame( 'Comment', $cobj->labels->singular_name );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_register_comment_type_return_value() {
+ $this->assertInstanceOf( 'WP_Comment_Type', register_comment_type( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @expectedIncorrectUsage register_comment_type
+ */
+ public function test_register_comment_type_with_too_long_name() {
+ $this->assertInstanceOf( 'WP_Error', register_comment_type( 'comment_type_with_a_too_long_name' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @expectedIncorrectUsage register_comment_type
+ */
+ public function test_register_comment_type_with_empty_name() {
+ $this->assertInstanceOf( 'WP_Error', register_comment_type( '' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_register_comment_type_show_ui_should_default_to_value_of_public() {
+ register_comment_type( 'public_type', array( 'public' => true ) );
+ $this->assertTrue( get_comment_type_object( 'public_type' )->show_ui );
+
+ register_comment_type( 'private_type', array( 'public' => false ) );
+ $this->assertFalse( get_comment_type_object( 'private_type' )->show_ui );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_built_in_comment_types_are_registered() {
+ $this->assertTrue( comment_type_exists( 'comment' ) );
+ $this->assertTrue( comment_type_exists( 'pingback' ) );
+ $this->assertTrue( comment_type_exists( 'trackback' ) );
+ $this->assertTrue( comment_type_exists( 'note' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_built_in_note_type_is_internal_and_non_public() {
+ $note = get_comment_type_object( 'note' );
+
+ $this->assertTrue( $note->internal );
+ $this->assertFalse( $note->public );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_comment_type_exists() {
+ $this->assertFalse( comment_type_exists( 'foo' ) );
+
+ register_comment_type( 'foo' );
+
+ $this->assertTrue( comment_type_exists( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_get_comment_types_names() {
+ register_comment_type( 'foo' );
+
+ $types = get_comment_types();
+
+ $this->assertContains( 'comment', $types );
+ $this->assertContains( 'foo', $types );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_get_comment_types_objects() {
+ register_comment_type( 'foo' );
+
+ $types = get_comment_types( array(), 'objects' );
+
+ $this->assertInstanceOf( 'WP_Comment_Type', $types['foo'] );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_get_comment_types_filtered_by_property() {
+ register_comment_type( 'foo', array( 'public' => false ) );
+
+ $public = get_comment_types( array( 'public' => true ) );
+
+ $this->assertContains( 'comment', $public );
+ $this->assertNotContains( 'foo', $public );
+ $this->assertNotContains( 'note', $public );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregister_comment_type() {
+ register_comment_type( 'foo' );
+
+ $this->assertTrue( unregister_comment_type( 'foo' ) );
+ $this->assertNull( get_comment_type_object( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregister_comment_type_unknown_returns_error() {
+ $this->assertWPError( unregister_comment_type( 'does_not_exist' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ */
+ public function test_unregister_comment_type_twice_returns_error() {
+ register_comment_type( 'foo' );
+
+ $this->assertTrue( unregister_comment_type( 'foo' ) );
+ $this->assertWPError( unregister_comment_type( 'foo' ) );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::unregister_comment_type
+ *
+ * @dataProvider data_built_in_comment_types
+ */
+ public function test_unregister_built_in_comment_type_is_not_allowed( $comment_type ) {
+ $this->assertWPError( unregister_comment_type( $comment_type ) );
+ $this->assertTrue( comment_type_exists( $comment_type ) );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_built_in_comment_types() {
+ return array(
+ array( 'comment' ),
+ array( 'pingback' ),
+ array( 'trackback' ),
+ array( 'note' ),
+ );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_registered_comment_type_actions_fire() {
+ $action = new MockAction();
+ $action_for_foo = new MockAction();
+
+ add_action( 'registered_comment_type', array( $action, 'action' ) );
+ add_action( 'registered_comment_type_foo', array( $action_for_foo, 'action' ) );
+
+ register_comment_type( 'foo' );
+
+ $this->assertSame( 1, $action->get_call_count() );
+ $this->assertSame( 1, $action_for_foo->get_call_count() );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_unregistered_comment_type_action_fires() {
+ register_comment_type( 'foo' );
+
+ $action = new MockAction();
+ add_action( 'unregistered_comment_type', array( $action, 'action' ) );
+
+ unregister_comment_type( 'foo' );
+
+ $this->assertSame( 1, $action->get_call_count() );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_labels_are_built_from_args() {
+ register_comment_type(
+ 'foo',
+ array(
+ 'label' => 'Foos',
+ 'labels' => array(
+ 'singular_name' => 'Foo',
+ ),
+ )
+ );
+
+ $cobj = get_comment_type_object( 'foo' );
+
+ $this->assertSame( 'Foos', $cobj->label );
+ $this->assertSame( 'Foos', $cobj->labels->name );
+ $this->assertSame( 'Foo', $cobj->labels->singular_name );
+ }
+
+ /**
+ * @ticket 35214
+ */
+ public function test_comment_type_labels_filter() {
+ add_filter(
+ 'comment_type_labels_foo',
+ static function ( $labels ) {
+ $labels->singular_name = 'Filtered Foo';
+ return $labels;
+ }
+ );
+
+ register_comment_type( 'foo' );
+
+ $this->assertSame( 'Filtered Foo', get_comment_type_object( 'foo' )->labels->singular_name );
+ }
+}
diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php
new file mode 100644
index 0000000000000..bf6d3c7df28dd
--- /dev/null
+++ b/tests/phpunit/tests/comment/wpCommentType.php
@@ -0,0 +1,120 @@
+assertSame( 'foo', $comment_type->name );
+ $this->assertTrue( $comment_type->public );
+ $this->assertFalse( $comment_type->internal );
+ $this->assertFalse( $comment_type->_builtin );
+ $this->assertTrue( $comment_type->show_ui );
+ $this->assertFalse( $comment_type->hierarchical );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_set_props_overrides_defaults() {
+ $comment_type = new WP_Comment_Type(
+ 'foo',
+ array(
+ 'public' => false,
+ 'internal' => true,
+ 'description' => 'A test comment type.',
+ )
+ );
+
+ $this->assertFalse( $comment_type->public );
+ $this->assertTrue( $comment_type->internal );
+ $this->assertSame( 'A test comment type.', $comment_type->description );
+ // show_ui follows public when not explicitly set.
+ $this->assertFalse( $comment_type->show_ui );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_register_comment_type_args_filter() {
+ $filter = static function ( $args ) {
+ $args['public'] = false;
+ return $args;
+ };
+
+ add_filter( 'register_comment_type_args', $filter );
+ $comment_type = new WP_Comment_Type( 'foo' );
+ remove_filter( 'register_comment_type_args', $filter );
+
+ $this->assertFalse( $comment_type->public );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::set_props
+ */
+ public function test_register_specific_comment_type_args_filter() {
+ $filter = static function ( $args ) {
+ $args['description'] = 'Filtered description.';
+ return $args;
+ };
+
+ add_filter( 'register_foo_comment_type_args', $filter );
+ $comment_type = new WP_Comment_Type( 'foo' );
+ $other_type = new WP_Comment_Type( 'bar' );
+ remove_filter( 'register_foo_comment_type_args', $filter );
+
+ $this->assertSame( 'Filtered description.', $comment_type->description );
+ $this->assertSame( '', $other_type->description );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_default_labels
+ * @covers ::reset_default_labels
+ */
+ public function test_get_default_labels_returns_expected_defaults() {
+ WP_Comment_Type::reset_default_labels();
+
+ $labels = WP_Comment_Type::get_default_labels();
+
+ $this->assertSame( 'Comments', $labels['name'][0] );
+ $this->assertSame( 'Comment', $labels['singular_name'][0] );
+ }
+
+ /**
+ * @ticket 35214
+ *
+ * @covers ::get_default_labels
+ * @covers ::reset_default_labels
+ */
+ public function test_reset_default_labels_clears_cache() {
+ // Prime the cache, then mutate the returned (by-value) array.
+ WP_Comment_Type::get_default_labels();
+
+ WP_Comment_Type::reset_default_labels();
+
+ // A fresh call rebuilds the defaults from translation functions.
+ $labels = WP_Comment_Type::get_default_labels();
+ $this->assertSame( 'Comments', $labels['name'][0] );
+ }
+}