Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cd33df8
Comments: Add a capabilities object to comment types.
adamsilverstein Jun 24, 2026
4eab01b
Comments: Add tests for comment type capabilities.
adamsilverstein Jun 24, 2026
c4978f3
Comments: Assert the full comment type capability set.
adamsilverstein Jun 25, 2026
520eb8e
Merge branch 'feature/register-comment-type' into feature/comment-typ…
adamsilverstein Jul 11, 2026
53ea297
Docs: Clarify the comment type capabilities contract.
adamsilverstein Jul 11, 2026
419bfc4
Merge branch 'feature/register-comment-type' into feature/comment-typ…
adamsilverstein Aug 13, 2026
2bc1f6e
Comments: Drop the unresolvable `read_comment` comment type capability.
adamsilverstein Aug 13, 2026
55145f4
Docs: Flag the comment type capabilities as advisory until enforcemen…
adamsilverstein Aug 13, 2026
787a835
Comments: Cover the shared built-in capability set and meta capabilit…
adamsilverstein Aug 13, 2026
f33be32
Merge branch 'feature/register-comment-type' into feature/comment-typ…
adamsilverstein Aug 23, 2026
9f56910
Comments: Stamp the capabilities object for 7.2.0 and tighten its docs.
adamsilverstein Aug 23, 2026
07a7492
Merge branch 'feature/register-comment-type' into feature/comment-typ…
adamsilverstein Nov 8, 2026
a32fc1e
Merge branch 'feature/register-comment-type' into feature/comment-typ…
adamsilverstein Nov 8, 2026
75676b9
Comments: Trim the comment type capabilities documentation.
adamsilverstein Nov 8, 2026
10d1fb7
Comments: Add native types to the comment type capabilities code and …
adamsilverstein Nov 8, 2026
3a929b4
Merge branch 'feature/register-comment-type' into feature/comment-typ…
adamsilverstein Nov 8, 2026
8cb0626
Docs: Mark the new comment type APIs as @since 7.3.0.
adamsilverstein Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions src/wp-includes/class-wp-comment-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ final class WP_Comment_Type {
*/
public $internal = false;

/**
* The base used to build the edit, delete, and moderate capabilities.
*
* Registered as an array of singular and plural bases, this holds the
* singular base. Default 'comment'.
*
* @since 7.3.0
* @var string
*/
public $capability_type = 'comment';

/**
* Capabilities for this comment type.
*
* Advisory only; map_meta_cap() does not consult them yet.
* See get_comment_type_capabilities().
*
* @since 7.3.0
* @var stdClass
*/
public $cap;

/**
* Whether this comment type is a native or "built-in" comment type.
*
Expand Down Expand Up @@ -172,17 +194,27 @@ public function set_props( $args ): void {

// No 'label' default, so get_comment_type_labels() can fall back to the default name.
$defaults = array(
'labels' => array(),
'description' => '',
'public' => true,
'internal' => false,
'_builtin' => false,
'labels' => array(),
'description' => '',
'public' => true,
'internal' => false,
'capability_type' => 'comment',
'capabilities' => array(),
'_builtin' => false,
);

$args = array_merge( $defaults, $args );

$args['name'] = $this->name;

$this->cap = get_comment_type_capabilities( (object) $args );
unset( $args['capabilities'] );

// Collapse an array capability type back to its singular base.
if ( is_array( $args['capability_type'] ) ) {
$args['capability_type'] = $args['capability_type'][0];
}

// The hierarchical label slots are null, so a true value would null every default label.
$args['hierarchical'] = false;

Expand Down
111 changes: 92 additions & 19 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -434,23 +434,32 @@ function create_initial_comment_types(): void {
* @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. Usually plural.
* Default is the value of $labels['name'].
* @type string[] $labels An array of labels for this comment type. If not set, the
* default comment labels are used. 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. Core does not
* currently act on this argument. Default true.
* @type bool $internal Whether the comment type is for internal use only. Internal
* types are meant to be excluded from comment queries and counts
* by default. Core does not currently act on this argument.
* Default false.
* @type bool $_builtin For internal core use only. Marks the type as native to
* WordPress, which blocks it from being re-registered or
* unregistered. Default false.
* @type string $label Name of the comment type. Usually plural.
* Default is the value of $labels['name'].
* @type string[] $labels An array of labels for this comment type. If not set, the
* default comment labels are used. 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.
* Core does not currently act on this argument.
* Default true.
* @type bool $internal Whether the comment type is for internal use only.
* Internal types are meant to be excluded from comment
* queries and counts by default. Core does not currently
* act on this argument.
* Default false.
* @type string|array $capability_type The base used to build the edit, delete, and moderate
* capabilities. May be an array of singular and plural
* bases, e.g. array( 'story', 'stories' ). Default 'comment'.
* @type string[] $capabilities Capabilities for this comment type, overriding those built
* from $capability_type. Advisory only; core does not
* enforce them. See get_comment_type_capabilities().
* @type bool $_builtin For internal core use only. Marks the type as native to
* WordPress, which blocks it from being re-registered or
* unregistered. Default false.
* }
* @return WP_Comment_Type|WP_Error The registered comment type object on success,
* WP_Error object on failure.
Expand Down Expand Up @@ -641,7 +650,7 @@ function comment_type_exists( $comment_type ) {
* @since 7.3.0
*
* @param WP_Comment_Type $comment_type_object Comment type object.
* @return stdClass {
* @return object {
* Comment type labels object.
*
* @type string $name General name for the comment type, usually plural. The same as and
Expand Down Expand Up @@ -698,6 +707,70 @@ function get_comment_type_labels( $comment_type_object ) {
return $labels;
}

/**
* Builds an object with all comment type capabilities out of a comment type object.
*
* Capabilities not set in the `capabilities` argument are built from the
* `capability_type` argument. A string base is pluralized by appending an 's';
* an array supplies the singular and plural bases, e.g. array( 'story', 'stories' ).
*
* The capabilities are advisory: map_meta_cap() does not consult them yet, and
* most of the generated names are not granted to any default role.
*
* Do not reuse a post type's `capability_type` as the base. A base of 'post'
* generates `edit_comment => 'edit_post'`, which map_meta_cap() resolves
* against a post with the comment's ID.
*
* Normalizes the `capability_type` property of `$args` to its array form.
*
* @since 7.3.0
*
* @param object $args Comment type registration arguments. Expects the
* `capability_type` and `capabilities` properties.
* @return stdClass {
* Object with all the capabilities as member variables.
*
* @type string $edit_comment Meta capability to edit a comment of this type.
* Default 'edit_comment'.
* @type string $delete_comment Meta capability to delete a comment of this type.
* Default 'delete_comment'.
* @type string $moderate_comment Meta capability to moderate a comment of this type.
* Default 'moderate_comment'.
* @type string $edit_comments Primitive capability to edit comments of this type.
* Default 'edit_comments'.
* @type string $edit_others_comments Primitive capability to edit comments of this type
* authored by other users. Default 'edit_others_comments'.
* @type string $delete_comments Primitive capability to delete comments of this type.
* Default 'delete_comments'.
* @type string $moderate_comments Primitive capability to moderate comments of this type.
* Default 'moderate_comments'.
* }
*/
function get_comment_type_capabilities( object $args ): stdClass {
if ( ! is_array( $args->capability_type ) ) {
$args->capability_type = array( $args->capability_type, $args->capability_type . 's' );
}

// Singular base for meta capabilities, plural base for primitive capabilities.
list( $singular_base, $plural_base ) = $args->capability_type;

$default_capabilities = array(
// Meta capabilities.
'edit_comment' => 'edit_' . $singular_base,
'delete_comment' => 'delete_' . $singular_base,
'moderate_comment' => 'moderate_' . $singular_base,
// Primitive capabilities.
'edit_comments' => 'edit_' . $plural_base,
'edit_others_comments' => 'edit_others_' . $plural_base,
'delete_comments' => 'delete_' . $plural_base,
'moderate_comments' => 'moderate_' . $plural_base,
);

$capabilities = array_merge( $default_capabilities, $args->capabilities );

return (object) $capabilities;
}

/**
* Retrieves all of the WordPress supported comment statuses.
*
Expand Down Expand Up @@ -1887,7 +1960,7 @@ function wp_check_comment_disallowed_list( $author, $email, $url, $comment, $use
*
* @param int $post_id Optional. Restrict the comment counts to the given post. Default 0, which indicates that
* comment counts for the whole site will be retrieved.
* @return stdClass {
* @return object {
* The number of comments keyed by their status.
*
* @type int $approved The number of approved comments.
Expand Down
180 changes: 180 additions & 0 deletions tests/phpunit/tests/comment/types.php
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,186 @@ public function test_registered_comment_type_action_receives_type_and_object():
$this->assertSame( 'foo', $args[0][1]->name );
}

/**
* @ticket 35214
*
* @covers ::register_comment_type
*/
public function test_registered_comment_type_exposes_cap_object(): void {
register_comment_type( 'foo', array( 'capability_type' => 'review' ) );

$cobj = get_comment_type_object( 'foo' );

$this->assertSame( 'edit_reviews', $cobj->cap->edit_comments );
$this->assertSame( 'moderate_reviews', $cobj->cap->moderate_comments );
}

/**
* The built-in comment type's capabilities match the existing core comment capabilities.
*
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_built_in_comment_type_capabilities_are_backward_compatible(): void {
$cobj = get_comment_type_object( 'comment' );

$this->assertSame( 'edit_comment', $cobj->cap->edit_comment );
$this->assertSame( 'moderate_comments', $cobj->cap->moderate_comments );
}

/**
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_get_comment_type_capabilities_from_string(): void {
$caps = get_comment_type_capabilities(
(object) array(
'capability_type' => 'review',
'capabilities' => array(),
)
);

$this->assertSame( 'edit_review', $caps->edit_comment );
$this->assertSame( 'edit_reviews', $caps->edit_comments );
$this->assertSame( 'edit_others_reviews', $caps->edit_others_comments );
$this->assertSame( 'delete_review', $caps->delete_comment );
$this->assertSame( 'moderate_reviews', $caps->moderate_comments );
}

/**
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_get_comment_type_capabilities_honors_capabilities_override(): void {
$caps = get_comment_type_capabilities(
(object) array(
'capability_type' => 'comment',
'capabilities' => array(
'edit_comments' => 'manage_stuff',
),
)
);

$this->assertSame( 'manage_stuff', $caps->edit_comments );
}

/**
* The full set of meta and primitive capabilities is generated from the base.
*
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_get_comment_type_capabilities_generates_full_set(): void {
$caps = get_comment_type_capabilities(
(object) array(
'capability_type' => 'review',
'capabilities' => array(),
)
);

$expected = array(
// Meta capabilities.
'edit_comment' => 'edit_review',
'delete_comment' => 'delete_review',
'moderate_comment' => 'moderate_review',
// Primitive capabilities.
'edit_comments' => 'edit_reviews',
'edit_others_comments' => 'edit_others_reviews',
'delete_comments' => 'delete_reviews',
'moderate_comments' => 'moderate_reviews',
);

$this->assertSame( $expected, (array) $caps );
}

/**
* An array capability type supplies an explicit plural base for primitive caps.
*
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_get_comment_type_capabilities_from_array(): void {
$caps = get_comment_type_capabilities(
(object) array(
'capability_type' => array( 'story', 'stories' ),
'capabilities' => array(),
)
);

// Singular base drives the meta capabilities.
$this->assertSame( 'edit_story', $caps->edit_comment );
$this->assertSame( 'delete_story', $caps->delete_comment );
// Explicit plural base drives the primitive capabilities.
$this->assertSame( 'edit_stories', $caps->edit_comments );
$this->assertSame( 'delete_stories', $caps->delete_comments );
$this->assertSame( 'moderate_stories', $caps->moderate_comments );
}

/**
* No 'read_comment' meta capability is generated, as core defines no semantics
* for reading a single comment.
*
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_get_comment_type_capabilities_omits_read_comment(): void {
$caps = get_comment_type_capabilities(
(object) array(
'capability_type' => 'review',
'capabilities' => array(),
)
);

$this->assertObjectNotHasProperty( 'read_comment', $caps );
$this->assertObjectNotHasProperty( 'read_comment', get_comment_type_object( 'comment' )->cap );
}

/**
* The built-in types register without a capability type, so they share one cap set.
*
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*
* @dataProvider data_built_in_comment_types
*
* @param string $comment_type Built-in comment type name.
*/
public function test_built_in_comment_types_share_the_comment_capabilities( string $comment_type ): void {
$this->assertEquals(
get_comment_type_object( 'comment' )->cap,
get_comment_type_object( $comment_type )->cap
);
}

/**
* An override applies to meta capabilities as well as primitives, so a type can point a
* meta capability at a name that map_meta_cap() already resolves.
*
* @ticket 35214
*
* @covers ::get_comment_type_capabilities
*/
public function test_get_comment_type_capabilities_honors_a_meta_capability_override(): void {
$caps = get_comment_type_capabilities(
(object) array(
'capability_type' => 'review',
'capabilities' => array(
'edit_comment' => 'edit_review_item',
),
)
);

$this->assertSame( 'edit_review_item', $caps->edit_comment, 'The override should win.' );
$this->assertSame( 'delete_review', $caps->delete_comment, 'The rest should still derive from the base.' );
}

/**
* Comment types are never hierarchical. The default labels reserve the hierarchical
* slot as null, so honoring a provided value would resolve every label to null.
Expand Down
Loading