Comments: Enforce registered comment type capabilities in map_meta_cap() (Trac #35214)#55
Conversation
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.
|
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 Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
d2ee949 to
c4978f3
Compare
…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.
Summary
Follow-up to PR #52 (the advisory cap object), implementing Stages 1-2 of the enforcement plan: wiring
WP_Comment_Type::$capintomap_meta_cap(). Per discussion this supports both models.Before this, the cap object was advisory:
map_meta_cap()only knewedit_comment(derived from the parent post) and all moderation was a baremoderate_commentsprimitive check at each call site.What changed (
map_meta_cap()only - no call-site reroutes yet)edit_commentgeneralized. A comment whose type uses the defaultcommentcapability model (its mappededit_commentis the generic meta cap) still derives edit permission from the parent post - byte-for-byte unchanged, orphaned-comment fallback included. A type that opts into its own caps maps toedit_comments(own) /edit_others_comments(others) byuser_id, mirroringedit_post.delete_commentadded. Default model follows the parent post (matching how core's delete flows gate onedit_commenttoday); independent model maps todelete_comments.moderate_commentadded. Default model requires the globalmoderate_commentsprimitive; independent model maps to the type'smoderate_comments(e.g.moderate_reviews).Design: supports both, maps don't grant
capability_type => 'comment'is unchanged. Built-incomment/pingback/trackback/noteall resolve exactly as today, so no role (admin included) is affected.cap->edit_comment === 'edit_comment'), which also avoids any self-recursion.Deferred
read_comment- the Comments: Add a capabilities object to comment types #52 cap object has no read primitive, and approved comments are publicly readable, so gating reads needs its own design.wp_set_comment_status(), admincomment.php+ list-table, REST permission callbacks, AJAX handlers - lands as small per-surface PRs so each gets a focused security review. This PR is intentionallymap_meta_cap()-only.Testing
New
tests/phpunit/tests/comment/commentCapabilities.php:moderate_comments.reviewtype: amoderate_reviewsuser can moderate reviews but not default comments, and an administrator (hasmoderate_comments, notmoderate_reviews) is the exact inverse - proving independence and the no-auto-grant rule.Registered the two new meta caps in the core meta-cap coverage list (
tests/phpunit/tests/user/capabilities.php). Full--group comment --group capabilitiessuite passes (797 tests). PHPCS + PHPStan clean.Review updates
_post_type_meta_capabilities():current_user_can( 'edit_review', $comment_id )- the names the cap object itself advertises - now translates to the generic meta capability instead of silently falling through to a literal primitive check. Includes a collision guard against post type meta cap names;'read_comment'is deliberately excluded from translation until amap_meta_cap()case exists for it.unregister_comment_type()cleans up the registered translations.map_meta_cap()cases are consolidated into a fall-through implementation with a single documented model-detection point.capability_typefootguns.Ecosystem impact
A case-sensitive regex audit of ~70,000 wordpress.org plugins (70,029 of 70,039 indexed, plus all themes) via Veloria (formerly WPDirectory) found near-zero direct use of the singular
delete_comment/moderate_commentstrings as capabilities:current_user_can( 'delete_comment' ): 1 plugin, via a custom wrapper unrelated to core caps (search).current_user_can( 'moderate_comment' ): 0 (search).user_can()with either string: 0 (search).add_cap()/has_cap()with either string: 0 (search, search).add_action( 'delete_comment' )hook usages (Akismet, ManageWP, W3TC, Simple History, WP Activity Log, Stream, cache purgers), the rest method names, webhook slugs, logger labels, and nonce actions - all unaffected.Only two genuine capability uses exist:
'delete_comment'touser_can()with a comment ID, expecting core to map it - a check that silently always failed and now behaves as its author intended (the same switch already relies on thedelete_post/delete_usermeta caps).'moderate_comment'as an admin menu capability, a typo formoderate_comments. It stays denied (the no-ID branch maps todo_not_allow), now surfacing the_doing_it_wrong()notice.The compatibility risk profile is equivalent to - and on this evidence lower than - the precedent set when WordPress 6.1 introduced the
edit_commentmeta capability.Stacking
Based on
feature/comment-type-caps(#52). Retarget totrunkonce the registration (#12311) and cap-object PRs land.See #35214.