#65639 closed enhancement (fixed)
Comments: Notify mentioned users and thread followers for Notes @mentions
| Reported by: | adamsilverstein | Owned by: | adamsilverstein |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.1 |
| Component: | Comments | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests commit |
| Cc: | Focuses: |
Description (last modified by )
Split out of #65622, which now covers only the kses allowance that lets note @mention markup survive sanitization. This ticket covers the notification layer for Notes @mentions.
When a collaborator is @mentioned in a note, they should be emailed so they hear about it. The Gutenberg Notes @mention completer stores each mention as a chip carrying the mentioned user's ID in a class token: <span class="wp-note-mention user-N">@Name</span>.
Proposed behavior
On rest_insert_comment for note comments (alongside the existing post-author notification), a new wp_notify_note_mentions():
- Parses mentions out of the saved content via
wp_get_note_mentioned_user_ids(). Only elements carrying both thewp-note-mentionclass and auser-Nclass token are treated as mentions, so ordinary markup cannot be used to address notifications. - Emails each mentioned user a short message linking back to the post editor (
wp_send_note_notification()), composed in the recipient's locale viaswitch_to_user_locale().
That is the whole feature.
Audience rules
- The post author is excluded;
wp_new_comment_via_rest_notify_postauthor()already notifies them of every note. - The note's own author is never notified about their own note.
- Recipients are limited to users who can
edit_commentthe note, matchingWP_REST_Comments_Controller::check_read_permission()for notes, so emails cannot leak note content to users who cannot see the note in the editor. - Notifications fire only when a note is created; editing a note does not re-notify.
- Everything honors the existing
wp_notes_notifyoption.
New public API
Three functions, and no new filters:
wp_get_note_mentioned_user_ids( $content )wp_notify_note_mentions( $comment, $request, $creating )wp_send_note_notification( $user, $comment, $post )
Existing hooks cover extension: comment_notification_recipients already runs on every note through wp_notify_postauthor(), so extra recipients can be added there, and pre_wp_mail / wp_mail can suppress delivery or reroute it to another channel.
Patch
Pull request: https://github.com/WordPress/wordpress-develop/pull/12548
Ports the notification layer from Gutenberg PR https://github.com/WordPress/gutenberg/pull/79606 (approved, but still open upstream; this should not land until it merges). The kses half is in #65622 / https://github.com/WordPress/wordpress-develop/pull/12503.
Scope changes since this ticket was filed
- The per-thread followers model (subscribing participants,
_wp_note_followersmeta, notifying on replies) was dropped. It is tracked upstream in Gutenberg #80279 and would be a separate ticket here. - The
wp_note_notification_recipients,wp_note_notification_subjectandwp_note_notification_textfilters were dropped in favor of the existing comment and mail hooks. - Mention markup moved from anchors to
spanchips in Gutenberg #80528, so the parser matches spans rather than anchors.
Related
Change History (13)
This ticket was mentioned in PR #12548 on WordPress/wordpress-develop by @adamsilverstein.
4 weeks ago
#1
#2
@
10 days ago
- Keywords commit added
- Milestone Awaiting Review → 7.1
- Owner set to
- Status new → assigned
@adamsilverstein commented on PR #12548:
9 days ago
#5
I'm fine adding the suggested filters, the argument for not adding them was to reduce new public APIs when we know a new full blown notification system is needed.
@westonruter commented on PR #12548:
9 days ago
#6
IMO, the filters can be added later as need arises.
@westonruter commented on PR #12548:
9 days ago
#7
- No email headers.
wp_notify_postauthor()sendsContent-Type: text/plain; charset=<blog_charset>(pluggable.php:1852-1853). This sends none, sowp_mail()'s defaults apply. Functionally fine, but inconsistent with the sibling notification.
This makes sense to me to add. We wouldn't want HTML email to be sent accidentally, right?
@westonruter commented on PR #12548:
9 days ago
#8
### Editor link is built with the _sender's_ capabilities
comment.php—$edit_link = $post ? get_edit_post_link( $post->ID, 'url' ) : '';
Everything else in this function is composed in the recipient's context (
switch_to_user_locale( $user->ID )), butget_edit_post_link()readscurrent_user_can().
This review also makes sense to me. Unless the functions get marked as private, it should probably make sure that the edit post link is constructed appropriately for the _recipient_. Also consider the case where the functions could be called during WP Cron when no user is logged-in, in which case no edit link would be included.
@adamsilverstein commented on PR #12548:
9 days ago
#9
- No email headers.
wp_notify_postauthor()sendsContent-Type: text/plain; charset=<blog_charset>(pluggable.php:1852-1853). This sends none, sowp_mail()'s defaults apply. Functionally fine, but inconsistent with the sibling notification.This makes sense to me to add. We wouldn't want HTML email to be sent accidentally, right?
right
This review also makes sense to me. Unless the functions get marked as private, it should probably make sure that the edit post link is constructed appropriately for the recipient. Also consider the case where the functions could be called during WP Cron when no user is logged-in, in which case no edit link would be included.
Good point, will adjust.
@adamsilverstein commented on PR #12548:
9 days ago
#10
Both addressed in 6b687d1869.
Content type. Now sends Content-Type: text/plain; charset=<blog_charset> as a header, same as wp_notify_postauthor() does at pluggable.php:1852-1853. Right, the point is that a filtered wp_mail() default should not be able to turn this into HTML.
Editor link. Switches to the recipient for the length of the get_edit_post_link() call and restores the previous user right after, so it brackets the same way switch_to_user_locale() / restore_previous_locale() already does in that function:
$edit_link = ''; if ( $post ) { $previous_user_id = get_current_user_id(); wp_set_current_user( $user->ID ); $edit_link = (string) get_edit_post_link( $post->ID, 'url' ); wp_set_current_user( $previous_user_id ); }
Good call on WP-Cron - I wrote a test that sets the current user to 0 before notifying, and against the old code it failed with no link in the message at all. Two new tests cover both changes (test_editor_link_is_built_for_the_recipient, test_email_is_sent_as_plain_text), verified red first. 16/16 green, phpcs and phpstan clean.
Worth noting wp_notify_postauthor() has the same problem on its note branch - get_edit_post_link( $comment->comment_post_ID, 'url' ) at pluggable.php:1945 is evaluated against whoever is current, not the post author it is mailing. Same class of thing as the double wp_specialchars_decode() on $blogname I mentioned above. Both look worth a separate ticket rather than widening this one.
On the filters - agreed, leaving them out and adding later as the need arises.
@adamsilverstein commented on PR #12548:
9 days ago
#12
Fixed in https://core.trac.wordpress.org/changeset/63012.
cc: @t-hamano
This ticket was mentioned in PR #13026 on WordPress/wordpress-develop by @adamsilverstein.
23 hours ago
#13
Backports the Notes thread followers and resolution notification layers from Gutenberg, building on the mention notifications that landed in 7.1 (#12548, Trac #65639).
Trac ticket: to be filed - see the open question at the bottom before it is.
Upstream status: the source Gutenberg PRs are open, WordPress/gutenberg#80281 (followers) and WordPress/gutenberg#81544 (events). This should not land before they do.
## What
Two audiences that Notes currently has no way to reach.
Thread followers. Start a thread, reply to it, or get mentioned in it, and you are subscribed to it. Later replies email you, even when they do not name you. Today only the post author (a generic email for every note) and users named in one specific note (the mention email) hear anything at all, so the people already in a conversation miss its replies.
Resolution events. Resolving or reopening a thread posts a child note carrying _wp_note_status meta. That note is bookkeeping, not conversation - the resolve one has no content whatsoever - but the post author is currently emailed the generic "a new note was added" message for it, with an empty body. Followers and the post author now get copy about the event instead, and the generic emails are suppressed for that note.
## How
Everything lands in wp-includes/comment.php next to the existing note notification functions, hooked from default-filters.php.
Priority on rest_insert_comment | Function | Role |
|---|---|---|
| 9 | wp_route_post_author_mention_notification() | A mentioned post author gets the mention email; the generic one is suppressed for that note through notify_post_author, the filter it already consults.
|
| 10 | wp_notify_note_mentions() | Unchanged, from 7.1. |
| 11 | wp_notify_note_followers() | Followers hear about a new reply, minus the users it mentions, its author, and the post author when they are already being emailed. |
| 11 | wp_notify_new_mentions_on_note_update() | An edit that adds a mention notifies the newly mentioned user; existing followers are not re-notified. |
| 12 | wp_maintain_note_followers() | Subscribes the author and mentioned users. Runs last, so "existing followers" means "before this note". |
Resolution events run on rest_after_insert_comment (wp_notify_note_event()). WP_REST_Comments_Controller saves comment meta *between* the two REST comment actions, so on the later one _wp_note_status is stored and the event is recognized whatever the client sent. For the same reason wp_get_note_status_event() consults the request first and the stored meta second, which is what lets the earlier hooks recognize a system note before its meta exists.
Followers are stored as one _wp_note_followers comment-meta row per user on the thread's top-level note, so concurrent replies subscribe users independently instead of racing over a single array value. Bookkeeping is deliberately not gated on wp_notes_notify: that option governs whether email is sent, not who is participating, so turning notifications on later works for threads that already exist.
Every email carries a tokenized unfollow link - wp_hash() over thread and user, compared with hash_equals(), handled through admin-post.php including the nopriv variant so it works logged out, like any email unsubscribe. It never expires, so links in old email keep working.
### One email per user per note insert
- Actor - never emailed about their own note or their own resolve.
- Mention - a mentioned user, post author included, gets the mention email and nothing else.
- Event - remaining followers and the post author get the event email.
- Follower, then the generic post-author email - unchanged for ordinary replies.
### Security
Every recipient must pass user_can( $user_id, 'edit_comment', $note_id ) before any note content is composed for them. That is the bar WP_REST_Comments_Controller::check_read_permission() already applies to reading a note; a read_post check would leak internal notes to, say, subscribers on a public post. Emails pin Content-Type: text/plain explicitly so a filtered default cannot turn a body into HTML, compose in the recipient's locale, and build the editor link as the recipient.
### Extensibility
wp_note_notification_sent fires once per addressed recipient with the user ID, the note, why they were notified (mention, post_author_mention, follower, resolved, reopen), and whether wp_mail() accepted it. Email is the only channel here; this is the seam another channel - or a future notifications API - can consume without reimplementing recipient resolution. Recipient, subject, and body filters are provided per notification type.
## Testing instructions
npm run test:php -- --group notes
tests/phpunit/tests/comment/wpNotifyNoteFollowers.php and wpNotifyNoteEvent.php cover subscription bookkeeping, the dedupe rules, the visibility gate, the unfollow token, the meta-timing behavior, and the suppression of both generic emails. The event tests dispatch real REST requests so the controller's own hook order and meta save are part of what is being tested.
## Open question
wp_new_comment_via_rest_notify_postauthor() gains optional $request and $creating parameters here so it can recognize a thread-event note and step aside. The alternative is leaving it untouched and suppressing it through the notify_post_author filter, which is what the Gutenberg plugin does, since it has to work on older versions. The direct check reads better in core, but happy to switch if the added parameters are unwelcome on an existing function.
#12548: https://github.com/WordPress/wordpress-develop/pull/12548
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Backports the notification layer from the Gutenberg Notes @mention work (WordPress/gutenberg#79606, still open upstream). Split out of #12503, which now covers only the kses allowance (the merged WordPress/gutenberg#79604 and WordPress/gutenberg#80221).
## What
On
rest_insert_commentfornotecomments (alongside the existing post-author notification),wp_notify_note_mentions():wp_get_note_mentioned_user_ids(): only anchors carrying both thewp-note-mentionclass and auser-Nclass token are treated as mentions, so ordinary links cannot be used to address notifications.wp_send_note_notification())._wp_note_followersmeta row per user on the thread's top-level note - so concurrent replies cannot clobber each other - and the meta is registered for REST (editable by users who canedit_commentthe note) so follower management UI can build on it.Audience rules:
wp_new_comment_via_rest_notify_postauthor()already notifies them of every note.edit_commentthe note, matchingWP_REST_Comments_Controller::check_read_permission()for notes, so emails cannot leak note content to users who cannot see the note in the editor.wp_notes_notifyoption.New filters:
wp_note_notification_recipients,wp_note_notification_subject,wp_note_notification_text.## Why
Mentioning a collaborator in a note is only useful if they hear about it: the notification half closes that loop, and the follower model keeps everyone who has participated in a thread informed of later replies without requiring them to be re-mentioned.
This depends conceptually on the kses allowance in #12503: that is what lets the
user-Nmention class survive sanitization on a real REST write for users withoutunfiltered_html. The two are otherwise independent - no shared code paths - so they are reviewed and merged separately.## Testing
Unit tests in
tests/phpunit/tests/comment/wpNotifyNoteMentions.phpcover mention parsing (plain links ignored, IDs deduped), thread-root resolution, the mentioned-user email + auto-subscribe, author self-exclusion, post-author exclusion, capability gating (a mentioned subscriber is not emailed), follower-of-reply notification, thewp_notes_notifyoff switch, the recipients filter, follower removal, and the REST meta registration.## Proposed commit message