Make WordPress Core

Opened 8 days ago

Last modified 7 days ago

#66351 new defect (bug)

Notes: A reply or status change to a deleted note is saved and lost

Reported by: pranjalpratapsingh Owned by:
Priority: normal Milestone: Awaiting Review
Component: REST API Version: 6.9
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses:

Description

Originally reported in Gutenberg: ​https://github.com/WordPress/gutenberg/issues/80678 | Reported by @mcliwanow

Problem

Two editor sessions can disagree about whether a note still exists. When one session deletes a note while another still has it open, the second session can keep changing it, and the REST API accepts every change:

  • Replying to the deleted note succeeds and the editor announces "Reply added.", but the reply is stored under the trashed note and is no longer shown anywhere in the editor.
  • Reopen & Reply on a resolved note sets the note's status, which moves it out of the trash without the replies that were trashed along with it, so the thread comes back with only the new reply.
  • Resolving or editing a deleted note is accepted in the same way.

This is distinct from #64240, which trashes the replies that already exist when a top-level note is trashed. Here, the change arrives after the delete has completed.

WP_REST_Comments_Controller::create_item() already rejects a reaction whose parent note is trashed or spam ("A reaction cannot be added to a trashed or spam note."), but not a note reply. update_item() changes the status or content of a trashed note like any other comment.

Steps to reproduce

In the editor:

  1. Sign the same administrator into two browser sessions, A and B (for example a normal and a private window).
  2. In A, create a post, add a Paragraph, add a note to it, and save the draft.
  3. Open the same post in B and open the note.
  4. In A, select the note and type a reply, but don't submit it.
  5. In B, delete the note from its Actions menu and wait for "Note deleted.".
  6. In A, without reloading, click Reply.

Result: "Reply added." After a reload, the reply is not shown, but it is stored with the deleted note as its parent.

For Reopen & Reply, resolve the note in A before step 4. The button then reads "Reopen & Reply", and clicking it brings back the deleted note as open, while its earlier replies stay in the trash.

The same thing from the browser console, in a single session, on a saved draft in the editor:

( async () => {
        const post = wp.data.select( 'core/editor' ).getCurrentPostId();
        const api = ( path, method, data ) => wp.apiFetch( { path, method, data } );

        const note = await api( '/wp/v2/comments', 'POST', { post, content: 'Note', status: 'hold', type: 'note' } );
        await api( `/wp/v2/comments/${ note.id }`, 'DELETE' ); // Moves the note to the trash.

        // 1. A reply to the deleted note is accepted.
        const reply = await api( '/wp/v2/comments', 'POST', { post, content: 'Reply', status: 'hold', type: 'note', parent: note.id } );
        console.log( 'Reply stored under trashed note:', reply.id, 'parent', reply.parent );

        // 2. Reopening the deleted note moves it out of the trash.
        const reopened = await api( `/wp/v2/comments/${ note.id }`, 'POST', { status: 'hold' } );
        console.log( 'Deleted note is now:', reopened.status );
} )();

Expected

  • Creating a note reply whose parent is trashed, spam, or no longer exists returns an error (for example 400 rest_comment_invalid_parent), as it already does for reactions.
  • Any change to a trashed or spam note other than restoring it (status: untrash or unspam) returns an error, so the editor can tell the user instead of losing their change. Undo in the editor uses untrash, so it keeps working.

The editor already shows REST error messages in a snackbar and keeps the typed text when a save fails, so no client changes are needed.

Fix

A fix for the Gutenberg plugin's WordPress 7.2 compat controller, with tests, is in ​https://github.com/WordPress/gutenberg/pull/85202. A Core patch with the same changes and PHPUnit tests will follow on this ticket. Suggesting the 7.2 milestone, since 7.2 also adds emoji reactions to notes in the same controller.

Change History (1)

This ticket was mentioned in ​PR #14560 on ​WordPress/wordpress-develop by ​@pranjalpratapsingh.


7 days ago
#1

  • Keywords has-patch has-unit-tests added

Another editor session can delete a note while it is still open. The REST API then accepts changes to the deleted note that the editor can no longer show:

  • A reply submitted after the delete returns 201, and the editor announces "Reply added.", but the reply is stored under the trashed note and is not shown anywhere.
  • Reopen & Reply on a resolved note first sets the note's status, which moves it out of the trash without the replies trashed along with it, then adds the reply. The thread comes back with only the new reply.
  • Resolving or editing a deleted note is accepted the same way.

This PR changes WP_REST_Comments_Controller:

  • create_item() rejects a note reply whose parent is trashed, spam, or no longer exists, with 400 rest_comment_invalid_parent. Reactions are already rejected on a trashed or spam note.
  • update_item() rejects any change to a trashed or spam note other than restoring it (status of untrash or unspam), with 400 rest_comment_invalid_status. Undo in the editor sends untrash, so it keeps working.

The editor already shows REST error messages in a snackbar, and keeps the note form's text when a save fails, so no client changes are needed. The same change is in the Gutenberg plugin's 7.2 compat controller: ​https://github.com/WordPress/gutenberg/pull/85202. Originally reported in ​https://github.com/WordPress/gutenberg/issues/80678.

Testing Instructions

  1. Sign the same administrator into two browser sessions, A and B (for example a normal and a private window).
  2. In A, create a post, add a Paragraph, add a note to it, and save the draft.
  3. Open the same post in B and open All notes.
  4. In A, select the note and type a reply, but don't submit it.
  5. In B, delete the note from its Actions menu and wait for "Note deleted.".
  6. In A, without reloading, click Reply.
    • Before: "Reply added." The reply is not shown after a reload, but it is stored under the deleted note.
    • After: "This note was deleted, so your reply could not be added." The reply text stays in the form.

For Reopen & Reply, resolve the note in A before step 4. Clicking Reopen & Reply after the delete:

  • Before: brings the deleted note back as open, with only the new reply. Its earlier replies stay in the trash.
  • After: "This note was deleted, so it could not be updated." The note stays deleted.

The console snippet in the Trac ticket reproduces both in a single session. With this PR, its first request fails with 400.

Unit tests: npm run test:php -- --group 66351

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Reproducing both issues in two browser sessions on trunk, writing the patch and the PHPUnit tests, and drafting this description.

🤖 Generated with ​Claude Code

Note: See TracTickets for help on using tickets.