#65622 closed defect (bug) (fixed)
Comments: allow note mention attributes in comment content
| Reported by: | adamsilverstein | Owned by: | adamsilverstein |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.1 |
| Component: | Comments | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests needs-testing commit |
| Cc: | Focuses: |
Description
The Notes feature (introduced in WordPress 6.9 as the note comment type) is gaining an @ mention autocompleter in the editor, developed in Gutenberg PR 79604. A mention is stored in the note's comment_content as:
<a class="wp-note-mention" data-user-id="2" href="https://example.org/?author=2">@Name</a>
The default comment kses allowlist ($allowedtags) only keeps href and title on a elements. For users without unfiltered_html, the attributes that make a mention a mention - the wp-note-mention chip class and the mentioned user's ID - are stripped by wp_filter_kses() when the note is saved, so the mention degrades to a plain link.
This ticket backports the PHP part of the Gutenberg PR: allow class and data-user-id on a elements in the comment-content kses context so saved mentions survive sanitization.
Implementation
The Core PR adds a pre_comment_content case to the context switch in wp_kses_allowed_html(), following the existing user_description/pre_term_description/pre_user_description case (which similarly adds rel and target to links):
<?php case 'pre_comment_content': $tags = $allowedtags; $tags['a']['class'] = true; $tags['a']['data-user-id'] = true; /** This filter is documented in wp-includes/kses.php */ return apply_filters( 'wp_kses_allowed_html', $tags, $context );
The Gutenberg plugin implements this as a wp_kses_allowed_html filter; the switch branch is the Core-native equivalent.
Security considerations
Both attributes are inert markup: data-* attributes carry data only, and class has no behavior of its own. Attribute values are still sanitized by kses as usual (e.g. wp_kses_attr_check()), so this does not open a path for scriptable attributes.
Note that the pre_comment_content context applies to all comment content, not only note comments - kses runs before the comment type is knowable in the filter, and visitors could already submit class attributes that are simply stripped today. The practical effect for front-end comments is that a class or data-user-id attribute on a link is now preserved instead of removed; neither executes or styles anything by default.
Testing
Two new unit tests in tests/phpunit/tests/kses.php:
test_wp_kses_allowed_html_pre_comment_content_allows_mention_attributes- the context allowlist includes the two attributes, and the defaultdatacontext is unchanged.test_note_mention_markup_survives_comment_content_sanitization- a mention link round-trips throughwp_kses()in the comment context.
Both fail without the kses.php change and pass with it.
Change History (21)
This ticket was mentioned in PR #12503 on WordPress/wordpress-develop by @adamsilverstein.
5 weeks ago
#1
@Mamaduka commented on PR #12503:
5 weeks ago
#3
Sorry, only have time for a surface review.
- Not sure if we need new methods like
wp_get_note_thread_root_id. Values can be easily derived. - The follower notifications seem like work for a separate PR. I don't think it should be in scope for mention notifications.
I think the general API for managing email (or other) notifications would be better for the project than handling each case individually.
This ticket was mentioned in PR #12548 on WordPress/wordpress-develop by @adamsilverstein.
5 weeks ago
#4
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).
Trac ticket: pending — a dedicated ticket is being filed for the notification layer; this PR will be updated with the number and a
Fixes #NNNNNline once it exists. The kses half remains on #65622.
Upstream status: the source Gutenberg PR (#79606) has not merged yet, so this backport should not land until it does.
## What
On rest_insert_comment for note comments (alongside the existing post-author notification), wp_notify_note_mentions():
- Parses mentions out of the saved content via
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. - Notifies each mentioned user plus any existing followers of the thread, with a short email linking back to the post (
wp_send_note_notification()). - Subscribes the note author and everyone they mention to the thread. Followers are stored as one
_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:
- 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. - Everything honors the existing
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-N mention class survive sanitization on a real REST write for users without unfiltered_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.php cover 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, the wp_notes_notify off switch, the recipients filter, follower removal, and the REST meta registration.
npm run test:php -- --group notes
## Proposed commit message
Comments: Notify mentioned users and thread followers about new notes. Port the notification layer from the Gutenberg Notes @mention work. On rest_insert_comment for note comments, parse the user-N mention classes out of the saved content, email each mentioned user plus any existing followers of the thread, and subscribe the note author and everyone they mention. Followers are stored as one _wp_note_followers meta row per user on the thread's top-level note so concurrent replies cannot clobber each other, and the meta is registered for REST so follower management UI can build on it. The post author is excluded because 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, and recipients are limited to users who can edit_comment the note so note content cannot leak. Everything honors the wp_notes_notify option. See related Gutenberg pull request: https://github.com/WordPress/gutenberg/pull/79606. Props mamaduka. Fixes #NNNNN.
@adamsilverstein commented on PR #12503:
5 weeks ago
#5
Sorry, only have time for a surface review.
- Not sure if we need new methods like
wp_get_note_thread_root_id. Values can be easily derived.- The follower notifications seem like work for a separate PR. I don't think it should be in scope for mention notifications.
I think the general API for managing email (or other) notifications would be better for the project than handling each case individually.
Reducing this to the kses only changes which matches the merged PRs. Opening a follow up for the notifications part to dig deeper.
@adamsilverstein commented on PR #12503:
5 weeks ago
#6
#7
@
5 weeks ago
## Patch Testing Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/12548
### Environment
WordPress: 7.1.0
PHP: 8.3.32
Server: nginx 1.31.2
Database: mysql 9.7.1
Browser: Helium 0.14.3.1 (Official Build, Chromium 150.0.7871.46) Arch Linux (64-bit)
OS: Operating System CachyOS 7.1.3
Theme: twentytwentyfive 1.5
MU Plugins:
None
Plugins:
- Hello Dolly 1.7.2
- Gutenberg 23.6.0-rc.1
- WP Mail Logging 1.16.0
### Steps taken
- Run build command for testing environment "npm run build:dev".
- Start testing environment.
- Installed Gutenberg plugin and WP Mail Logging to assert email notifications without using a SMTP server.
- Created the 4 users (username/role)
- test4 / author
- test3 / editor
- test2 / subscriber
- test1/ editor
- With test4 user logged in, created a example post then sign out.
- With test1 user logged in, create one note.
- Reply to your note mentioning each user created including yourself.
- Save the post and sign out.
- With admin user logged in, Check "WP MAIL LOGGING"
✅ Patch is solving the problem: Only logs for post test4 and test3 email should appear indicating that the feature works as intended.
### Expected result
Only post author and users with edit_comment permission should be notified when mention in a post using notes.
.
### Additional Notes
This test was focused on audience rules, subscribers and filters were not tested.
#8
@
5 weeks ago
## Patch Testing Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/12503
### Environment
- WordPress: 7.1-beta1 (trunk)
- PHP: 8.5.6 (CLI) / Docker php-fpm image (local-env)
- Server: nginx (local-env Docker)
- Database: MySQL 9.7 (local-env Docker)
- OS: macOS (Darwin 24.6.0)
### Note on scope
The implementation on the linked PR has evolved since the ticket description was written. The description shows an early revision that added class and data-user-id to the pre_comment_content context globally. The current PR instead:
- Allows only
classona(notdata-user-id— the mentioned user's ID travels in auser-Nclass token instead). - Scopes the allowance to
note-type comments only, via a filter added immediately before and removed immediately after thepre_comment_contentfilter runs insidewp_filter_comment(), rather than loosening the sharedpre_comment_contentcontext for all comments (including anonymous front-end comments).
This is a meaningfully safer design than the one described in the ticket, so testing below covers the actual PR code.
### Steps taken
- Checked out PR #12503 (
add/notes-mention-kses) against trunk. - Ran the PR's automated tests:
npm run test:php -- --filter test_note_mention— all 5 new tests pass. - Ran the full existing suite for regressions:
tests/phpunit/tests/kses.php(363 tests) andtests/phpunit/tests/comment.php(84 tests), plus the REST comments controller group (201 tests) — all pass. - Manually verified the real-world flow with WP-CLI (
wp eval-file) as anauthor-role user (nounfiltered_html):- Saved a note comment containing
<a class="wp-note-mention user-2" href="...">@Name</a>→ theclassattribute survived sanitization. - Saved a regular comment with the identical markup → the
classattribute was stripped, degrading to a plain link, confirming the allowance does not leak to ordinary comments. - Confirmed
wp_kses_allowed_html( 'pre_comment_content' )still returns the stock$allowedtagsafter a note is filtered (no state leak between requests).
- Saved a note comment containing
### Result
✅ Patch is solving the problem as scoped. Note mentions saved by users without unfiltered_html retain the wp-note-mention/user-N classes needed for the mention chip to render, while regular (including anonymous front-end) comment sanitization is unchanged. All existing and new automated tests pass, and manual verification matches the expected behavior described in the PR.
### Notes
- This report covers the kses allowance only (PR #12503 / this ticket). The notification layer (PR #12548) is tracked separately and was not re-tested here.
- No security regressions observed: attributes other than
class(e.g.data-user-id,onclick,style) remain stripped from note links pertest_note_mention_allows_only_class_on_note_links, andclassvalues are inert markup as noted in the ticket description.
@adamsilverstein commented on PR #12503:
4 weeks ago
#9
I wonder, instead of relying on classes, would it have been better to use a data attribute instead? It could be like
data-wp-note-mention-user="N". This would be less of a "blast radius" for allowing class names on notes, since in the admin allowing anyclasscould produce undesirable results. Like someone could craft a link that is using some class that makes it look weird or maybe something malicious. In contrast, if a data attribute were used, it would be allowed by Kses by default so there would be nothing special needed. This entire PR wouldn't be necessary then.
Hmmm, thats a great suggestion and we can still change this in beta since we just introduced this feature.
When we added this originally some commits explored other attributes to use that would already be accepted by kses to avoid having to add this increased permission. I think the PR went with classes in part because it offers styling for free and also... a data attribute may not have been considered.
Let me explore that suggested in a Gutenberg PR and maybe we can eliminate the need for this PR entirely as you suggested. We could still add styles dynamically if we need them to style the mentions (admin or front end).
@Mamaduka commented on PR #12503:
4 weeks ago
#10
Left to comment in https://github.com/WordPress/gutenberg/pull/80496#issuecomment-5030847683.
It's not hard to switch between implementation details, but unless we have clear goals: code, UI, UX, we might end up going in circles.
@adamsilverstein commented on PR #12503:
4 weeks ago
#11
Updated in 69ec26b015 to match the approach that landed from the discussion here and on gutenberg#80496 / gutenberg#80528:
- Mentions are now non-interactive
<span class="wp-note-mention user-N">chips - per @Mamaduka, mentions aren't links, and an anchor-based mention breaks in the Link format UI regardless of how the ID is carried. - @westonruter's open-
classconcern is addressed without the data attribute (which needs a kses allowance in the comment context anyway): the allowance is now always-on but a companionpre_comment_contentpass reduces span classes to exactly the two mention tokens, using the HTML API. Thewp_filter_comment()arm/disarm is gone entirely -comment.phpis untouched. - The Gutenberg plugin copy disables itself when core's
_wp_kses_allow_note_mention_span()exists.
Local runs: kses 366/366, comment group 582/582, REST comments controller 200/200.
@adamsilverstein commented on PR #12503:
4 weeks ago
#12
Copilot's whitespace-brittleness point on the <span > assertion was valid - fixed in 5000cb4596 using assertEqualHTML(), and mirrored to the Gutenberg PR.
@adamsilverstein commented on PR #12548:
2 weeks ago
#15
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:
2 weeks ago
#16
IMO, the filters can be added later as need arises.
@westonruter commented on PR #12548:
2 weeks ago
#17
- 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:
2 weeks ago
#18
### 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:
2 weeks ago
#19
- 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:
2 weeks ago
#20
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:
2 weeks ago
#21
Fixed in https://core.trac.wordpress.org/changeset/63012.
cc: @t-hamano
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Backports the PHP changes from the Gutenberg Notes @mention PR (WordPress/gutenberg#79604).
## What
Adds a
pre_comment_contentcontext towp_kses_allowed_html()that allowsclassanddata-user-idonaelements, following the existinguser_descriptioncontext pattern in the same switch.## Why
The Notes @mention completer stores a mention as
<a class="wp-note-mention" data-user-id="N" href="…">@Name</a>. The default comment kses allowlist ($allowedtags) only keepshrefandtitleon links, so for users withoutunfiltered_htmlthe attributes that make a mention a mention (the chip class and the mentioned user's ID) are stripped when the note is saved. Allowing them in the comment-content context lets saved mentions survive sanitization; both attributes are inert markup (data-*carries data only andclasshas no behavior of its own).The Gutenberg implementation achieves this via a
wp_kses_allowed_htmlfilter; in Core the switch branch inwp_kses_allowed_html()is the native equivalent (see theuser_description/pre_term_description/pre_user_descriptioncase, which similarly addsrelandtargetto links).## Testing
Two new unit tests in
tests/phpunit/tests/kses.php:test_wp_kses_allowed_html_pre_comment_content_allows_mention_attributesverifies the context allowlist (and that the defaultdatacontext is unchanged).test_note_mention_markup_survives_comment_content_sanitizationverifies a mention link round-trips throughwp_kses()in the comment context.Both fail without the
kses.phpchange and pass with it.