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 000000000000..f002e4fbe899 --- /dev/null +++ b/projects/packages/comments/changelog/fix-comments-youtube-embed-shortcode @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +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 95359e582620..77c8c9b71751 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,35 +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. - $html = (string) preg_replace( '#]*>.*?(?:|$)#is', '', $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' ), ); - $html = wp_kses( $html, $allowed ); + $sent = new \WP_HTML_Tag_Processor( $html ); + while ( $sent->next_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 ) : ''; - if ( ! preg_match( '#<(iframe|img|video|audio)\b#i', $html ) && '' === trim( wp_strip_all_tags( $html ) ) ) { - return ''; + 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', $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 ''; } /**