From 827f41174de2be5fed06b5e9a6514d99826892bf Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 13:56:43 -0400 Subject: [PATCH] Media: Send Document-Isolation-Policy on the site editor's site preview iframe. The site editor embeds the front end of the site in a same-origin `?wp_site_preview=1` iframe. The editor parent document already sends Document-Isolation-Policy, but the iframed front end does not, so the browser places the two in different agent clusters and the editor loses access to the iframe's `contentDocument`. Add `wp_send_site_preview_isolation_header()`, hooked on `send_headers`, to send the same DIP header on genuine site preview requests (`?wp_site_preview=1` + `edit_theme_options`). Mirrors the gating used by the existing `wp_initialize_site_preview_hooks()` so the header is only sent for real site preview frames. Extract the existing Chromium-137-plus check into a new `wp_should_use_document_isolation_policy()` helper and call it from both `wp_start_cross_origin_isolation_output_buffer()` and the new site preview callback so the browser-support check lives in one place. The helper introduces a new `wp_use_document_isolation_policy` filter that lets site owners force-enable or force-disable DIP independent of the user agent. Backport of WordPress/gutenberg#78404. --- src/wp-includes/default-filters.php | 1 + src/wp-includes/media.php | 64 ++++++- .../tests/media/wpCrossOriginIsolation.php | 174 +++++++++++++++++- 3 files changed, 232 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index cf895eb748dbe..01775d3768b9c 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -691,6 +691,7 @@ add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'send_headers', 'wp_send_site_preview_isolation_header' ); // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); add_filter( 'nav_menu_css_class', 'wp_nav_menu_remove_menu_item_has_children_class', 10, 4 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index d318a275a9607..bbb98471c3335 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6566,6 +6566,66 @@ function wp_set_up_cross_origin_isolation(): void { wp_start_cross_origin_isolation_output_buffer(); } +/** + * Determines whether Document-Isolation-Policy should be used for cross-origin isolation. + * + * DIP is only available in Chromium 137+. The result can be overridden via the + * `wp_use_document_isolation_policy` filter. + * + * @since 7.1.0 + * + * @return bool Whether DIP is supported and should be used. + */ +function wp_should_use_document_isolation_policy(): bool { + $chromium_version = wp_get_chromium_major_version(); + + /** + * Filters whether to use Document-Isolation-Policy for cross-origin isolation. + * + * Document-Isolation-Policy provides per-document cross-origin isolation + * without affecting other iframes on the page, avoiding breakage of plugins + * whose iframes lose credentials/DOM access. + * + * @since 7.1.0 + * + * @param bool $use_dip Whether DIP is supported and should be used. + */ + return (bool) apply_filters( + 'wp_use_document_isolation_policy', + null !== $chromium_version && $chromium_version >= 137 + ); +} + +/** + * Sends the Document-Isolation-Policy header on the site editor's site preview iframe. + * + * The site editor embeds the front end of the site in a same-origin + * `?wp_site_preview=1` iframe. Sending the same DIP header on that frame keeps + * it in the editor's browsing agent cluster, so the editor can still reach the + * iframe's `contentDocument`. Without this, the parent editor and the iframed + * front end land in different agent clusters and the canvas interactions break. + * + * Mirrors the gating used by {@see wp_initialize_site_preview_hooks()} so the + * header is only sent for genuine site preview requests. + * + * @since 7.1.0 + */ +function wp_send_site_preview_isolation_header(): void { + if ( + ! isset( $_GET['wp_site_preview'] ) || + 1 !== (int) $_GET['wp_site_preview'] || + ! current_user_can( 'edit_theme_options' ) + ) { + return; + } + + if ( ! wp_should_use_document_isolation_policy() ) { + return; + } + + header( 'Document-Isolation-Policy: isolate-and-credentialless' ); +} + /** * Sends the Document-Isolation-Policy header for cross-origin isolation. * @@ -6574,9 +6634,7 @@ function wp_set_up_cross_origin_isolation(): void { * @since 7.1.0 */ function wp_start_cross_origin_isolation_output_buffer(): void { - $chromium_version = wp_get_chromium_major_version(); - - if ( null === $chromium_version || $chromium_version < 137 ) { + if ( ! wp_should_use_document_isolation_policy() ) { return; } diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index 3ec4231d5bede..c6aaa309b4d63 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -6,6 +6,8 @@ * @group media * @covers ::wp_set_up_cross_origin_isolation * @covers ::wp_start_cross_origin_isolation_output_buffer + * @covers ::wp_should_use_document_isolation_policy + * @covers ::wp_send_site_preview_isolation_header * @covers ::wp_is_client_side_media_processing_enabled */ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { @@ -30,12 +32,18 @@ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { */ private ?string $original_get_action; + /** + * Original $_GET['wp_site_preview'] value. + */ + private $original_get_site_preview; + public function set_up() { parent::set_up(); - $this->original_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null; - $this->original_http_host = $_SERVER['HTTP_HOST'] ?? null; - $this->original_https = $_SERVER['HTTPS'] ?? null; - $this->original_get_action = $_GET['action'] ?? null; + $this->original_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null; + $this->original_http_host = $_SERVER['HTTP_HOST'] ?? null; + $this->original_https = $_SERVER['HTTPS'] ?? null; + $this->original_get_action = $_GET['action'] ?? null; + $this->original_get_site_preview = $_GET['wp_site_preview'] ?? null; } public function tear_down() { @@ -63,12 +71,19 @@ public function tear_down() { $_GET['action'] = $this->original_get_action; } + if ( null === $this->original_get_site_preview ) { + unset( $_GET['wp_site_preview'] ); + } else { + $_GET['wp_site_preview'] = $this->original_get_site_preview; + } + // Clean up any output buffers started during tests. while ( ob_get_level() > 1 ) { ob_end_clean(); } remove_all_filters( 'wp_client_side_media_processing_enabled' ); + remove_all_filters( 'wp_use_document_isolation_policy' ); parent::tear_down(); } @@ -362,4 +377,155 @@ public function test_output_buffer_handles_mixed_tags() { // Script and audio should have crossorigin. $this->assertSame( 2, substr_count( $output, 'crossorigin="anonymous"' ), 'Script and audio should both get crossorigin, but not img.' ); } + + /** + * The DIP-supported helper returns true for Chromium 137+. + * + * @ticket 64766 + */ + public function test_should_use_document_isolation_policy_chromium_137() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; + + $this->assertTrue( wp_should_use_document_isolation_policy() ); + } + + /** + * The DIP-supported helper returns false for Chromium below 137. + * + * @ticket 64766 + */ + public function test_should_use_document_isolation_policy_chromium_136() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'; + + $this->assertFalse( wp_should_use_document_isolation_policy() ); + } + + /** + * The DIP-supported helper returns false for non-Chromium browsers. + * + * @ticket 64766 + */ + public function test_should_use_document_isolation_policy_firefox() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0'; + + $this->assertFalse( wp_should_use_document_isolation_policy() ); + } + + /** + * The filter can force-disable DIP on otherwise supported browsers. + * + * @ticket 64766 + */ + public function test_should_use_document_isolation_policy_filter_disables() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; + + add_filter( 'wp_use_document_isolation_policy', '__return_false' ); + + $this->assertFalse( wp_should_use_document_isolation_policy() ); + } + + /** + * The filter can force-enable DIP on unsupported browsers. + * + * @ticket 64766 + */ + public function test_should_use_document_isolation_policy_filter_enables() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0'; + + add_filter( 'wp_use_document_isolation_policy', '__return_true' ); + + $this->assertTrue( wp_should_use_document_isolation_policy() ); + } + + /** + * The site preview header callback no-ops when the request is not a + * site preview request. + * + * @ticket 64766 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_send_site_preview_isolation_header_returns_early_without_query_param() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; + unset( $_GET['wp_site_preview'] ); + + wp_send_site_preview_isolation_header(); + + $this->assertFalse( + in_array( 'Document-Isolation-Policy: isolate-and-credentialless', headers_list(), true ), + 'No DIP header should be sent without the wp_site_preview query parameter.' + ); + } + + /** + * The site preview header callback requires the edit_theme_options + * capability. + * + * @ticket 64766 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_send_site_preview_isolation_header_requires_edit_theme_options() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; + $_GET['wp_site_preview'] = 1; + + $author_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $author_id ); + + wp_send_site_preview_isolation_header(); + + $this->assertFalse( + in_array( 'Document-Isolation-Policy: isolate-and-credentialless', headers_list(), true ), + 'No DIP header should be sent for users who cannot edit_theme_options.' + ); + } + + /** + * The site preview header callback skips browsers without DIP support. + * + * @ticket 64766 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_send_site_preview_isolation_header_skips_unsupported_browser() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0'; + $_GET['wp_site_preview'] = 1; + + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + wp_send_site_preview_isolation_header(); + + $this->assertFalse( + in_array( 'Document-Isolation-Policy: isolate-and-credentialless', headers_list(), true ), + 'No DIP header should be sent to non-Chromium browsers.' + ); + } + + /** + * The site preview header callback sends the DIP header on a genuine + * site preview request from an authorized user in a supported browser. + * + * @ticket 64766 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_send_site_preview_isolation_header_sends_header() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; + $_GET['wp_site_preview'] = 1; + + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + wp_send_site_preview_isolation_header(); + + $this->assertTrue( + in_array( 'Document-Isolation-Policy: isolate-and-credentialless', headers_list(), true ), + 'DIP header should be sent on a valid site preview request.' + ); + } }