Comments: Add a per-action lock for notes - #13028
Conversation
Notes can currently be edited and deleted by anyone who can edit the post, with no way to freeze a discussion or preserve an audit trail. There is no admin UI for notes, so a deleted note and its whole reply thread are gone for good. Introduce wp_note_action_is_locked(), which reads a `_wp_notes_locked` post meta and then runs the `note_action_is_locked` filter over it. The lock is expressed per action - create, reply, edit, resolve, delete - so a site can either freeze a post's notes entirely or preserve them selectively, such as by disallowing deletion while review continues. Reading notes is never gated. WP_REST_Comments_Controller enforces the lock in its create, update and delete permission checks, and get_block_editor_settings() advertises the locked actions as `lockedNoteActions` so the editor can hide affordances the REST API will refuse.
|
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. |
Trac Ticket MissingThis pull request is missing a link to a Trac ticket. For a contribution to be considered, there must be a corresponding ticket in Trac. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. More information about contributing to WordPress on GitHub can be found in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
Claude wrote the code and this description; the plan and review are mine:
Backports note locking from Gutenberg WordPress/gutenberg#81546, which implements WordPress/gutenberg#72724.
What
Notes shipped in 6.9 with no way to freeze one. Anyone who can edit the post can edit or delete any note on it, and since there is no admin UI for notes, a deleted note - and its whole reply thread - is gone for good. Editorial flows that need an audit trail, agency sign-off, or a discussion frozen after publish have nothing to reach for.
This adds a lock that leaves notes readable but stops them changing. It is expressed per action -
create,reply,edit,resolve,delete- so a site can either freeze a post's notes entirely or preserve them selectively, for example by disallowing deletion while review carries on.How
One predicate.
wp_note_action_is_locked( $action, $post, $comment )reads a_wp_notes_lockedpost meta and then runs thenote_action_is_lockedfilter over it, passing the action name.Enforcement in the controller's permission checks.
WP_REST_Comments_Controller::create_item_permissions_check(),update_item_permissions_check()anddelete_item_permissions_check()each consult the lock and return arest_notes_locked403. Two new private methods do the work:get_note_request_actions()classifies the request. A DELETE isdelete. A create carrying_wp_note_statusmeta isresolve, one with aparentisreply, otherwisecreate. On an item route, sendingcontentiseditand changingstatusisresolve- resending the status a note already has mutates nothing, so it does not count. Anything else on the note (author, date, meta) falls back toedit.check_note_lock_permission()runs the predicate over those actions.check_edit_permission()is deliberately left alone. It is shared by update, delete and the read path, and it never learns which action is underway, so a per-action lock cannot live there.Who may lock. Whoever can write the meta:
edit_others_postsplusedit_poston the target, enforced by the meta'sauth_callback. Editors and up, not the post's author alone - an author locking reviewers out of their own review thread would defeat the point. No new capabilities.The editor follows along.
get_block_editor_settings()publishes alockedNoteActionsarray for the post being edited. The Gutenberg side reads it and hides or disables what the server will refuse. That is a courtesy only; every mutation is enforced here.Two supporting pieces:
_wp_post_type_supports_notes()inpost.phpreads thenotesflag out of theeditorsupport arguments.WP_REST_Comments_Controllerhad this inline as a private method; it now needs a second caller, so it moves out and the private duplicate goes away.wp_register_note_lock_meta()registers_wp_notes_lockedon every post type that supports notes, hooked toinitat priority 20 so post types registered at the default priority are already in place.Scope
The gate is REST-scoped. Direct
wp_update_comment()/wp_delete_comment()calls, XML-RPC, and wp-admin bulk actions are not covered.That was a deliberate call rather than an oversight. There is no symmetric seam:
wp_update_comment_datacan return aWP_Error, but comment deletion has no equivalent short-circuit filter, so enforcing at the data layer would produce a lock that stops edits and not deletions - exactly backwards from the case that motivated the feature. Enforcing there would also block legitimate programmatic work like migrations and CLI cleanup. Notes are created and mutated exclusively through REST by the editor, and they are excluded from the default admin comment queries, so the REST layer is the whole surface a user can reach. Worth revisiting if a note admin UI arrives.Lock management UI is also out of scope. This is the enforcement layer a toggle can later sit on.
Testing instructions
wp post meta update <post-id> _wp_notes_locked 1GET /wp/v2/comments?post=<post-id>&type=note&status=allstill returns the notes.rest_notes_locked403.Run the tests:
Open questions
Carried over from the Gutenberg PR, worth settling before the names fossilise:
_wp_notes_locked-edit_others_posts(as here),manage_options, or plainedit_post?note_action_is_locked( $locked, $action, $post, $comment )(as here) or an array-returningnotes_locked_actions( $actions, $post )? The boolean composes better for capability carve-outs._wp_notes_locked/note_action_is_locked/rest_notes_locked/lockedNoteActions.Tests
tests/phpunit/tests/rest-api/rest-notes-locking.php- 12 tests, 81 assertions, driving real REST dispatches so the permission checks actually run:auth_callbackrefusing an author and accepting an editor;lockedNoteActionsreflecting both the meta and the filter.@ticketannotations still need adding once the ticket exists.