Skip to content

Comments: Add a capabilities object to comment types - #52

Open
adamsilverstein wants to merge 17 commits into
feature/register-comment-typefrom
feature/comment-type-caps
Open

adamsilverstein wants to merge 17 commits into
feature/register-comment-typefrom
feature/comment-type-caps

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 24, 2026 •

Copy link
Copy Markdown
Owner

Stack: Custom Comment Types (#35214)

PR Change Builds on
#12311 register_comment_type() registration API trunk
#51 REST /wp/v2/comment-types endpoint WordPress#12311
👉 #52 Capabilities object (advisory) (this PR) WordPress#12311
#55 Enforce type capabilities in map_meta_cap() #52
#56 Type-aware REST moderation #55
#57 Type-aware admin comment dimming #56
#53 render_callback display callback WordPress#12311
#54 is_ping ping grouping #53
#12310 default_excluded_comment_types filter trunk
#58 wp_update_comment_counts() recount helper WordPress#12310

WordPress#12311 and WordPress#12310 are on WordPress/wordpress-develop; the rest are fork PRs targeting their parent branch and will retarget to trunk as their base lands.

Description refreshed by Claude after another review:

What

Adds a capabilities object to comment types, modeled on WP_Post_Type and get_post_type_capabilities(). A registered comment type can describe its own edit, delete, and moderate capabilities through $comment_type_object->cap.

register_comment_type( 'review', array( 'capability_type' => 'review' ) );

$obj = get_comment_type_object( 'review' );
$obj->cap->edit_comment;      // 'edit_review' (meta)
$obj->cap->edit_comments;     // 'edit_reviews'
$obj->cap->moderate_comments; // 'moderate_reviews'

Why

Per-type permissions are one of the main asks on https://core.trac.wordpress.org/ticket/35214. Before core can enforce anything, a type needs a way to declare which capabilities belong to it, and the REST endpoint needs something to expose. This PR adds only that model.

On its own this PR changes no behavior. map_meta_cap() is untouched, so the capabilities are advisory. Anyone with moderate_comments can still edit, approve, spam, or delete comments of every type. The enforcement lives in the next PR (#55, #55), and the two should land in the same release: once #55 maps them, current_user_can( 'edit_review', $id ) on a custom base changes from a literal capability check to a mapped one. Shipping a release in between would leave that meaning ambiguous.

How

  • New capability_type registration argument (default 'comment'). An array like array( 'story', 'stories' ) supplies an explicit plural.
  • New capabilities registration argument to override individual generated names, meta or primitive.
  • New get_comment_type_capabilities() in src/wp-includes/comment.php builds the set: meta caps edit_comment, delete_comment, moderate_comment from the singular base, and primitives edit_comments, edit_others_comments, delete_comments, moderate_comments from the plural.
  • WP_Comment_Type::set_props() stores the result in $cap, collapses an array capability_type back to its singular base, and drops the capabilities input.
  • The built-in types register without a capability_type, so they all resolve to core's existing edit_comment and moderate_comments.

A few deliberate choices:

  • No read_comment. Core has no semantics for reading a single comment and nothing would map it, so it is left out (and a test pins that) until there is.
  • The "others" axis is edit-only for now: edit_others_comments without a delete_others_comments counterpart. One can be added compatibly once enforcement settles which primitives the moderation flows actually check.
  • The docs warn against reusing a post type base. A base of 'post' generates edit_comment => 'edit_post', which sends a comment ID through the post branch of map_meta_cap().

Testing

  1. Run the comment and capability suites:
    • vendor/bin/phpunit --group comment
    • vendor/bin/phpunit --group capabilities
  2. In a mu-plugin, register a type with a custom base and inspect it:
    add_action( 'init', function () {
        register_comment_type( 'review', array( 'capability_type' => array( 'review', 'reviews' ) ) );
    } );
    Then wp eval 'print_r( get_comment_type_object( "review" )->cap );' should list the *_review / *_reviews names.
  3. Confirm nothing changed for existing comments: as an editor, moderate and edit regular comments in wp-admin as before.

Out of scope / follow-ups

See #35214.

Trac ticket: https://core.trac.wordpress.org/ticket/35214

AI Use

Claude Code did the typing here, I did the asking. I will review and test.

@github-actions

github-actions Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6a2c5333-5f64-40b7-8790-39b591d7416c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
Finishing Touches

🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Commit unit tests in branch feature/comment-type-caps
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@adamsilverstein

Copy link
Copy Markdown
Owner Author

Follow-up: capability enforcement via map_meta_cap()

This PR intentionally keeps the cap object advisory. Proposing the lowest-risk path to actual enforcement, mirroring register_post_type():

  • Opt-in. Default capability_type => 'comment' stays byte-for-byte identical (editing still derives from the parent post's edit_post; moderation still uses the moderate_comments primitive). Only types declaring a non-default capability_type / explicit capabilities route through the new handling.
  • Map, don't grant. New per-type meta caps (read_comment / edit_comment / delete_comment / moderate_comment) map to the type's primitives via map_meta_cap(); sites grant the primitives to roles, exactly as with custom post types. No role (admin included) is silently locked out because the default path is unchanged.
  • Stage the call-site rerouting (edit → moderation → delete) instead of one audit-heavy change, so each surface gets a focused security review.

One question to settle before coding: do custom comment types keep deriving edit permission from the parent post, or get a permission boundary independent of it? That is the substantive design fork (the comment:19 concern). Happy to write this up on Trac #35214 if the direction looks right.

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.
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.
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.
@adamsilverstein
adamsilverstein force-pushed the feature/comment-type-caps branch from d2ee949 to c4978f3 Compare June 25, 2026 06:32
…e-caps

# Conflicts:
#	src/wp-includes/class-wp-comment-type.php
#	src/wp-includes/comment.php
#	tests/phpunit/tests/comment/types.php
- 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.
…e-caps

# Conflicts:
#	src/wp-includes/class-wp-comment-type.php
#	src/wp-includes/comment.php
#	tests/phpunit/tests/comment/types.php
`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`.
…t 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.
…y 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.
…e-caps

# Conflicts:
#	src/wp-includes/comment.php
The branch was written while trunk was 7.1-alpha; trunk is now
7.2-alpha, so the new capability_type and cap properties and
get_comment_type_capabilities() carried @SInCE tags for a release
that will never contain them, contradicting the 7.2.0 stamps on the
rest of the class.

Also close three documentation gaps the stamps review surfaced: the
read_comment omission rationale claimed map_meta_cap() coverage as
the bar while delete_comment and moderate_comment are advertised
without cases, so it now rests on read having no per-comment
semantics yet; the edit-only "others" axis is called out as
deliberate so the missing delete_others_comments key reads as a
decision rather than an oversight; and the three cap tests without
@Covers annotations gain them.

See #35214.
The docblocks for get_comment_type_capabilities(), the register_comment_type()
arguments, and the WP_Comment_Type capability properties repeated the
"advisory" caveat several times and carried design rationale (the
edit-only "others" axis, which primitives default roles grant) that
belongs in the ticket rather than the file. Keep what a caller needs:
how the base builds the names, that they are not enforced yet, the
post base collision, and the argument mutation.

The "used outside of map_meta_cap()" inline comment is dropped, as
nothing consults those primitives yet.
…tests.

get_comment_type_capabilities() is new in 7.2.0, so it can declare its
object argument and stdClass return natively without a back-compat cost;
its only caller, WP_Comment_Type::set_props(), already passes an object.
The new tests get `: void` and a typed data provider parameter to match
the rest of the file.
…e-caps

# Conflicts:
#	src/wp-includes/class-wp-comment-type.php
Trunk is in the 7.2 beta, so these additions can no longer ship in 7.2.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant