From cd33df87b23d0cc122c4865a57733e27b25609b4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 15:28:25 -0700 Subject: [PATCH 1/3] Comments: Add a capabilities object to comment types. Give `WP_Comment_Type` a `cap` object built from new `capability_type` and `capabilities` registration arguments, modeled on `WP_Post_Type` and `get_post_type_capabilities()`. A new `get_comment_type_capabilities()` helper builds the capability strings from the `capability_type` base (default 'comment'), so a registered type can describe its own read, edit, delete, and moderate capabilities. This is advisory metadata only: `map_meta_cap()` is intentionally not changed, so there is no behavior change. The built-in `comment` type resolves to the existing `edit_comment` and `moderate_comments` capabilities, preserving current behavior. Enforcing per-type capabilities through `map_meta_cap()` is left to a follow-up so the capability model can be agreed on first. See #35214. --- src/wp-includes/class-wp-comment-type.php | 49 ++++++++++++++++--- src/wp-includes/comment.php | 59 ++++++++++++++++++++++- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 6f88c5e7b499d..b30e13f33d01c 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -94,6 +94,32 @@ final class WP_Comment_Type { */ public $show_ui; + /** + * The string to use to build the read, edit, and delete capabilities. + * + * May be passed as an array to allow for alternative plurals when using + * this argument as a base to construct the capabilities, e.g. + * array( 'story', 'stories' ). Default 'comment'. + * + * @since 7.1.0 + * @var string|array + */ + public $capability_type = 'comment'; + + /** + * Capabilities for this comment type. + * + * Built by {@see get_comment_type_capabilities()} from the + * `capability_type` and `capabilities` arguments. This is advisory metadata + * describing the capabilities associated with the comment type; the + * capability mapping in {@see map_meta_cap()} is not affected by this + * property in this release. + * + * @since 7.1.0 + * @var stdClass + */ + public $cap; + /** * Whether this comment type is a native or "built-in" comment type. * @@ -187,12 +213,14 @@ public function set_props( $args ) { * 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, + 'labels' => array(), + 'description' => '', + 'public' => true, + 'internal' => false, + 'show_ui' => null, + 'capability_type' => 'comment', + 'capabilities' => array(), + '_builtin' => false, ); $args = array_merge( $defaults, $args ); @@ -204,6 +232,15 @@ public function set_props( $args ) { $args['name'] = $this->name; + // Build the capabilities object, then remove the input array from the props. + $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]; + } + foreach ( $args as $property_name => $property_value ) { $this->$property_name = $property_value; } diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index e8b4f28fe8ed9..5aec1d5301cb4 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -365,8 +365,16 @@ function create_initial_comment_types() { * 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. + * @type bool $show_ui Whether to generate and allow a UI for managing this comment + * type in the admin. Default is value of $public. + * @type string|array $capability_type The string to use to build the read, edit, and delete + * capabilities. May be passed as an array to allow for + * alternative plurals when using this argument as a base to + * construct the capabilities, e.g. array( 'story', 'stories' ). + * Default 'comment'. + * @type string[] $capabilities Array of capabilities for this comment type. $capability_type + * is used as a base to construct capabilities by default. + * See get_comment_type_capabilities(). * } * @return WP_Comment_Type|WP_Error The registered comment type object on success, * WP_Error object on failure. @@ -569,6 +577,53 @@ function get_comment_type_labels( $comment_type_object ) { return $labels; } +/** + * Builds an object with all comment type capabilities out of a comment type object. + * + * Comment type capabilities use the `capability_type` argument as a base, if + * the capability is not set in the `capabilities` argument. + * + * This is advisory metadata describing the capabilities associated with a comment + * type, modeled on {@see get_post_type_capabilities()}. The capability mapping in + * {@see map_meta_cap()} is not affected by these capabilities in this release. + * + * The capability strings are built from the `capability_type` argument, which may + * be a string or an array. When a string, the plural is created by appending an + * 's'. When an array, the first element is the singular base and the second the + * plural base, e.g. array( 'story', 'stories' ). + * + * @since 7.1.0 + * + * @param object $args Comment type registration arguments. Expects the + * `capability_type` and `capabilities` properties. + * @return object Object with all the capabilities as member variables. + */ +function get_comment_type_capabilities( $args ) { + 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, + 'read_comment' => 'read_' . $singular_base, + 'delete_comment' => 'delete_' . $singular_base, + 'moderate_comment' => 'moderate_' . $singular_base, + // Primitive capabilities used outside of map_meta_cap(). + '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. * From 4eab01b8c4a120232f8fc06436c78ef715189676 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 15:28:25 -0700 Subject: [PATCH 2/3] Comments: Add tests for comment type capabilities. Cover the new `capability_type`/`capabilities` arguments and the `get_comment_type_capabilities()` helper: default and custom capability types, array capability types with explicit plurals, the `capabilities` override, that the input `capabilities` array is not retained as a property, and that the built-in `comment` type stays backward compatible with the existing core capabilities. See #35214. --- tests/phpunit/tests/comment/types.php | 62 +++++++++++++++ tests/phpunit/tests/comment/wpCommentType.php | 77 +++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index 54f8e2aa9ad33..dd7fa6aaa4c7b 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -291,4 +291,66 @@ static function ( $labels ) { $this->assertSame( 'Filtered Foo', get_comment_type_object( 'foo' )->labels->singular_name ); } + + /** + * @ticket 35214 + */ + public function test_registered_comment_type_exposes_cap_object() { + 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 + */ + public function test_built_in_comment_type_capabilities_are_backward_compatible() { + $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() { + $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() { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'comment', + 'capabilities' => array( + 'edit_comments' => 'manage_stuff', + ), + ) + ); + + $this->assertSame( 'manage_stuff', $caps->edit_comments ); + } } diff --git a/tests/phpunit/tests/comment/wpCommentType.php b/tests/phpunit/tests/comment/wpCommentType.php index bf6d3c7df28dd..8429c69f88b15 100644 --- a/tests/phpunit/tests/comment/wpCommentType.php +++ b/tests/phpunit/tests/comment/wpCommentType.php @@ -117,4 +117,81 @@ public function test_reset_default_labels_clears_cache() { $labels = WP_Comment_Type::get_default_labels(); $this->assertSame( 'Comments', $labels['name'][0] ); } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_default_capability_type_and_cap_object() { + $comment_type = new WP_Comment_Type( 'foo' ); + + $this->assertSame( 'comment', $comment_type->capability_type ); + $this->assertIsObject( $comment_type->cap ); + $this->assertSame( 'edit_comment', $comment_type->cap->edit_comment ); + $this->assertSame( 'moderate_comments', $comment_type->cap->moderate_comments ); + } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_custom_capability_type_builds_cap_object() { + $comment_type = new WP_Comment_Type( 'foo', array( 'capability_type' => 'review' ) ); + + $this->assertSame( 'review', $comment_type->capability_type ); + $this->assertSame( 'edit_review', $comment_type->cap->edit_comment ); + $this->assertSame( 'edit_reviews', $comment_type->cap->edit_comments ); + $this->assertSame( 'moderate_reviews', $comment_type->cap->moderate_comments ); + } + + /** + * An array capability type allows an explicit plural and is collapsed to its singular base. + * + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_array_capability_type_uses_explicit_plural() { + $comment_type = new WP_Comment_Type( 'foo', array( 'capability_type' => array( 'story', 'stories' ) ) ); + + $this->assertSame( 'story', $comment_type->capability_type ); + $this->assertSame( 'edit_story', $comment_type->cap->edit_comment ); + $this->assertSame( 'edit_stories', $comment_type->cap->edit_comments ); + } + + /** + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_capabilities_argument_overrides_generated_caps() { + $comment_type = new WP_Comment_Type( + 'foo', + array( + 'capability_type' => 'review', + 'capabilities' => array( + 'moderate_comments' => 'manage_reviews', + ), + ) + ); + + $this->assertSame( 'manage_reviews', $comment_type->cap->moderate_comments ); + // Non-overridden caps are still generated from the capability type. + $this->assertSame( 'edit_reviews', $comment_type->cap->edit_comments ); + } + + /** + * The input `capabilities` array is consumed and not kept as a public property. + * + * @ticket 35214 + * + * @covers ::set_props + */ + public function test_capabilities_input_is_not_retained_as_property() { + $comment_type = new WP_Comment_Type( 'foo', array( 'capabilities' => array( 'edit_comments' => 'x' ) ) ); + + $this->assertObjectNotHasProperty( 'capabilities', $comment_type ); + } } From c4978f39a252d2d778b2abf9551fa5ba083993eb Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 20:27:17 -0700 Subject: [PATCH 3/3] Comments: Assert the full comment type capability set. The existing tests spot-checked individual generated capabilities; read_comment, moderate_comment, and delete_comments were never asserted. Add a test pinning the complete meta and primitive capability set built by get_comment_type_capabilities() from a string base, plus a direct test of the array capability_type form (explicit plural). See #35214. --- tests/phpunit/tests/comment/types.php | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index dd7fa6aaa4c7b..d169912bcdf03 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -353,4 +353,59 @@ public function test_get_comment_type_capabilities_honors_capabilities_override( $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() { + $caps = get_comment_type_capabilities( + (object) array( + 'capability_type' => 'review', + 'capabilities' => array(), + ) + ); + + $expected = array( + // Meta capabilities. + 'edit_comment' => 'edit_review', + 'read_comment' => 'read_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() { + $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( 'read_story', $caps->read_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 ); + } }