diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 20303dc1e7e17..33f8918d9fef4 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -2186,6 +2186,7 @@ function wp_insert_comment( $commentdata ) { * filtering the same comment more than once. * * @since 2.0.0 + * @since 7.1.0 The note mention attributes are allowed in the content of `note` comments. * * @param array $commentdata Contains information on the comment. * @return array Parsed comment information. @@ -2218,6 +2219,19 @@ function wp_filter_comment( $commentdata ) { $commentdata['comment_agent'] = apply_filters( 'pre_comment_user_agent', ( $commentdata['comment_agent'] ?? '' ) ); /** This filter is documented in wp-includes/comment.php */ $commentdata['comment_author'] = apply_filters( 'pre_comment_author_name', $commentdata['comment_author'] ); + /* + * Notes may contain `@` mention markup that the default comment kses + * allowlist would strip. The extended allowlist is attached only while + * this note's content is filtered - never for other comment types - so + * that the markup is not writable from regular (including anonymous) + * comments. See _wp_kses_allow_note_mention_attributes(). + */ + $is_note = isset( $commentdata['comment_type'] ) && 'note' === $commentdata['comment_type']; + + if ( $is_note ) { + add_filter( 'wp_kses_allowed_html', '_wp_kses_allow_note_mention_attributes', 10, 2 ); + } + /** * Filters the comment content before it is set. * @@ -2226,6 +2240,10 @@ function wp_filter_comment( $commentdata ) { * @param string $comment_content The comment content. */ $commentdata['comment_content'] = apply_filters( 'pre_comment_content', $commentdata['comment_content'] ); + + if ( $is_note ) { + remove_filter( 'wp_kses_allowed_html', '_wp_kses_allow_note_mention_attributes' ); + } /** * Filters the comment author's IP address before it is set. * diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 37d457a3e18a2..7ae7dd4848236 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1131,6 +1131,46 @@ function wp_kses_allowed_html( $context = '' ) { } } +/** + * Allows the note mention markup in comment content. + * + * The notes `@` mention completer stores a mention as a link to the + * mentioned user's author page, the `user-N` class carrying the mentioned + * user's ID: `@Name`. The + * default comment allowlist includes `a` but only its `href` and `title` + * attributes, so for users without `unfiltered_html` the mention classes + * would be stripped on save. + * + * This callback is deliberately not attached globally: `class` attributes are + * CSS and JavaScript selector hooks, so allowing them in every comment would + * extend what anonymous commenters can publish (for example, text disguised + * by theme classes). Instead, wp_filter_comment() attaches it + * around the 'pre_comment_content' filter only while a `note` comment is + * being filtered. Notes can only be written by logged-in users who can edit + * the post, and are never rendered on the front end, so the sanitization of + * regular comments is unchanged. + * + * @since 7.1.0 + * @access private + * + * @param array|string $allowed The allowed tags structure for the context. + * @param string $context The kses context. + * @return array|string Modified allowed tags structure. + */ +function _wp_kses_allow_note_mention_attributes( $allowed, $context ) { + if ( 'pre_comment_content' !== $context || ! is_array( $allowed ) ) { + return $allowed; + } + + if ( ! isset( $allowed['a'] ) || ! is_array( $allowed['a'] ) ) { + $allowed['a'] = array(); + } + + $allowed['a']['class'] = true; + + return $allowed; +} + /** * You add any KSES hooks here. * diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 59353a2b7a20c..c115309a4a5e3 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -536,6 +536,145 @@ public function test_wp_kses_allowed_html() { $this->assertSame( $allowedtags, wp_kses_allowed_html( 'data' ) ); } + /** + * Tests that the 'pre_comment_content' context is not loosened by the note mention support. + * + * The note mention attributes must only be allowed while a `note` comment is + * being filtered (see wp_filter_comment()), never in the comment content + * context itself, which also sanitizes regular (including anonymous) comments. + * + * @ticket 65622 + * + * @covers ::wp_kses_allowed_html + */ + public function test_wp_kses_allowed_html_pre_comment_content_disallows_mention_attributes() { + global $allowedtags; + + $this->assertSame( $allowedtags, wp_kses_allowed_html( 'pre_comment_content' ) ); + } + + /** + * Tests that a note mention survives content sanitization of a `note` comment. + * + * @ticket 65622 + * + * @covers ::wp_filter_comment + * @covers ::_wp_kses_allow_note_mention_attributes + */ + public function test_note_mention_markup_survives_note_content_sanitization() { + add_filter( 'pre_comment_content', 'wp_filter_kses' ); + + /* + * The mention href is external to the test site so that wp_rel_ugc() + * - which applies to notes like any other comment - deterministically + * appends `rel="nofollow ugc"`. + */ + $content = 'Hello @admin!'; + $filtered = wp_filter_comment( wp_slash( $this->get_mention_commentdata( 'note', $content ) ) ); + + remove_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $this->assertSame( + 'Hello @admin!', + wp_unslash( $filtered['comment_content'] ) + ); + } + + /** + * Tests that only the `class` attribute is allowed on note links beyond the defaults. + * + * @ticket 65622 + * + * @covers ::wp_filter_comment + * @covers ::_wp_kses_allow_note_mention_attributes + */ + public function test_note_mention_allows_only_class_on_note_links() { + add_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $content = 'Hello @admin!'; + $filtered = wp_filter_comment( wp_slash( $this->get_mention_commentdata( 'note', $content ) ) ); + + remove_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $this->assertSame( + 'Hello @admin!', + wp_unslash( $filtered['comment_content'] ), + 'Attributes beyond `class` and the default link attributes should be stripped from note links.' + ); + } + + /** + * Tests that the note mention markup is stripped from regular comment content. + * + * @ticket 65622 + * + * @covers ::wp_filter_comment + */ + public function test_note_mention_markup_stripped_from_regular_comment_content() { + add_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $content = 'Hello @admin!'; + $filtered = wp_filter_comment( wp_slash( $this->get_mention_commentdata( 'comment', $content ) ) ); + + remove_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $this->assertSame( + 'Hello @admin!', + wp_unslash( $filtered['comment_content'] ) + ); + } + + /** + * Tests that the note mention allowance does not leak beyond the note being filtered. + * + * @ticket 65622 + * + * @covers ::wp_filter_comment + */ + public function test_note_mention_allowance_does_not_leak_after_note_filtering() { + global $allowedtags; + + add_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $content = 'Hello @admin!'; + wp_filter_comment( wp_slash( $this->get_mention_commentdata( 'note', $content ) ) ); + + // A regular comment filtered after a note still gets the default rules. + $filtered = wp_filter_comment( wp_slash( $this->get_mention_commentdata( 'comment', $content ) ) ); + + remove_filter( 'pre_comment_content', 'wp_filter_kses' ); + + $this->assertSame( + 'Hello @admin!', + wp_unslash( $filtered['comment_content'] ), + 'The mention classes should be stripped from a regular comment filtered after a note.' + ); + $this->assertSame( + $allowedtags, + wp_kses_allowed_html( 'pre_comment_content' ), + 'The comment content allowlist should be back to its default after a note is filtered.' + ); + } + + /** + * Builds a complete commentdata array for wp_filter_comment(). + * + * @param string $comment_type The comment type. + * @param string $content The comment content. + * @return array Commentdata containing every field wp_filter_comment() reads. + */ + private function get_mention_commentdata( $comment_type, $content ) { + return array( + 'comment_content' => $content, + 'comment_type' => $comment_type, + 'comment_author' => 'admin', + 'comment_author_IP' => '127.0.0.1', + 'comment_author_url' => 'http://example.org', + 'comment_author_email' => 'admin@example.org', + 'comment_agent' => '', + ); + } + public function test_hyphenated_tag() { $content = 'Alot of hyphens.'; $custom_tags = array(