From 84902ef7caf72c56583109d5c8ae314dab9733b6 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 13:56:43 -0400 Subject: [PATCH 1/3] Media: Skip Document-Isolation-Policy on the classic-theme site preview. The site editor renders the front end of a classic theme in a same-origin iframe and must reach the iframe's `contentDocument` to neutralize its interactive elements. Document-Isolation-Policy isolates the editor into its own agent cluster, which blocks that same-origin access. Skip cross-origin isolation in `wp_set_up_cross_origin_isolation()` for the classic-theme site editor home route so the preview keeps working. Block themes and other routes are unaffected. Backport of WordPress/gutenberg#78404. --- src/wp-includes/media.php | 9 ++ .../tests/media/wpCrossOriginIsolation.php | 115 ++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index d318a275a9607..a40cd7e7768b6 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6549,6 +6549,15 @@ function wp_set_up_cross_origin_isolation(): void { return; } + /* + * Skip when rendering the classic-theme home route, which shows the site + * preview in an iframe and must reach its `contentDocument` to neutralize + * interactive elements. DIP would block that same-origin access. + */ + if ( 'site-editor' === $screen->id && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) { + return; + } + /* * Skip when a third-party page builder overrides the block editor. * DIP isolates the document into its own agent cluster, diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index 3ec4231d5bede..d0328b17dcb55 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -30,12 +30,18 @@ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { */ private ?string $original_get_action; + /** + * Original $_GET['p'] value. + */ + private ?string $original_get_p; + 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_get_p = $_GET['p'] ?? null; } public function tear_down() { @@ -63,11 +69,19 @@ public function tear_down() { $_GET['action'] = $this->original_get_action; } + if ( null === $this->original_get_p ) { + unset( $_GET['p'] ); + } else { + $_GET['p'] = $this->original_get_p; + } + // Clean up any output buffers started during tests. while ( ob_get_level() > 1 ) { ob_end_clean(); } + $GLOBALS['current_screen'] = null; + remove_all_filters( 'wp_client_side_media_processing_enabled' ); parent::tear_down(); } @@ -159,6 +173,107 @@ public function test_does_not_start_output_buffer_for_safari() { $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Safari.' ); } + /** + * The site editor home route on a classic theme skips DIP, because the + * editor renders the front end in a same-origin iframe and must reach its + * `contentDocument` to neutralize interactive elements. DIP would block + * that access. + * + * @ticket 64766 + * + * @dataProvider data_classic_theme_site_editor_home_routes + * + * @param array $get The $_GET state representing the home route. + */ + public function test_skips_cross_origin_isolation_for_classic_theme_site_editor_home( array $get ) { + $_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'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + switch_theme( 'twentytwentyone' ); + set_current_screen( 'site-editor' ); + + unset( $_GET['p'] ); + foreach ( $get as $key => $value ) { + $_GET[ $key ] = $value; + } + + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after, 'DIP should be skipped on the classic-theme site editor home route.' ); + } + + /** + * Data provider for the classic-theme site editor home route. + * + * @return array[] + */ + public function data_classic_theme_site_editor_home_routes() { + return array( + 'no p query var' => array( array() ), + 'p query var is /' => array( array( 'p' => '/' ) ), + ); + } + + /** + * The site editor on a classic theme still sets up cross-origin isolation + * for routes other than the home route. + * + * @ticket 64766 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_sets_up_cross_origin_isolation_for_classic_theme_site_editor_non_home_route() { + $_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'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + switch_theme( 'twentytwentyone' ); + set_current_screen( 'site-editor' ); + + $_GET['p'] = '/page/about'; + + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before + 1, $level_after, 'DIP should be set up on a non-home site editor route.' ); + + ob_end_clean(); + } + + /** + * The site editor on a block theme always sets up cross-origin isolation, + * including on the home route, because block themes do not render the + * classic site preview iframe. + * + * @ticket 64766 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_sets_up_cross_origin_isolation_for_block_theme_site_editor_home() { + $_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'; + $_SERVER['HTTP_HOST'] = 'localhost'; + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + switch_theme( 'twentytwentyfour' ); + set_current_screen( 'site-editor' ); + + unset( $_GET['p'] ); + + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before + 1, $level_after, 'DIP should be set up on the block-theme site editor home route.' ); + + ob_end_clean(); + } + /** * @ticket 64803 */ From eabd2c2e89b8123234d4f8d97bed3709e034315f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 3 Jun 2026 10:16:34 +0200 Subject: [PATCH 2/3] Update @ticket references to 65399 for the classic-theme site preview tests --- tests/phpunit/tests/media/wpCrossOriginIsolation.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index d0328b17dcb55..3ba8aadd9c6c0 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -179,7 +179,7 @@ public function test_does_not_start_output_buffer_for_safari() { * `contentDocument` to neutralize interactive elements. DIP would block * that access. * - * @ticket 64766 + * @ticket 65399 * * @dataProvider data_classic_theme_site_editor_home_routes * @@ -221,7 +221,7 @@ public function data_classic_theme_site_editor_home_routes() { * The site editor on a classic theme still sets up cross-origin isolation * for routes other than the home route. * - * @ticket 64766 + * @ticket 65399 * * @runInSeparateProcess * @preserveGlobalState disabled @@ -250,7 +250,7 @@ public function test_sets_up_cross_origin_isolation_for_classic_theme_site_edito * including on the home route, because block themes do not render the * classic site preview iframe. * - * @ticket 64766 + * @ticket 65399 * * @runInSeparateProcess * @preserveGlobalState disabled From 5dc0e17401893fc142a3b34dc2ef4282c9f55cd8 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 27 Jun 2026 19:57:17 -0700 Subject: [PATCH 3/3] Media: Key the site-preview DIP skip off $pagenow instead of the screen The classic-theme site-preview guard checked $screen->id, which is only available because the header set-up currently runs on the load-{screen}.php hooks. Keying off $pagenow instead keeps the guard working if that set-up is moved to an earlier hook such as admin_init, where get_current_screen() is not yet populated. This matches the Gutenberg source (PR #76662, #78404). Update the tests to set $pagenow alongside the current screen, with save/restore in setUp/tearDown. --- src/wp-includes/media.php | 9 ++++++++- .../tests/media/wpCrossOriginIsolation.php | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index a40cd7e7768b6..c0aeec47d83fa 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6553,8 +6553,15 @@ function wp_set_up_cross_origin_isolation(): void { * Skip when rendering the classic-theme home route, which shows the site * preview in an iframe and must reach its `contentDocument` to neutralize * interactive elements. DIP would block that same-origin access. + * + * Keyed off $pagenow rather than the current screen so the guard keeps + * working if the header set-up is ever moved to an earlier hook (such as + * admin_init) where the screen is not yet available. */ - if ( 'site-editor' === $screen->id && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) { + global $pagenow; + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( 'site-editor.php' === $pagenow && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) { return; } diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index 3ba8aadd9c6c0..ce6432aa8894c 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -35,6 +35,11 @@ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { */ private ?string $original_get_p; + /** + * Original $pagenow value. + */ + private ?string $original_pagenow; + public function set_up() { parent::set_up(); $this->original_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null; @@ -42,6 +47,7 @@ public function set_up() { $this->original_https = $_SERVER['HTTPS'] ?? null; $this->original_get_action = $_GET['action'] ?? null; $this->original_get_p = $_GET['p'] ?? null; + $this->original_pagenow = $GLOBALS['pagenow'] ?? null; } public function tear_down() { @@ -80,6 +86,12 @@ public function tear_down() { ob_end_clean(); } + if ( null === $this->original_pagenow ) { + unset( $GLOBALS['pagenow'] ); + } else { + $GLOBALS['pagenow'] = $this->original_pagenow; + } + $GLOBALS['current_screen'] = null; remove_all_filters( 'wp_client_side_media_processing_enabled' ); @@ -192,6 +204,7 @@ public function test_skips_cross_origin_isolation_for_classic_theme_site_editor_ wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); switch_theme( 'twentytwentyone' ); set_current_screen( 'site-editor' ); + $GLOBALS['pagenow'] = 'site-editor.php'; unset( $_GET['p'] ); foreach ( $get as $key => $value ) { @@ -233,6 +246,7 @@ public function test_sets_up_cross_origin_isolation_for_classic_theme_site_edito wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); switch_theme( 'twentytwentyone' ); set_current_screen( 'site-editor' ); + $GLOBALS['pagenow'] = 'site-editor.php'; $_GET['p'] = '/page/about'; @@ -262,6 +276,7 @@ public function test_sets_up_cross_origin_isolation_for_block_theme_site_editor_ wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); switch_theme( 'twentytwentyfour' ); set_current_screen( 'site-editor' ); + $GLOBALS['pagenow'] = 'site-editor.php'; unset( $_GET['p'] );