Skip to content

Comments: Enforce registered comment type capabilities in map_meta_cap() (Trac #35214) - #55

Open
adamsilverstein wants to merge 14 commits into
feature/comment-type-capsfrom
feature/comment-type-cap-enforcement
Open

adamsilverstein wants to merge 14 commits into
feature/comment-type-capsfrom
feature/comment-type-cap-enforcement

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jun 25, 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) WordPress#12311
👉 #55 Enforce type capabilities in map_meta_cap() (this PR) #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.

Claude dug through the cap mapping again and wrote this up:

What

This wires the comment type capabilities object from #52 into map_meta_cap(). Registered comment types can now opt into their own capabilities, the same way custom post types do, while every type that keeps the default comment model behaves exactly as it does today.

Why

Part of custom comment types, see https://core.trac.wordpress.org/ticket/35214

Right now WP_Comment_Type::$cap is advisory only. map_meta_cap() only knows edit_comment (from the parent post), and moderation is a bare moderate_comments check. So a plugin can register a review type with capability_type => 'review', but nothing actually enforces edit_reviews or moderate_reviews.

How

  • map_meta_cap() in src/wp-includes/capabilities.php handles edit_comment, plus new delete_comment and moderate_comment meta caps, all against a specific comment ID.
  • Default model (the type's mapped meta cap is still the generic name, or the type isn't registered): edit and delete follow the parent post, orphaned comments fall back to edit_posts, moderation needs moderate_comments. Built-in comment, pingback, trackback and note all stay on this model.
  • Independent model (the type overrides a meta cap): edit maps to edit_comments / edit_others_comments by comment author, delete to delete_comments, moderation to moderate_comments, all using the type's own names (eg. edit_reviews). The parent post isn't consulted. Nothing is granted automatically - the plugin adds the primitives to roles, like custom post types.
  • Custom meta cap names translate too: _comment_type_meta_capabilities() in src/wp-includes/comment.php records them, so current_user_can( 'edit_review', $comment_id ) maps to edit_comment, mirroring _post_type_meta_capabilities(). Names already used by a post type are rejected with _doing_it_wrong(), and unregister_comment_type() removes the translations.
  • No call sites change here. wp_set_comment_status(), the admin screens, REST and AJAX get rerouted in follow-up PRs so each gets its own security review.

Ecosystem check

A search of ~70,000 wordpress.org plugins via https://veloria.dev found almost no use of delete_comment / moderate_comment as capabilities. Most matches are the delete_comment action hook, which isn't affected. The two real uses:

  • Sugar Calendar Lite passes 'delete_comment' to user_can() with a comment ID. That check always failed before; now it works as the author expected.
  • Custom Review (<10 installs) uses 'moderate_comment' as a menu capability, a typo for moderate_comments. It stays denied, now with a _doing_it_wrong() notice.

This looks like the same or lower risk than when 6.1 added the edit_comment meta capability.

Testing

  • vendor/bin/phpunit --group capabilities --filter Tests_Comment_CommentCapabilities covers default-model parity, the independent review type (a moderate_reviews user can moderate reviews but not comments, and an admin is the reverse), own vs. others editing, custom meta cap translation and cleanup, the post type name collision, and non-string cap names.
  • vendor/bin/phpunit --group capabilities includes the meta cap coverage check, which now lists delete_comment and moderate_comment.
  • vendor/bin/phpunit --group comment for regressions.

To try it by hand, register a type with register_comment_type( 'review', array( 'capability_type' => 'review' ) ), add a review comment, and check current_user_can( 'moderate_comment', $id ) as an admin (false) and as a user with moderate_reviews (true).

Out of scope / follow-ups

See #35214.

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

AI Use

Claude wrote this code and description with some careful prompting. I will review and test.

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.
…ap()`.

The capabilities object added for comment types was advisory only;
`map_meta_cap()` knew a single comment cap (`edit_comment`, derived from
the parent post) and all moderation ran through a bare `moderate_comments`
primitive check.

Wire the cap object into `map_meta_cap()` so a registered type can enforce
its own permissions, supporting both models:

- Types using the default `comment` capability model are unchanged. Their
  mapped `edit_comment` is the generic meta cap, so editing and deletion
  still derive from the comment's parent post and moderation still requires
  the global `moderate_comments` primitive.
- A type that opts into its own `capability_type`/`capabilities` is gated by
  its own primitives (e.g. `moderate_reviews`, `edit_others_reviews`),
  mirroring how registered post types map `edit_post`. Caps are mapped, not
  granted: sites assign the primitives to roles, so nothing is silently
  escalated or locked out.

Generalize the `edit_comment` case and add `delete_comment` and
`moderate_comment` meta caps. `read_comment` is deferred: the cap object has
no read primitive and approved comments are publicly readable, so gating
reads needs its own design.

See #35214.
Cover both capability models in `map_meta_cap()`: the default `comment`
model still derives edit/delete from the parent post (including the orphaned
comment fallback) and moderation from `moderate_comments`, while a type with
an independent `capability_type` is gated by its own primitives with no
fallback to the default caps. Register `delete_comment` and `moderate_comment`
in the meta-capability coverage list.

See #35214.
@github-actions

github-actions Bot commented Jun 25, 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 25, 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: 00ae1d86-665e-43e2-a1bc-dfb439e98fd1

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-cap-enforcement
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

…ature/comment-type-cap-enforcement

# Conflicts:
#	tests/phpunit/tests/comment/types.php
current_user_can( 'edit_review', $comment_id ) - the idiom developers
know from custom post types, and the exact names the capability object
advertises - previously fell through map_meta_cap()'s default case to a
literal primitive check that ignored the comment ID and the own/others
split. Mirror _post_type_meta_capabilities(): remember each type's
custom edit_comment/delete_comment/moderate_comment names and translate
them in the default case. read_comment is deliberately excluded until a
corresponding case exists.

- Guard against claiming names already registered as post type meta
  capabilities (those take precedence); reject with _doing_it_wrong().
- Remove a type's translations in unregister_comment_type(), mirroring
  the post type cleanup, and reset the registry between tests.
- Consolidate the three map_meta_cap() comment cases into one
  fall-through block with a single, documented model-detection point
  (overriding a singular meta capability opts that action into the
  independent model), and note that the independent model deliberately
  ignores the parent post.
- Rewrite the 'advisory only' capability docblocks from the caps PR to
  describe the live model, including the mixed-model and empty
  capability_type footguns, and add map_meta_cap()'s @SInCE entry.
- Tests: legacy empty-string types across all three meta caps, orphaned
  independent comments, invalid and missing IDs, anonymous own/others,
  array capability_type end to end, single-override model flips,
  register/unregister fallback, trashed parents, custom-name
  translation, and the post type collision guard.
…p-enforcement

# Conflicts:
#	tests/phpunit/includes/abstract-testcase.php
…p-enforcement

# Conflicts:
#	tests/phpunit/includes/abstract-testcase.php
The registration API ships in 7.2.0, so the new meta capabilities and
helper carried the wrong @SInCE and _doing_it_wrong() versions. The
inline comments and property docs also restated the capability model
at length; they now say what a reader needs and leave the rationale
to the ticket.
Matches the typing used across the rest of the comment type tests and
keeps the file clean under PHPStan level 10.
A plugin passing a non-string value in the 'capabilities' argument made
_comment_type_meta_capabilities() use it as an array key, which throws
a TypeError on PHP 8. Only string names are stored now, and only the
three translatable meta capabilities are read, so the helper takes the
merged array directly instead of a cast object.

Also corrects the @SInCE and _doing_it_wrong() versions to 7.2.0 and
drops the docblock note that said map_meta_cap() resolves only
edit_comment, which this branch makes untrue.
…p-enforcement

# Conflicts:
#	src/wp-includes/class-wp-comment-type.php
#	src/wp-includes/comment.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