Skip to content

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

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

Comments: Add a capabilities object to comment types#52
adamsilverstein wants to merge 9 commits into
feature/register-comment-typefrom
feature/comment-type-caps

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Description

Adds a capabilities object to comment types, modeled on WP_Post_Type and get_post_type_capabilities(). Registered comment types can now describe their own edit/delete/moderate capabilities, which is a prerequisite for per-type permission handling on the #35214 tracking ticket (and for exposing capabilities through the REST endpoint).

What it adds

  • A capability_type registration argument (default 'comment'; may be an array like array( 'story', 'stories' ) for an explicit plural).
  • A capabilities registration argument to override individual generated capabilities.
  • A WP_Comment_Type::$cap object, built by the new get_comment_type_capabilities() helper.
register_comment_type( 'review', array( 'capability_type' => 'review' ) );

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

Non-breaking by design

Within this PR the capabilities are advisory metadata only: map_meta_cap() is left unchanged here, so this PR on its own causes no behavior change to comment moderation or editing. The built-in comment type resolves to the existing edit_comment (meta) and moderate_comments (primitive) capabilities, so current behavior is preserved exactly.

Wiring these capabilities into map_meta_cap() enforcement was deliberately split out so the capability model could be reviewed before touching the live permission path. That follow-up now exists: the next PR in the stack (#55) enforces these capabilities in map_meta_cap() and rewrites the capability docblocks to describe the live model.

These two need to land in the same release. Once #55's mapping exists, a check changes meaning for anything registered under a #52-only release: with a custom base, current_user_can( 'edit_review', $id ) flips from requiring the literal edit_review to mapping onto edit_reviews / edit_others_reviews. Both directions are defensible, but only if no released version sits in between. Committers, please take these as a pair.

Scope / boundary

Stacked on WordPress#12311 (the register_comment_type() API) and targets that branch; it will be retargeted to trunk once WordPress#12311 lands. Complements the REST endpoint (#51, also stacked on WordPress#12311) and the default_excluded_comment_types query filter (WordPress#12310 / #65537).

Review updates

Following a review pass over the stack:

  • Dropped the read_comment meta capability. map_meta_cap() has no case for it and none is planned in Comments: Enforce registered comment type capabilities in map_meta_cap() (Trac #35214) #55, so a consumer following the documented advice would get a capability that denies everyone 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. A test pins the omission so it is only reintroduced alongside a mapping.
  • The advisory warning moved to where it gets read. It lived on WP_Comment_Type::$cap and in get_comment_type_capabilities(), but not in the register_comment_type() argument docs. Someone registering a 'review' type and granting edit_reviews to a role could reasonably conclude core's moderation paths now require it. They do not - anyone with moderate_comments can still edit, spam, approve, or delete those comments. The exposure is not the checks a plugin makes, which are fail-closed, it is the checks it skips believing core makes them.
  • Qualified the meta capability guidance. Only edit_comment on the default base resolves through map_meta_cap() today; everything else is a literal capability check that no default role satisfies.
  • Added a caution against reusing a post type's capability_type. A base of 'post' generates edit_comment => 'edit_post', which sends a comment ID through map_meta_cap()'s post branch.
  • New tests: pingback, trackback, and note are asserted to share the comment cap set (the backward-compatibility claim for three of the four built-ins was untested), and a capabilities override of a meta capability is covered.

Testing

$ phpunit --group comment
OK (706 tests, 1715 assertions)

Capability/meta-cap suites also pass unchanged, confirming no enforcement-path regression. PHPCS reports no new warnings on the changed files and PHPStan is clean.

See #35214.

AI Use

Code and description both written with 🤖 Claude Code. I will review and test.

@github-actions

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. You can also manually ask me to refresh this list by adding the props-bot label.

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.

@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

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48e42292-d8bd-4fcc-ad18-62bbff5acc74

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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.
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