Skip to content

Comments: Make REST comment moderation comment-type aware (Trac #35214, Stage 3) - #56

Open
adamsilverstein wants to merge 10 commits into
feature/comment-type-cap-enforcementfrom
feature/comment-type-moderation-rest
Open

adamsilverstein wants to merge 10 commits into
feature/comment-type-cap-enforcementfrom
feature/comment-type-moderation-rest

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() #52
👉 #56 Type-aware REST moderation (this PR) #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.

I pointed Claude at this PR again, and this came back:

What

Routes the REST comments controller's per-comment moderation checks through the moderate_comment meta capability from #55 instead of the global moderate_comments primitive. A comment type with its own capability model is then moderated by its own moderators over REST, and a global moderator no longer gets a pass on it.

Why

check_edit_permission() (the gate for update and delete) short-circuited on moderate_comments. For a type with independent capabilities, that meant a moderate_reviews user could not act on review comments, while any global moderator could act on every type. See https://core.trac.wordpress.org/ticket/35214

For the default capability model moderate_comment resolves to moderate_comments, so comment, pingback, trackback and unregistered types behave exactly as before. Notes and reactions still go through edit_comment, unchanged from trunk.

How

  • check_edit_permission() - the moderator shortcut uses current_user_can( 'moderate_comment', $comment_id ). Notes and reactions keep their edit_comment branch.
  • get_item_permissions_check() - edit-context reads of non-note, non-reaction comments use moderate_comment, so whoever can update or delete a comment can also read it with ?context=edit.
  • check_read_permission() - the orphaned-comment branch uses moderate_comment.
  • Moving a comment to another post still goes through check_target_post_permission() unchanged. Only the site-wide moderate_comments primitive skips the edit_post check on the destination, so a type moderator can't drop comments onto posts they can't edit, like someone else's private draft.
  • Collection-level and creation-time checks (?post=0, edit-context collections, setting author / author_ip / status on create) stay on moderate_comments, since there is no single comment to check against yet.

Known divergence, also pinned by a test: on the ?post=0 collection a global moderator gets the list with independent-type orphans filtered out, while a type moderator is rejected at the gate. A proper fix belongs with a future type parameter rework of the collection endpoint.

Testing

New tests/phpunit/tests/rest-api/rest-comment-type-moderation.php registers a review type with capability_type => review and dispatches real requests:

  • A moderate_reviews user and an edit_others_reviews user can update and delete review comments, and read them in edit context. A review moderator can move one only to a post they can edit.
  • A global moderate_comments user gets a 403 on review comments, and a review moderator gets a 403 on default comments.
  • Default, pingback, legacy empty-string, and unregistered (webmention) comments still work for a global moderator.
  • Notes and reactions: a bare global moderator can't read their own in edit context or delete them, while a user who can edit the post can.
  • Orphaned comments under both models, plus the ?post=0 divergence.

Run:

vendor/bin/phpunit --group restapi --filter Tests_REST_Comment_Type_Moderation
vendor/bin/phpunit --group comment
vendor/bin/phpunit --group capabilities

Out of scope / follow-ups

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

AI Use

Written with 🤖 Claude Code, code and description. I will review and test.

`WP_REST_Comments_Controller::check_edit_permission()` short-circuited on the
global `moderate_comments` primitive, so a moderator of a custom comment type
could not edit or delete comments of that type via REST, while a global
moderator could act on every type regardless of its capability model.

Route the moderator shortcut through the `moderate_comment` meta capability
instead. For comment types using the default capability model this resolves
to `moderate_comments` (behavior unchanged), while a type with its own
capabilities is gated by its own moderation primitive. Apply the same change
to the orphaned-comment branch of `check_read_permission()`.

This consumes the meta capabilities added in the previous commit; it makes no
change for the built-in comment, pingback, trackback, or note types.

See #35214.
Dispatch real update and delete requests through the comments controller to
prove `check_edit_permission()` honors per-type capabilities: a moderator (or
editor) of an independent `review` type can edit and delete review comments,
a global `moderate_comments` moderator cannot touch them, and the default and
built-in ping types behave exactly as before.

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: d623ea8f-b009-4c13-b6d0-b7a04f447571

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

…ment types.

get_item_permissions_check() still gated edit-context reads of non-note
comments on the global 'moderate_comments' primitive. After the update
and delete paths moved to the per-comment 'moderate_comment' meta
capability, a type moderator could update or delete a comment (with the
response already returning edit-context data) yet receive 403 reading
the same comment with ?context=edit. Route the non-note branch through
array( 'moderate_comment', $comment->comment_ID ) to match.

Also mark every comment gate that intentionally stays on the global
primitive (collection-level and creation-time gates, including the
documented ?post=0 divergence), add the missing @SInCE entries on
check_edit_permission() and check_read_permission(), and expand the
tests: the unregistered-type back-compat pin (a global moderator keeps
full control of never-registered types), legacy empty-string types,
single-item edit-context reads per model, orphaned-comment reads under
both models, and the ?post=0 collection matrix. Denials now assert
error codes via assertErrorResponse().
…ent-type-moderation-rest

# Conflicts:
#	src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
The moderation gates landed with `@since 7.1.0` tags, but this ships in
7.2.0, and the inline comments narrated design history that belongs in
the PR. Shortens them, corrects the version tags, documents the
edit-context change on get_item_permissions_check(), and restores the
unrelated note comment trunk wording. No behavior change.

See #35214.
Trunk added the `reaction` type, gated like `note` by `edit_comment`
rather than moderation. The merge kept that, but nothing failed if
reactions were routed through `moderate_comment`, which would let a bare
global moderator delete or read reactions in the edit context. Adds
note and reaction cases for both paths, and native types so the file
passes PHPStan level 10.

See #35214.
check_edit_permission() now admits a type's own moderators, but moving a
comment to another post still runs check_target_post_permission(),
where only the site-wide `moderate_comments` primitive skips the
`edit_post` check on the destination. That keeps a type-scoped
moderator from placing comments on posts they cannot edit, such as
another user's private post. Pins both sides of that rule plus the
unchanged global moderator behavior.

See #35214.
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