From 88db844623cdf041e6fc6359b4f002dbf44fe680 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Thu, 8 Oct 2026 20:44:36 +0200 Subject: [PATCH 1/6] Comments: Keep YouTube embeds as players when sanitizing --- .../changelog/fix-comments-youtube-embed-shortcode | 4 ++++ projects/packages/comments/src/editor/class-embeds.php | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode diff --git a/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode b/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode new file mode 100644 index 00000000000..be1eb1610dd --- /dev/null +++ b/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Show YouTube embeds as players instead of shortcode text. diff --git a/projects/packages/comments/src/editor/class-embeds.php b/projects/packages/comments/src/editor/class-embeds.php index 95359e58262..c0ce0639ade 100644 --- a/projects/packages/comments/src/editor/class-embeds.php +++ b/projects/packages/comments/src/editor/class-embeds.php @@ -230,7 +230,15 @@ public static function sanitize_html( $html ) { 'name' => true, ); + // Without pre_kses: its embed-to-shortcode reversals, like the shortcodes module's for YouTube, + // would turn the player into text such as "[youtube …]" that nothing here expands. + global $wp_filter; + $pre_kses = $wp_filter['pre_kses'] ?? null; + unset( $wp_filter['pre_kses'] ); $html = wp_kses( $html, $allowed ); + if ( null !== $pre_kses ) { + $wp_filter['pre_kses'] = $pre_kses; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restoring what was set aside above. + } if ( ! preg_match( '#<(iframe|img|video|audio)\b#i', $html ) && '' === trim( wp_strip_all_tags( $html ) ) ) { return ''; From 89a3b2d043e61ba43667d5772fed289e295ed745 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Thu, 8 Oct 2026 20:57:50 +0200 Subject: [PATCH 2/6] Comments: Expand shortcodes kses adds instead of skipping pre_kses --- .../comments/src/editor/class-embeds.php | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/projects/packages/comments/src/editor/class-embeds.php b/projects/packages/comments/src/editor/class-embeds.php index c0ce0639ade..b8db01cd226 100644 --- a/projects/packages/comments/src/editor/class-embeds.php +++ b/projects/packages/comments/src/editor/class-embeds.php @@ -230,14 +230,18 @@ public static function sanitize_html( $html ) { 'name' => true, ); - // Without pre_kses: its embed-to-shortcode reversals, like the shortcodes module's for YouTube, - // would turn the player into text such as "[youtube …]" that nothing here expands. - global $wp_filter; - $pre_kses = $wp_filter['pre_kses'] ?? null; - unset( $wp_filter['pre_kses'] ); - $html = wp_kses( $html, $allowed ); - if ( null !== $pre_kses ) { - $wp_filter['pre_kses'] = $pre_kses; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restoring what was set aside above. + $tags_in = static function ( $text ) { + preg_match_all( '@\[([^<>&/\[\]\x00-\x20=]++)@', $text, $matches ); + return array_filter( array_unique( $matches[1] ), 'shortcode_exists' ); + }; + $sent = $tags_in( $html ); + $html = wp_kses( $html, $allowed ); + + // The host's pre_kses reversals turn known players into shortcodes, such as "[youtube …]" on WordPress.com. + // Expand those as post content would, and leave any the provider sent as text. + $added = array_diff( $tags_in( $html ), $sent ); + if ( $added ) { + $html = (string) preg_replace_callback( '/' . get_shortcode_regex( $added ) . '/', 'do_shortcode_tag', $html ); } if ( ! preg_match( '#<(iframe|img|video|audio)\b#i', $html ) && '' === trim( wp_strip_all_tags( $html ) ) ) { From 2834e81906c0ad7ad4ea2de85136e2964938c5d5 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Thu, 8 Oct 2026 21:02:16 +0200 Subject: [PATCH 3/6] Comments: Expand only the youtube shortcode and sanitize its output --- .../comments/src/editor/class-embeds.php | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/projects/packages/comments/src/editor/class-embeds.php b/projects/packages/comments/src/editor/class-embeds.php index b8db01cd226..271c8ed3707 100644 --- a/projects/packages/comments/src/editor/class-embeds.php +++ b/projects/packages/comments/src/editor/class-embeds.php @@ -209,7 +209,8 @@ public static function sanitize_html( $html ) { } // Whole, so a script's body does not survive as text once kses drops its tags. - $html = (string) preg_replace( '#]*>.*?(?:|$)#is', '', $html ); + $scripts = '#]*>.*?(?:|$)#is'; + $html = (string) preg_replace( $scripts, '', $html ); $allowed = wp_kses_allowed_html( 'post' ); unset( $allowed['object'] ); @@ -230,18 +231,15 @@ public static function sanitize_html( $html ) { 'name' => true, ); - $tags_in = static function ( $text ) { - preg_match_all( '@\[([^<>&/\[\]\x00-\x20=]++)@', $text, $matches ); - return array_filter( array_unique( $matches[1] ), 'shortcode_exists' ); - }; - $sent = $tags_in( $html ); - $html = wp_kses( $html, $allowed ); - - // The host's pre_kses reversals turn known players into shortcodes, such as "[youtube …]" on WordPress.com. - // Expand those as post content would, and leave any the provider sent as text. - $added = array_diff( $tags_in( $html ), $sent ); - if ( $added ) { - $html = (string) preg_replace_callback( '/' . get_shortcode_regex( $added ) . '/', 'do_shortcode_tag', $html ); + $sent = str_contains( $html, '[youtube' ); + $html = wp_kses( $html, $allowed ); + + // WordPress.com's pre_kses reversals turn a YouTube player into "[youtube …]". Expand only that, only when + // kses added it, then apply the same rules through kses's own steps, minus the hook that would reverse it. + if ( ! $sent && str_contains( $html, '[youtube' ) && shortcode_exists( 'youtube' ) ) { + $html = (string) preg_replace_callback( '/' . get_shortcode_regex( array( 'youtube' ) ) . '/', 'do_shortcode_tag', $html ); + $html = (string) preg_replace( $scripts, '', $html ); + $html = wp_kses_split( wp_kses_normalize_entities( wp_kses_no_null( $html, array( 'slash_zero' => 'keep' ) ) ), $allowed, wp_allowed_protocols() ); } if ( ! preg_match( '#<(iframe|img|video|audio)\b#i', $html ) && '' === trim( wp_strip_all_tags( $html ) ) ) { From 33fde5c2aee0137af980c35238bf244c213378b1 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Thu, 8 Oct 2026 21:12:31 +0200 Subject: [PATCH 4/6] Comments: Rebuild the embed's iframe or image instead of running kses --- .../fix-comments-youtube-embed-shortcode | 2 +- .../comments/src/editor/class-embeds.php | 64 ++++++++----------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode b/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode index be1eb1610dd..f002e4fbe89 100644 --- a/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode +++ b/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode @@ -1,4 +1,4 @@ Significance: patch Type: fixed -Show YouTube embeds as players instead of shortcode text. +Show YouTube and other video players in comments on WordPress.com, and show embeds that need a script, such as TikTok, as links. diff --git a/projects/packages/comments/src/editor/class-embeds.php b/projects/packages/comments/src/editor/class-embeds.php index 271c8ed3707..1319aa13150 100644 --- a/projects/packages/comments/src/editor/class-embeds.php +++ b/projects/packages/comments/src/editor/class-embeds.php @@ -184,7 +184,7 @@ private static function respond( $data ) { unset( $data->scripts ); $data->html = self::sanitize_html( $data->html ?? '' ); - // Nothing to show without the script, so the comment will hold the link. Say so now. + // No player or photo to show, so the comment will hold the link. Say so now. if ( '' === $data->html ) { return new WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) ); } @@ -196,9 +196,8 @@ private static function respond( $data ) { } /** - * A provider's HTML as markup alone, or '' when nothing visible is left. Its script would run - * first-party for every reader on a commenter's say-so, and so would an inline handler, so the - * HTML goes through kses with core's post rules plus the iframe the players need. + * A provider's player or photo, rebuilt as a bare iframe or img, or '' when it sent neither. Nothing else of + * its HTML is kept, so no script or inline handler runs first-party for every reader on a commenter's say-so. * * @param mixed $html What the provider sent. * @return string @@ -208,45 +207,36 @@ public static function sanitize_html( $html ) { return ''; } - // Whole, so a script's body does not survive as text once kses drops its tags. - $scripts = '#]*>.*?(?:|$)#is'; - $html = (string) preg_replace( $scripts, '', $html ); - - $allowed = wp_kses_allowed_html( 'post' ); - unset( $allowed['object'] ); - $allowed['iframe'] = array( - 'src' => true, - 'width' => true, - 'height' => true, - 'title' => true, - 'allow' => true, - 'allowfullscreen' => true, - 'frameborder' => true, - 'scrolling' => true, - 'loading' => true, - 'referrerpolicy' => true, - 'sandbox' => true, - 'style' => true, - 'class' => true, - 'name' => true, + $kept = array( + 'IFRAME' => array( 'width', 'height', 'title', 'allow', 'allowfullscreen', 'frameborder', 'loading', 'referrerpolicy', 'sandbox' ), + 'IMG' => array( 'width', 'height', 'alt' ), ); - $sent = str_contains( $html, '[youtube' ); - $html = wp_kses( $html, $allowed ); + $sent = new \WP_HTML_Tag_Processor( $html ); + while ( $sent->next_tag() ) { + $name = $sent->get_tag(); + $src = $sent->get_attribute( 'src' ); + $src = is_string( $src ) && str_starts_with( $src, '//' ) ? 'https:' . $src : $src; - // WordPress.com's pre_kses reversals turn a YouTube player into "[youtube …]". Expand only that, only when - // kses added it, then apply the same rules through kses's own steps, minus the hook that would reverse it. - if ( ! $sent && str_contains( $html, '[youtube' ) && shortcode_exists( 'youtube' ) ) { - $html = (string) preg_replace_callback( '/' . get_shortcode_regex( array( 'youtube' ) ) . '/', 'do_shortcode_tag', $html ); - $html = (string) preg_replace( $scripts, '', $html ); - $html = wp_kses_split( wp_kses_normalize_entities( wp_kses_no_null( $html, array( 'slash_zero' => 'keep' ) ) ), $allowed, wp_allowed_protocols() ); - } + if ( ! isset( $kept[ $name ] ) || ! is_string( $src ) || 'https' !== wp_parse_url( $src, PHP_URL_SCHEME ) ) { + continue; + } - if ( ! preg_match( '#<(iframe|img|video|audio)\b#i', $html ) && '' === trim( wp_strip_all_tags( $html ) ) ) { - return ''; + // A tag of our own, so only the attributes listed above come along. + $tag = new \WP_HTML_Tag_Processor( 'IFRAME' === $name ? '' : '' ); + $tag->next_tag(); + $tag->set_attribute( 'src', esc_url_raw( $src ) ); + foreach ( $kept[ $name ] as $attribute ) { + $value = $sent->get_attribute( $attribute ); + if ( null !== $value ) { + $tag->set_attribute( $attribute, $value ); + } + } + + return $tag->get_updated_html(); } - return $html; + return ''; } /** From 455c7b79f84aed37ef09bf4e7c60e0295a5f9fc3 Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Thu, 8 Oct 2026 21:16:39 +0200 Subject: [PATCH 5/6] Comments: Check the escaped embed src before rebuilding the tag --- projects/packages/comments/src/editor/class-embeds.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/packages/comments/src/editor/class-embeds.php b/projects/packages/comments/src/editor/class-embeds.php index 1319aa13150..8aa59fbd49b 100644 --- a/projects/packages/comments/src/editor/class-embeds.php +++ b/projects/packages/comments/src/editor/class-embeds.php @@ -216,16 +216,16 @@ public static function sanitize_html( $html ) { while ( $sent->next_tag() ) { $name = $sent->get_tag(); $src = $sent->get_attribute( 'src' ); - $src = is_string( $src ) && str_starts_with( $src, '//' ) ? 'https:' . $src : $src; + $src = is_string( $src ) ? esc_url_raw( str_starts_with( $src, '//' ) ? 'https:' . $src : $src ) : ''; - if ( ! isset( $kept[ $name ] ) || ! is_string( $src ) || 'https' !== wp_parse_url( $src, PHP_URL_SCHEME ) ) { + if ( ! isset( $kept[ $name ] ) || 'https' !== wp_parse_url( $src, PHP_URL_SCHEME ) ) { continue; } // A tag of our own, so only the attributes listed above come along. $tag = new \WP_HTML_Tag_Processor( 'IFRAME' === $name ? '' : '' ); $tag->next_tag(); - $tag->set_attribute( 'src', esc_url_raw( $src ) ); + $tag->set_attribute( 'src', $src ); foreach ( $kept[ $name ] as $attribute ) { $value = $sent->get_attribute( $attribute ); if ( null !== $value ) { From 5c7deec550fd31a0d4dab996d1f2bb2e4505faaa Mon Sep 17 00:00:00 2001 From: Tony Arcangelini Date: Thu, 8 Oct 2026 21:28:28 +0200 Subject: [PATCH 6/6] Comments: Fix Phan nullable index in embed sanitizer --- projects/packages/comments/src/editor/class-embeds.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/comments/src/editor/class-embeds.php b/projects/packages/comments/src/editor/class-embeds.php index 8aa59fbd49b..77c8c9b7175 100644 --- a/projects/packages/comments/src/editor/class-embeds.php +++ b/projects/packages/comments/src/editor/class-embeds.php @@ -214,7 +214,7 @@ public static function sanitize_html( $html ) { $sent = new \WP_HTML_Tag_Processor( $html ); while ( $sent->next_tag() ) { - $name = $sent->get_tag(); + $name = (string) $sent->get_tag(); $src = $sent->get_attribute( 'src' ); $src = is_string( $src ) ? esc_url_raw( str_starts_with( $src, '//' ) ? 'https:' . $src : $src ) : '';