Skip to content

REST API: Explore a dedicated notes endpoint - #13043

Draft
adamsilverstein wants to merge 5 commits into
WordPress:trunkfrom
adamsilverstein:add/rest-notes-controller
Draft

REST API: Explore a dedicated notes endpoint#13043
adamsilverstein wants to merge 5 commits into
WordPress:trunkfrom
adamsilverstein:add/rest-notes-controller

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 13, 2026

Copy link
Copy Markdown
Member

What

Exploratory Core companion to Gutenberg #81599, which answers Mamaduka's suggestion that notes deserve their own REST endpoint instead of a filtered view of wp/v2/comments.

Two halves, one per commit, so the trade is easy to read:

  1. Add WP_REST_Notes_Controller, serving wp/v2/notes.
  2. Take note handling back out of WP_REST_Comments_Controller.

The second half is the interesting one. wp/v2/comments currently carries note rules in nine places. After this, it carries one.

The endpoint

WP_REST_Notes_Controller extends WP_REST_Comments_Controller, so notes stay comment rows: the same comment meta, the same rest_prepare_comment filter, and anything registered through register_rest_field( 'comment', ... ) all keep applying. The schema title stays comment on purpose.

What changes is the shape of the collection.

wp/v2/comments?type=note wp/v2/notes
Scoping client sends type=note&status=all every time fixed by the route
post optional required, and access is one question: can you edit_post?
Replies sibling rows the client re-parents nested in a replies array on their thread
Reply context _embed prepares children in view, so no content.raw prepared in the thread's context, content.raw all the way down
Pagination a page break can separate a reply from its parent pages cut between threads; X-WP-Total counts threads
Tally fetch every row to count _fields=id,post,reply_count
Per-note cost a COUNT query for the children link no children link, replies already travel inside

Replies for a whole page are fetched in one WP_Comment_Query, so the cost does not grow with the number of threads on the page.

reply_count counts the replies someone wrote. Resolving or reopening a thread records a reply of its own carrying _wp_note_status, and those would otherwise read as replies - a thread nobody answered reported two of them once it had been resolved and reopened. They stay in replies, since the thread renders them as status entries.

What comes out of the comments controller

WP_REST_Comments_Controller: +54 / -129. Its tests: -440.

Removed:

  • the $is_note branch and the forbidden-params fallback in get_items_permissions_check()
  • the edit_post re-mapping of edit context, in both get_items_permissions_check() and get_item_permissions_check()
  • the note login requirement, rest_cannot_create_note, the edit_post status cap, and the post type support check in create_item_permissions_check()
  • the && ! $is_note carve-outs that let notes past the draft and comments_open gates
  • 'note' in the create-route type allowlist
  • the wp_allow_comment() bypass for notes
  • the _wp_note_status injection before the content check, and the empty-note allowance inside it
  • the children link rebuild that re-added type and status for notes
  • check_post_type_supports_notes() itself

Three protected seams take their place, each with a one-line default that restores the pre-notes behaviour:

protected function get_allowed_comment_types()                                  // array( 'comment' )
protected function prepare_comment_for_content_check( $prepared, $request )      // $prepared
protected function determine_comment_approval( $prepared )                       // wp_allow_comment( $prepared, true )

The one branch that has to stay

check_read_permission() keeps excluding notes from the "approved comment on a readable post" shortcut:

if ( 'note' !== $comment->comment_type && ! empty( $comment->comment_post_ID ) ) {

Every note is stored approved - approved means resolved, not public - so without this line an anonymous request to wp/v2/comments/<id> returns any note on a public post. A new endpoint does not remove notes from the comments routes' ID space, so the guard belongs where the leak is. Removing it turned test_get_items_type_arg_unauthenticated red, which is a good test.

Testing

tests/phpunit/tests/rest-api/rest-notes-controller.php, 37 tests / 129 assertions, covering the nine behaviours that moved plus threading, context, pagination, _fields, and the role matrix that data_note_get_items_permissions_data_provider used to cover.

tests/phpunit/tests/comment/wpNotifyNoteMentions.php has two tests that post and update a note over HTTP to exercise the rest_insert_comment wiring. They follow notes to wp/v2/notes.

npm run test:php -- --filter 'WP_Test_REST_Notes_Controller|WP_Test_REST_Comments_Controller'
OK (211 tests, 1047 assertions)

npm run test:php
Tests: 30865, Assertions: 4558991, Failures: 1

The remaining failure is local environment, not this branch: Tests_Script_Modules_WpScriptModules::test_default_script_module_files_exist wants built files this worktree has not generated. Test_oEmbed_Controller::test_proxy_with_classic_embed_provider joins it whenever the run cannot reach the network.

tests/qunit/fixtures/wp-api-generated.js is regenerated for the two new routes, in its own commit.

Open questions

  • Trac ticket. Not filed yet. The notes REST work so far sits under #64096 and #64152; this may want its own.
  • Deprecation path. wp/v2/comments?type=note works today and Gutenberg trunk still uses it. This branch removes the permission handling that makes it usable, which is a decision to make deliberately, not a side effect. Keeping both alive through one release is the conservative option.
  • edit_comment for the single-note route. get_item_permissions_check() here uses edit_comment, which maps to edit_post on the parent. That preserves what the comments controller did, but is worth a second look.
  • Preloading. block_editor_rest_api_preload_paths would want wp/v2/notes alongside or instead of the comments path.

If this direction holds, the Gutenberg side needs no compat shim at all - the plugin can drop its copy of the controller and consume the Core route directly.

AI Use

Claude Code did the typing here, I did the asking. I will review and test.

Serve editorial notes from wp/v2/notes instead of asking clients to filter
wp/v2/comments by type. The collection returns threads with their replies
nested and prepared in the same context, so pagination never orphans a reply
and context=edit carries content.raw all the way down, which _embed cannot do.

Opens up check_post_type_supports_notes() so the subclass can reuse it.
With wp/v2/notes serving notes, the comments controller no longer needs to
branch on comment type. Note permissions, the empty-note-on-resolve
allowance, duplicate and flood bypass, and the post type support check all
move to WP_REST_Notes_Controller, reached through three protected seams:
get_allowed_comment_types(), prepare_comment_for_content_check() and
determine_comment_approval().

One branch stays: check_read_permission() keeps excluding notes from the
approved-comment shortcut. Notes are stored approved, so without it the
comments routes would hand them to anonymous readers by ID.
@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

These two exercise the rest_insert_comment wiring through HTTP, so they have
to follow notes to wp/v2/notes now that the comments route no longer accepts
the note type.
Resolving or reopening a thread writes a reply of its own carrying
`_wp_note_status`, so a thread nobody answered reported two replies once it
had been resolved and reopened. Count only the replies someone wrote, while
still returning the resolution entries in `replies` so the thread can render
its history.
@adamsilverstein

Copy link
Copy Markdown
Member Author

Claude carried a fix over from the plugin PR, details below:

@Mamaduka's review on WordPress/gutenberg#81599 asked whether the notes count should include replies, and whether replies to resolutions count. Chasing that turned up a bug in reply_count that this branch inherited.

Resolving or reopening a thread does not just flip comment_approved - it also writes a reply of its own carrying _wp_note_status. Those rows were being counted as replies, so a thread nobody ever answered reported reply_count: 2 once it had been resolved and reopened. Fixed in de4fce6: only replies someone wrote are counted, while replies still carries the resolution entries, since the thread UI renders them as status rows rather than dropping them.

Test is test_reply_count_ignores_resolution_entries in tests/phpunit/tests/rest-api/rest-notes-controller.php. It fails on the previous commit with 3 where 1 is expected.

The other half of that review, moving the client entity to a commentType kind, is plugin side only and stays on the Gutenberg PR.

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