From cd33df87b23d0cc122c4865a57733e27b25609b4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 15:28:25 -0700 Subject: [PATCH 1/7] 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/7] 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/7] 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 ); + } } From 53ea2972d9febd39c5a9262b093573e062773826 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 11 Jul 2026 10:33:06 -0700 Subject: [PATCH 4/7] Docs: Clarify the comment type capabilities contract. - Document that with the default 'comment' capability_type, most generated primitive capabilities exist in no default role and are not consulted by the default mapping: consumers should check meta capabilities with a comment ID, not the primitives. - Add the per-capability @return reference to get_comment_type_capabilities() (mirroring the post type version) and note that it normalizes the passed object's capability_type as a side effect. - Type WP_Comment_Type::$capability_type as string: set_props() collapses the array registration form to the singular base. --- src/wp-includes/class-wp-comment-type.php | 7 ++--- src/wp-includes/comment.php | 33 ++++++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 9aace60f60a40..6bc4f6c606976 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -93,12 +93,13 @@ final class WP_Comment_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 + * May be registered 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'. + * array( 'story', 'stories' ). set_props() collapses the array form back to + * the singular base once the capabilities are built. Default 'comment'. * * @since 7.1.0 - * @var string|array + * @var string */ public $capability_type = 'comment'; diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 93df47e308c57..132e20e2585b6 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -638,11 +638,42 @@ function get_comment_type_labels( $comment_type_object ) { * 's'. When an array, the first element is the singular base and the second the * plural base, e.g. array( 'story', 'stories' ). * + * Note: With the default `capability_type` of 'comment', most of the generated + * primitive capabilities (`edit_comments`, `edit_others_comments`, + * `delete_comments`) exist in no default role and are not consulted by the + * default capability mapping; `moderate_comments` is the only generated + * primitive that default roles grant. Consumers should check the meta + * capabilities together with a comment ID instead of testing those primitives + * directly. + * + * Note: The `capability_type` property of the passed object is normalized to + * its array form as a side effect of calling this function, matching + * get_post_type_capabilities(). + * * @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. + * @return object { + * 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 $read_comment Meta capability to read a comment of this type. + * Default 'read_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( $args ) { if ( ! is_array( $args->capability_type ) ) { From 2bc1f6e6888275385fd0969c8af45d37f6dc197f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 17:06:38 -0700 Subject: [PATCH 5/7] Comments: Drop the unresolvable `read_comment` comment type capability. `get_comment_type_capabilities()` generated a `read_comment` meta capability alongside `edit_comment`, `delete_comment`, and `moderate_comment`, but `map_meta_cap()` has no case for it and none is planned with the enforcement follow-up. A consumer taking the documented advice - check the meta capabilities with a comment ID and let `map_meta_cap()` resolve them - would get a literal capability that no default role grants, so the check denies everyone including administrators, with no release in which that changes. Post types ship `read_post` from day one together with its mapping, so the parity argument does not carry to shipping one unmapped. Drop it from the generated set rather than document a capability that cannot work, and add a test pinning the omission so it is only reintroduced with its mapping. No behavior change for the built-in types: nothing in core reads `read_comment`. --- src/wp-includes/class-wp-comment-type.php | 2 +- src/wp-includes/comment.php | 5 +---- tests/phpunit/tests/comment/types.php | 24 +++++++++++++++++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index fb4edb444dc57..a4f04ddd554d7 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -90,7 +90,7 @@ final class WP_Comment_Type { public $internal = false; /** - * The string to use to build the read, edit, and delete capabilities. + * The string to use to build the edit, delete, and moderate capabilities. * * May be registered as an array to allow for alternative plurals when using * this argument as a base to construct the capabilities, e.g. diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 192d6c0d601a8..6e08385d3205a 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -451,7 +451,7 @@ function create_initial_comment_types() { * counts by default, through the * {@see 'default_excluded_comment_types'} filter. * Default false. - * @type string|array $capability_type The string to use to build the read, edit, and delete + * @type string|array $capability_type The string to use to build the edit, delete, and moderate * 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' ). @@ -754,8 +754,6 @@ function get_comment_type_labels( $comment_type_object ) { * * @type string $edit_comment Meta capability to edit a comment of this type. * Default 'edit_comment'. - * @type string $read_comment Meta capability to read a comment of this type. - * Default 'read_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. @@ -781,7 +779,6 @@ function get_comment_type_capabilities( $args ) { $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(). diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index 68de7ddf70685..6dc53f7ed0ee7 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -603,7 +603,6 @@ public function test_get_comment_type_capabilities_generates_full_set() { $expected = array( // Meta capabilities. 'edit_comment' => 'edit_review', - 'read_comment' => 'read_review', 'delete_comment' => 'delete_review', 'moderate_comment' => 'moderate_review', // Primitive capabilities. @@ -633,13 +632,34 @@ public function test_get_comment_type_capabilities_from_array() { // Singular base drives the meta capabilities. $this->assertSame( 'edit_story', $caps->edit_comment ); - $this->assertSame( 'read_story', $caps->read_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 ); } + /** + * A 'read_comment' meta capability is deliberately not generated: map_meta_cap() has no + * case for it, so advertising it would hand consumers a capability that denies everyone. + * Pinned so it is only added alongside its mapping. + * + * @ticket 35214 + * + * @covers ::get_comment_type_capabilities + */ + public function test_get_comment_type_capabilities_omits_read_comment() { + $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 ); + } + /** * 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. From 55145f4594407181ec9de3f29aa064b975d37f73 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 17:06:57 -0700 Subject: [PATCH 6/7] Docs: Flag the comment type capabilities as advisory until enforcement lands. The "advisory metadata only, `map_meta_cap()` is not affected" warning lived on `WP_Comment_Type::$cap` and in `get_comment_type_capabilities()`, but not in the `register_comment_type()` argument docs, which is the surface a plugin author reads before passing `capability_type`. Someone registering a 'review' type and granting `edit_reviews` to a role could reasonably conclude that core's moderation and edit paths now require it for their type. They do not: anyone with `moderate_comments` can still edit, spam, approve, or delete those comments through every existing path. The risk is not the checks a plugin makes - those are fail-closed - it is the checks it skips believing core makes them, so the only fix available here is to say so where it will be read. Also qualify the "check the meta capabilities" advice, which only resolves for `edit_comment` on the default base today, and warn against reusing a post type's `capability_type`: a base of 'post' would send a comment ID through `map_meta_cap()`'s post branch. --- src/wp-includes/comment.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 6e08385d3205a..0b9282c919461 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -455,10 +455,14 @@ function create_initial_comment_types() { * 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' ). + * These capabilities are advisory metadata and are not + * enforced by core's capability mapping in this release. * Default 'comment'. * @type string[] $capabilities Array of capabilities for this comment type. * $capability_type is used as a base to construct - * capabilities by default. + * capabilities by default. As with $capability_type, these + * are advisory metadata and are not enforced by core's + * capability mapping in this release. * 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 @@ -741,6 +745,17 @@ function get_comment_type_labels( $comment_type_object ) { * capabilities together with a comment ID instead of testing those primitives * directly. * + * Note: In this release, `edit_comment` on the default 'comment' base is the + * only generated capability that map_meta_cap() resolves. The remaining meta + * capabilities, and every capability generated from a custom base, are treated + * as primitives: a check requires the literal capability, which no default role + * grants, so it denies everyone until comment type capability mapping is added. + * + * Note: Do not reuse a post type's `capability_type` as a comment type base. + * A base of 'post' generates `edit_comment => 'edit_post'`, which map_meta_cap() + * resolves through its post branch, so the check would be answered by a post + * with the passed comment's ID. + * * Note: The `capability_type` property of the passed object is normalized to * its array form as a side effect of calling this function, matching * get_post_type_capabilities(). From 787a835e6a09a7488cf0ef66bd589d3b4cb91eaf Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 17:07:18 -0700 Subject: [PATCH 7/7] Comments: Cover the shared built-in capability set and meta capability overrides. The backward-compatibility claim for `pingback`, `trackback`, and `note` rested on all three registering without a `capability_type`, which nothing asserted: only the `comment` type's cap object was checked. Compare each built-in against it directly, so a future registration that quietly gives one of them its own base is caught. Also cover a `capabilities` override of a meta capability. Only primitive overrides were tested, and a type pointing `edit_comment` at a name `map_meta_cap()` already resolves is the one route to working per-type checks before enforcement lands. --- tests/phpunit/tests/comment/types.php | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index 6dc53f7ed0ee7..895b6ad2b88fc 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -660,6 +660,45 @@ public function test_get_comment_type_capabilities_omits_read_comment() { $this->assertObjectNotHasProperty( 'read_comment', get_comment_type_object( 'comment' )->cap ); } + /** + * The built-in types all register without a capability type, so they share one cap set. + * Anything that gated on the existing comment capabilities keeps working for all four. + * + * @ticket 35214 + * + * @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( $comment_type ) { + $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() { + $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.