Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/wp-includes/deprecated.php
Original file line number Diff line number Diff line change
Expand Up @@ -6532,3 +6532,41 @@ function wp_sanitize_script_attributes( $attributes ) {
}
return $attributes_string;
}

/**
* Starts an output buffer that sends the Document-Isolation-Policy header
* and adds crossorigin="anonymous" to cross-origin resources.
*
* The attribute injection was dropped because `isolate-and-credentialless`
* loads cross-origin resources without it, and forcing CORS mode broke
* resources served without `Access-Control-Allow-Origin`. Only the header
* is sent now.
*
* @since 7.1.0
* @deprecated 7.2.0 Use wp_send_document_isolation_policy_header() instead.
* @see wp_send_document_isolation_policy_header()
*/
function wp_start_cross_origin_isolation_output_buffer(): void {
_deprecated_function( __FUNCTION__, '7.2.0', 'wp_send_document_isolation_policy_header()' );

wp_send_document_isolation_policy_header();
}

/**
* Adds crossorigin="anonymous" to relevant tags in the given HTML string.
*
* No longer modifies the HTML. Under `Document-Isolation-Policy:
* isolate-and-credentialless` cross-origin resources load without the
* attribute, and adding it broke resources served without CORS headers.
*
* @since 7.1.0
* @deprecated 7.2.0
*
* @param string $html HTML input.
* @return string The unmodified HTML.
*/
function wp_add_crossorigin_attributes( string $html ): string {
_deprecated_function( __FUNCTION__, '7.2.0' );

return $html;
}
50 changes: 0 additions & 50 deletions src/wp-includes/media-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,6 @@ class="wp-video-shortcode {{ classes.join( ' ' ) }}"
function wp_print_media_templates() {
$class = 'media-modal wp-core-ui';

$is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled();

if ( $is_cross_origin_isolation_enabled ) {
ob_start();
}

$alt_text_description = sprintf(
/* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */
__( '<a href="%1$s" %2$s>Learn how to describe the purpose of the image%3$s</a>. Leave empty if the image is purely decorative.' ),
Expand Down Expand Up @@ -1596,48 +1590,4 @@ function wp_print_media_templates() {
* @since 3.5.0
*/
do_action( 'print_media_templates' );

if ( $is_cross_origin_isolation_enabled ) {
$html = (string) ob_get_clean();

/*
* The media templates are inside <script type="text/html"> tags,
* whose content is treated as raw text by the HTML Tag Processor.
* Extract each script block's content, process it separately,
* then reassemble the full output.
*/
$script_processor = new WP_HTML_Tag_Processor( $html );
while ( $script_processor->next_tag( 'SCRIPT' ) ) {
if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {
continue;
}
/*
* Unlike wp_add_crossorigin_attributes(), this does not check whether
* URLs are actually cross-origin. Media templates use Underscore.js
* template expressions (e.g. {{ data.url }}) as placeholder URLs,
* so actual URLs are not available at parse time.
* The crossorigin attribute is added unconditionally to all relevant
* media tags to ensure cross-origin isolation works regardless of
* the final URL value at render time.
*
* IMG is intentionally excluded, matching wp_add_crossorigin_attributes().
* Under Document-Isolation-Policy: isolate-and-credentialless the browser
* loads cross-origin images in credentialless mode without CORS headers,
* so adding crossorigin="anonymous" would force a CORS request and break
* previews of images served without Access-Control-Allow-Origin headers.
*/
$template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );
while ( $template_processor->next_tag() ) {
if (
in_array( $template_processor->get_tag(), array( 'AUDIO', 'VIDEO' ), true )
&& ! is_string( $template_processor->get_attribute( 'crossorigin' ) )
) {
$template_processor->set_attribute( 'crossorigin', 'anonymous' );
}
}
$script_processor->set_modifiable_text( $template_processor->get_updated_html() );
}

echo $script_processor->get_updated_html();
}
}
116 changes: 23 additions & 93 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -6646,20 +6646,23 @@ function wp_get_chromium_major_version(): ?int {
* same-origin iframe access that these editors rely on.
*
* @since 7.1.0
* @since 7.2.0 Returns whether the header was sent.
*
* @return bool Whether the Document-Isolation-Policy header was sent.
*/
function wp_set_up_cross_origin_isolation(): void {
function wp_set_up_cross_origin_isolation(): bool {
if ( ! wp_is_client_side_media_processing_enabled() ) {
return;
return false;
}

$screen = get_current_screen();

if ( ! $screen ) {
return;
return false;
}

if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
return;
return false;
}

/*
Expand All @@ -6675,7 +6678,7 @@ function wp_set_up_cross_origin_isolation(): void {

// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'site-editor.php' === $pagenow && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) {
return;
return false;
}

/*
Expand All @@ -6684,112 +6687,39 @@ function wp_set_up_cross_origin_isolation(): void {
* which blocks same-origin iframe access that these editors rely on.
*/
if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
return;
return false;
}

// Cross-origin isolation is not needed if users can't upload files anyway.
if ( ! current_user_can( 'upload_files' ) ) {
return;
return false;
}

wp_start_cross_origin_isolation_output_buffer();
return wp_send_document_isolation_policy_header();
}

/**
* Sends the Document-Isolation-Policy header for cross-origin isolation.
*
* Uses an output buffer to add crossorigin="anonymous" where needed.
* `isolate-and-credentialless` loads cross-origin subresources without
* credentials instead of blocking them, so no `crossorigin` attribute is
* needed on scripts, styles, images, audio, or video for the page to work.
* Forcing `crossorigin="anonymous"` would turn those into CORS requests
* and break any resource served without `Access-Control-Allow-Origin`.
*
* @since 7.1.0
* @since 7.2.0
*
* @return bool Whether the header was sent.
*/
function wp_start_cross_origin_isolation_output_buffer(): void {
function wp_send_document_isolation_policy_header(): bool {
$chromium_version = wp_get_chromium_major_version();

if ( null === $chromium_version || $chromium_version < 137 ) {
return;
return false;
}

ob_start(
static function ( string $output ): string {
header( 'Document-Isolation-Policy: isolate-and-credentialless' );

return wp_add_crossorigin_attributes( $output );
}
);
}

/**
* Adds crossorigin="anonymous" to relevant tags in the given HTML string.
*
* @since 7.1.0
*
* @param string $html HTML input.
* @return string Modified HTML.
*/
function wp_add_crossorigin_attributes( string $html ): string {
$site_url = site_url();

$processor = new WP_HTML_Tag_Processor( $html );

// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
$cross_origin_tag_attributes = array(
'AUDIO' => array( 'src' ),
'LINK' => array( 'href' ),
'SCRIPT' => array( 'src' ),
'VIDEO' => array( 'src', 'poster' ),
'SOURCE' => array( 'src' ),
);
header( 'Document-Isolation-Policy: isolate-and-credentialless' );

while ( $processor->next_tag() ) {
$tag = $processor->get_tag();

if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
continue;
}
$crossorigin = $processor->get_attribute( 'crossorigin' );
if ( null !== $crossorigin ) {
continue;
}

if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
$processor->set_bookmark( 'audio-video-parent' );
}

$processor->set_bookmark( 'resume' );

$sought = false;

$is_cross_origin = false;

foreach ( $cross_origin_tag_attributes[ $tag ] as $attr ) {
$url = $processor->get_attribute( $attr );
if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
$is_cross_origin = true;
}

if ( $is_cross_origin ) {
break;
}
}

if ( $is_cross_origin ) {
if ( 'SOURCE' === $tag ) {
$sought = $processor->seek( 'audio-video-parent' );

if ( $sought ) {
$processor->set_attribute( 'crossorigin', 'anonymous' );
}
} else {
$processor->set_attribute( 'crossorigin', 'anonymous' );
}

if ( $sought ) {
$processor->seek( 'resume' );
$processor->release_bookmark( 'audio-video-parent' );
}
}
}

return $processor->get_updated_html();
return true;
}

Loading
Loading