Commit 28da1c2042 for wordpress.org
commit 28da1c2042173e65b459021be5dcb2da64cbcaec
Author: Mamaduka <mamaduka@git.wordpress.org>
Date: Tue Nov 3 07:36:00 2026 +0000
REST API: Only let a reaction's author delete it.
Since r65038, deleting a note reaction goes through the same `edit_comment` check as its note, so anyone who can edit the note's post, including the post author and administrators, could remove someone else's reaction. A reaction speaks for its author, so `WP_REST_Comments_Controller::delete_item_permissions_check()` now only allows its author to delete it.
Reactions on a resolved thread also can no longer be deleted, matching the existing check in `create_item()`. The thread-root walk moves into a private `is_thread_resolved()` helper shared by both.
The reaction type is matched case-insensitively, like in `check_edit_permission()`.
Developed in: https://github.com/WordPress/wordpress-develop/pull/15383
Developed in: https://github.com/WordPress/gutenberg/pull/85415
Follow-up to r64488, r65038.
Props adamsilverstein, ramonopoly, mamaduka.
Fixes #66447.
Built from https://develop.svn.wordpress.org/trunk@65222
git-svn-id: http://core.svn.wordpress.org/trunk@64371 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
index 1d035d3a6e..f5739e6b3a 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
@@ -787,21 +787,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
* reactions from then on. Hold requests from stale editor sessions
* to that too, for the root note and for every reply in its thread.
*/
- $thread_root = $parent_comment;
- $visited = array( (int) $thread_root->comment_ID => true );
- while ( $thread_root->comment_parent ) {
- $ancestor = get_comment( $thread_root->comment_parent );
-
- // Stop at a missing ancestor or a corrupt, cyclic chain.
- if ( ! $ancestor || isset( $visited[ (int) $ancestor->comment_ID ] ) ) {
- break;
- }
-
- $visited[ (int) $ancestor->comment_ID ] = true;
- $thread_root = $ancestor;
- }
-
- if ( '1' === $thread_root->comment_approved ) {
+ if ( $this->is_thread_resolved( $parent_comment ) ) {
return new WP_Error(
'rest_comment_invalid_parent',
__( 'A reaction cannot be added to a resolved note.' ),
@@ -1350,6 +1336,28 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
array( 'status' => rest_authorization_required_code() )
);
}
+
+ if ( 'reaction' === strtolower( $comment->comment_type ) ) {
+ // A reaction speaks for its author, so only they can remove it.
+ if ( get_current_user_id() !== (int) $comment->user_id ) {
+ return new WP_Error(
+ 'rest_cannot_delete',
+ __( 'Sorry, you can only remove your own reactions.' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ // Reactions on a resolved thread are frozen, like adding new ones.
+ $note = get_comment( $comment->comment_parent );
+ if ( $note instanceof WP_Comment && $this->is_thread_resolved( $note ) ) {
+ return new WP_Error(
+ 'rest_cannot_delete',
+ __( 'A reaction cannot be removed from a resolved note.' ),
+ array( 'status' => 403 )
+ );
+ }
+ }
+
return true;
}
@@ -2652,6 +2660,34 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
return true;
}
+ /**
+ * Checks whether a note belongs to a resolved thread.
+ *
+ * Resolving a thread approves its root note.
+ *
+ * @since 7.2.0
+ *
+ * @param WP_Comment $note A root note or a reply.
+ * @return bool True if the note's thread is resolved.
+ */
+ private function is_thread_resolved( WP_Comment $note ): bool {
+ $thread_root = $note;
+ $visited = array( (int) $thread_root->comment_ID => true );
+ while ( $thread_root->comment_parent ) {
+ $ancestor = get_comment( $thread_root->comment_parent );
+
+ // Stop at a missing ancestor or a corrupt, cyclic chain.
+ if ( ! $ancestor || isset( $visited[ (int) $ancestor->comment_ID ] ) ) {
+ break;
+ }
+
+ $visited[ (int) $ancestor->comment_ID ] = true;
+ $thread_root = $ancestor;
+ }
+
+ return '1' === $thread_root->comment_approved;
+ }
+
/**
* Check if post type supports notes.
*
diff --git a/wp-includes/version.php b/wp-includes/version.php
index c0859a7834..0b829eb59f 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta3-65221';
+$wp_version = '7.2-beta3-65222';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.