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.