Skip to content

Fix note @ mentions render as links and rely on an open kses class allowance #80502

Description

@adamsilverstein

Description

Notes @ mentions are currently rendered as anchors carrying the mentioned user in a pair of CSS classes:

<a class="wp-note-mention user-12" href="">@Name</a>

That markup causes two separate problems.

1. Mentions are not links, but the Link format claims them

A mention is a token, not a navigable link. Because it is an a element, the Link format UI attaches to it: placing the caret inside a saved mention opens the link popover, and editing through it destroys the mention markup (screencast). The user ID is silently lost and the chip degrades to a plain link.

2. The kses allowance that lets mentions survive a save is open-ended

Comment content is filtered through kses in the pre_comment_content context, whose minimal $allowedtags allow only href/title on a. To let mention markup survive a save, #80221 added an allowance for class on a - but class is open-ended, so it cannot be left on permanently for every comment. It is instead armed and disarmed around each note write:

  • preprocess_comment / rest_preprocess_comment arm the allowance when the comment being written is a note,
  • a self-removing pre_comment_content filter at PHP_INT_MAX disarms it,
  • a rest_request_after_callbacks backstop disarms it if the write bails out before that point.

Two consequences:

  • Blast radius. While armed, a note author without unfiltered_html can persist any class on a link, not just user-N. Arbitrary classes on admin-rendered content are a live CSS and JavaScript selector surface. This was @westonruter's original concern on the Core backport.
  • Fragility. Correctness depends on every arm having a matching disarm across two request paths (direct and REST) - roughly 120 lines of stateful machinery in lib/compat/wordpress-7.1/block-comments.php guarding a single integer.

Proposed change

Render the mention as a non-interactive span chip and narrow the allowance instead of arming it:

-<a class="wp-note-mention user-12" href="…">@Name</a>
+<span class="wp-note-mention user-12">@Name</span>
  • A span takes mentions out of the Link format UI entirely, fixing problem 1. Keeping the wp-note-mention class prevents any registered format from claiming the element (formats are differentiated by tag name and class), and RichText round-trips the unregistered span without a dedicated mention format.
  • Replace the arm/disarm machinery with two always-on filters: one allowing span with class in pre_comment_content, and one running immediately after wp_filter_kses that reduces every span's class to the only two tokens the mention markup uses (wp-note-mention and user-N). The allowance becomes unconditional but is no longer open-ended, so there is nothing to arm and disarm.

The narrowing must only apply while the restrictive comment allowlist is active - users with unfiltered_html are filtered through wp_filter_post_kses, where arbitrary classes are already legal, and narrowing their markup would restrict what core permits them to post.

Accepted behavior change: any commenter can then persist <span> elements whose class is limited to those two tokens. This is inert - span is semantics-free, neither token is a styling or scripting hook outside the notes sidebar, and mention notification parsing only processes note-type comments.

Note on using a data attribute instead

The first attempt carried the user ID in data-wp-note-mention-user on the anchor. That was dropped for two reasons:

  • It does not avoid the kses allowance. data-* attributes are default-allowed in the post context only; the comment context uses the minimal $allowedtags, and the data-* wildcard path in wp_kses_attr_check() only applies when the allowlist already contains data-*. So an explicit allowance is required either way, and a strictly-allowlisted class has no less security value than an inert attribute.
  • An attribute unregistered with the link format was stripped by the format on edit, so it did not survive problem 1 either.

Legacy markup

The wp-note-mention / user-N classes have never shipped in a stable WordPress release, so no legacy selector or fallback needs to be retained for the tag-name change.

Step-by-step reproduction instructions

Problem 1:

  1. As a user without the unfiltered_html capability (e.g. an author), open a post, add a note on a block, type @ and pick a teammate, then save the note.
  2. Place the caret inside the saved mention chip. The Link format popover appears.
  3. Edit the mention through the link popover - the mention markup is destroyed and the user ID is lost.

Problem 2:

  1. With the same user, save a note containing a mention.
  2. Inspect the saved anchor: it carries class="wp-note-mention user-N", persisted through the temporarily-armed kses class allowance, which during that window would equally have persisted any other class.

Follow-ups

  • #79606 (mention notifications) parses mention anchors in PHP and needs to match mention spans instead; the user-N class parsing itself is unchanged. It should also gain an identity check before notifying, since the chip text can be partially edited while the span retains its user-N class.
  • wordpress-develop#12503 reduces to the same two filters in Core.

Environment info

  • Gutenberg trunk
  • Affects lib/compat/wordpress-7.1/block-comments.php and the collab sidebar mention completer

Metadata

Metadata

Labels

[Feature] NotesPhase 3 of the Gutenberg roadmap around block commenting[Package] Editor/packages/editor[Status] In ProgressTracking issues with work in progress[Type] BugAn existing feature does not function as intended[Type] Code QualityIssues or PRs that relate to code quality

Type

Projects

Status
✅ Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions