Skip to content
Merged
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
3 changes: 3 additions & 0 deletions backport-changelog/7.2/13446.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/13446

* https://github.com/WordPress/gutenberg/pull/82614
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,15 @@ Document-Isolation-Policy: isolate-and-credentialless

This header provides per-document cross-origin isolation which is required to use the SharedArrayBuffer capability. Client-side media uses this capability to run the processing in a separate worker thread, which improves performance and avoids blocking the main thread during image processing.

The header is set via `gutenberg_start_cross_origin_isolation_output_buffer()` (in `lib/media/load.php`), which uses PHP output buffering on `load-post.php`, `load-post-new.php`, `load-site-editor.php`, and `load-widgets.php` screens. DIP is skipped on admin pages with an `action` parameter other than `edit` to avoid conflicts with page builders that rely on same-origin iframe access.
The header is sent by `gutenberg_send_document_isolation_policy_header()` (in `lib/media/load.php`) on the `load-post.php`, `load-post-new.php`, `load-site-editor.php`, and `load-widgets.php` screens. DIP is skipped on admin pages with an `action` parameter other than `edit` to avoid conflicts with page builders that rely on same-origin iframe access.

The `gutenberg_use_document_isolation_policy` filter can be used to control whether DIP is applied. Disabling DIP will also disable client-side media processing, since the WASM worker cannot run without SharedArrayBuffer.

### HTML attribute injection
### No `crossorigin` attribute injection

Cross-origin isolation requires that cross-origin resources include proper CORS attributes. WordPress handles this at two levels:
`isolate-and-credentialless` loads cross-origin subresources without credentials instead of blocking them, so scripts, styles, images, audio, and video from other origins work without a `crossorigin` attribute. WordPress therefore does not add one. Forcing `crossorigin="anonymous"` would turn each load into a CORS request, which fails for any resource served without `Access-Control-Allow-Origin` headers, such as media offloaded to a CDN or an Image block pointing at a third-party URL.

**Server-side** (PHP output buffer): The `wp_add_crossorigin_attributes()` function uses `WP_HTML_Tag_Processor` to add `crossorigin="anonymous"` to `<audio>`, `<link>`, `<script>`, `<video>`, and `<source>` tags that load cross-origin URLs.

**Client-side** (JavaScript MutationObserver): A MutationObserver in `packages/block-editor/src/hooks/cross-origin-isolation.js` monitors the DOM for dynamically added elements and adds `crossorigin="anonymous"` attributes at runtime.

> **Note:** `<img>` is intentionally excluded from the mutated-element list. Document-Isolation-Policy doesn't require `<img>` resources to be CORS-enabled, and forcing `crossorigin="anonymous"` on cross-origin images would break previews for the common case of an Image block linking to a third-party URL without CORS headers.
The attribute is only needed when script reads the pixels of a cross-origin image or video frame back out of a canvas, and only when the origin serves CORS headers. Code with that need sets `crossOrigin` on the element it creates, optionally through the `media.crossOrigin` filter, rather than relying on page-wide injection.

### External images

Expand Down
2 changes: 1 addition & 1 deletion docs/how-to-guides/client-side-media.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Client-side media processing requires `SharedArrayBuffer` for WASM threading. Wo

### Impact on plugins

- **External scripts**: Scripts loaded from other origins will automatically get a `crossorigin="anonymous"` attribute added, handled by WordPress server-side (via HTML processing) and client-side (via a MutationObserver).
- **External resources**: Scripts, styles, images, audio, and video loaded from other origins keep working without a `crossorigin` attribute. `isolate-and-credentialless` loads them without credentials rather than blocking them, so resources behind a signed cookie or a logged-in session are the one case that breaks. WordPress does not add `crossorigin="anonymous"` to them, since that would force a CORS request that fails for hosts without `Access-Control-Allow-Origin` headers.
- **Third-party page builders**: DIP is skipped on admin pages with an `action` parameter other than `edit`, to avoid conflicts with page builders that rely on same-origin iframe access.
- **External images**: Importing an external image into the media library is handled server-side — the editor sends the image URL to `POST /wp/v2/media` (the `url` parameter) and the server downloads and sideloads the file. A browser cross-origin fetch would be subject to CORS and fails in a `credentialless` isolated document, so plugins importing remote media should use the same server-side path (exposed to the editor as the `mediaSideloadFromUrl` setting) rather than `fetch()`ing image bytes in the browser.

Expand Down
2 changes: 1 addition & 1 deletion lib/media/docs/client-side-media-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ WASM-based image optimization requires `SharedArrayBuffer` support, which in tur

This is achieved using the [`Document-Isolation-Policy`](https://github.com/WICG/document-isolation-policy) header, which provides per-document cross-origin isolation without affecting other iframes on the page. This avoids the breakage that the older `Cross-Origin-Embedder-Policy` / `Cross-Origin-Opener-Policy` headers caused for third-party plugins and embeds.

Once the page is served with this header, `SharedArrayBuffer` will be available in the browser, and WASM-based image optimization will work as expected. All embedded resources (e.g., images, scripts) are served with `crossorigin="anonymous"` to ensure cross-origin isolation is maintained.
Once the page is served with this header, `SharedArrayBuffer` will be available in the browser, and WASM-based image optimization will work as expected. Embedded resources from other origins (images, scripts, styles, audio, video) keep loading without a `crossorigin` attribute, because `isolate-and-credentialless` loads them without credentials instead of blocking them.

### Troubleshooting

Expand Down
126 changes: 32 additions & 94 deletions lib/media/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ function gutenberg_set_up_cross_origin_isolation() {
return;
}

gutenberg_start_cross_origin_isolation_output_buffer();
gutenberg_send_document_isolation_policy_header();
}

add_action( 'load-post.php', 'gutenberg_set_up_cross_origin_isolation' );
Expand All @@ -365,9 +365,16 @@ function gutenberg_set_up_cross_origin_isolation() {
/**
* 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`,
* such as media offloaded to a CDN.
*
* @return bool Whether the header was sent.
*/
function gutenberg_start_cross_origin_isolation_output_buffer(): void {
function gutenberg_send_document_isolation_policy_header(): bool {
$chromium_version = gutenberg_get_chromium_major_version();

/**
Expand All @@ -387,102 +394,30 @@ function gutenberg_start_cross_origin_isolation_output_buffer(): void {
);

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

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

return gutenberg_add_crossorigin_attributes( $output );
}
);
return true;
}

/**
* Adds crossorigin="anonymous" to relevant tags in the given HTML string.
* Removes `crossorigin` attributes from the printed media templates.
*
* @param string $html HTML input.
*
* @return string Modified HTML.
*/
function gutenberg_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.
$tags = array(
'AUDIO' => 'src',
'LINK' => 'href',
'SCRIPT' => 'src',
'VIDEO' => 'src',
'SOURCE' => 'src',
);

$tag_names = array_keys( $tags );

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

if ( ! in_array( $tag, $tag_names, true ) ) {
continue;
}

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

$processor->set_bookmark( 'resume' );

$sought = false;

$crossorigin = $processor->get_attribute( 'crossorigin' );

$url = $processor->get_attribute( $tags[ $tag ] );

if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) {
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();
}

/**
* Updates `crossorigin` attributes in the printed media templates.
*
* Adds `crossorigin="anonymous"` to AUDIO and VIDEO tags inside the
* Backbone `<script type="text/html">` templates so the media modal can
* play cross-origin audio and video under cross-origin isolation. Tags
* that already have the attribute are left untouched so the output does
* not gain duplicates on WordPress versions where Core adds it itself.
*
* IMG is intentionally excluded: under
* `Document-Isolation-Policy: isolate-and-credentialless` the browser
* already loads cross-origin images in credentialless mode, so forcing
* `crossorigin="anonymous"` triggers a CORS request that breaks previews
* of images served without CORS headers, such as media offloaded to a
* CDN. See https://core.trac.wordpress.org/ticket/65673.
* WordPress 7.1 forces `crossorigin="anonymous"` onto the AUDIO and VIDEO
* tags inside the Backbone `<script type="text/html">` templates whenever
* client-side media processing is enabled. Under
* `Document-Isolation-Policy: isolate-and-credentialless` the attribute is
* not needed to play cross-origin media, and it turns the load into a CORS
* request that fails for media served without CORS headers, such as media
* offloaded to a CDN. See https://core.trac.wordpress.org/ticket/65930.
*
* @param string $html The printed media templates.
*
* @return string Modified media templates.
*/
function gutenberg_update_media_template_crossorigin_attributes( string $html ): string {
function gutenberg_remove_media_template_crossorigin_attributes( string $html ): string {
/*
* The media templates are inside <script type="text/html"> tags,
* whose content is treated as raw text by the HTML Tag Processor.
Expand All @@ -497,10 +432,10 @@ function gutenberg_update_media_template_crossorigin_attributes( string $html ):
$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' ) )
in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true )
&& 'anonymous' === $template_processor->get_attribute( 'crossorigin' )
) {
$template_processor->set_attribute( 'crossorigin', 'anonymous' );
$template_processor->remove_attribute( 'crossorigin' );
}
}
$script_processor->set_modifiable_text( $template_processor->get_updated_html() );
Expand All @@ -512,11 +447,14 @@ function gutenberg_update_media_template_crossorigin_attributes( string $html ):
/**
* Overrides templates from wp_print_media_templates with custom ones.
*
* Updates the `crossorigin` attributes on media tags so cross-origin
* audio and video can be processed under cross-origin isolation without
* breaking previews of images served without CORS headers.
* Only needed on WordPress 7.1, the one release whose
* `wp_print_media_templates()` injects `crossorigin="anonymous"` itself.
*/
function gutenberg_override_media_templates(): void {
if ( ! function_exists( 'wp_add_crossorigin_attributes' ) || function_exists( 'wp_send_document_isolation_policy_header' ) ) {
return;
}

remove_action( 'admin_footer', 'wp_print_media_templates' );
add_action(
'admin_footer',
Expand All @@ -525,7 +463,7 @@ static function (): void {
wp_print_media_templates();
$html = (string) ob_get_clean();

echo gutenberg_update_media_template_crossorigin_attributes( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo gutenberg_remove_media_template_crossorigin_attributes( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Internal

- Remove the `crossorigin` MutationObserver. Under `Document-Isolation-Policy: isolate-and-credentialless` cross-origin resources load without the attribute, and adding it broke any resource served without CORS headers ([#82614](https://github.com/WordPress/gutenberg/pull/82614)).

### New Features

- Add a text shadow block support and its control in the typography panel ([#79584](https://github.com/WordPress/gutenberg/pull/79584)).
Expand Down
73 changes: 0 additions & 73 deletions packages/block-editor/src/hooks/cross-origin-isolation.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/block-editor/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
createBlockSaveFilter,
} from './utils';
import './compat';
import './cross-origin-isolation';
import align from './align';
import background from './background';
import './lock';
Expand Down
Loading
Loading