diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index efb82399ed688..1307ce3f0b115 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6393,6 +6393,12 @@ function wp_set_client_side_media_processing_flag(): void { wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' ); + $chrome_version = wp_get_chrome_major_version(); + + if ( null !== $chrome_version && $chrome_version >= 137 ) { + wp_add_inline_script( 'wp-block-editor', 'window.__documentIsolationPolicy = true', 'before' ); + } + /* * Register the @wordpress/vips/worker script module as a dynamic dependency * of the wp-upload-media classic script. This ensures it is included in the @@ -6405,15 +6411,33 @@ function wp_set_client_side_media_processing_flag(): void { ); } +/** + * Returns the major Chrome/Chromium version from the current request's User-Agent. + * + * Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave). + * + * @since 7.0.0 + * + * @return int|null The major Chrome version, or null if not a Chromium browser. + */ +function wp_get_chrome_major_version(): ?int { + if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) { + return null; + } + if ( preg_match( '/Chrome\/(\d+)/', $_SERVER['HTTP_USER_AGENT'], $matches ) ) { + return (int) $matches[1]; + } + return null; +} + /** * Enables cross-origin isolation in the block editor. * * Required for enabling SharedArrayBuffer for WebAssembly-based - * media processing in the editor. + * media processing in the editor. Uses Document-Isolation-Policy + * on supported browsers (Chrome 137+). * * @since 7.0.0 - * - * @link https://web.dev/coop-coep/ */ function wp_set_up_cross_origin_isolation(): void { if ( ! wp_is_client_side_media_processing_enabled() ) { @@ -6430,6 +6454,14 @@ function wp_set_up_cross_origin_isolation(): void { return; } + // Skip when a third-party page builder overrides the block editor. + // DIP isolates the document into its own agent cluster, + // which blocks same-origin iframe access that these editors rely on. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) { + return; + } + // Cross-origin isolation is not needed if users can't upload files anyway. if ( ! current_user_can( 'upload_files' ) ) { return; @@ -6439,26 +6471,22 @@ function wp_set_up_cross_origin_isolation(): void { } /** - * Starts an output buffer to send cross-origin isolation headers. + * Sends the Document-Isolation-Policy header for cross-origin isolation. * - * Sends headers and uses an output buffer to add crossorigin="anonymous" - * attributes where needed. + * Uses an output buffer to add crossorigin="anonymous" where needed. * * @since 7.0.0 - * - * @link https://web.dev/coop-coep/ - * - * @global bool $is_safari */ function wp_start_cross_origin_isolation_output_buffer(): void { - global $is_safari; + $chrome_version = wp_get_chrome_major_version(); - $coep = $is_safari ? 'require-corp' : 'credentialless'; + if ( null === $chrome_version || $chrome_version < 137 ) { + return; + } ob_start( - static function ( string $output ) use ( $coep ): string { - header( 'Cross-Origin-Opener-Policy: same-origin' ); - header( "Cross-Origin-Embedder-Policy: $coep" ); + static function ( string $output ): string { + header( 'Document-Isolation-Policy: isolate-and-credentialless' ); return wp_add_crossorigin_attributes( $output ); } diff --git a/tests/phpunit/tests/media/wpGetChromeMajorVersion.php b/tests/phpunit/tests/media/wpGetChromeMajorVersion.php new file mode 100644 index 0000000000000..ba4ee55923586 --- /dev/null +++ b/tests/phpunit/tests/media/wpGetChromeMajorVersion.php @@ -0,0 +1,71 @@ +original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null; + } + + public function tear_down() { + if ( null === $this->original_user_agent ) { + unset( $_SERVER['HTTP_USER_AGENT'] ); + } else { + $_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent; + } + parent::tear_down(); + } + + public function test_returns_null_when_no_user_agent() { + unset( $_SERVER['HTTP_USER_AGENT'] ); + $this->assertNull( wp_get_chrome_major_version() ); + } + + public function test_returns_null_for_empty_user_agent() { + $_SERVER['HTTP_USER_AGENT'] = ''; + $this->assertNull( wp_get_chrome_major_version() ); + } + + public function test_returns_null_for_firefox() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0'; + $this->assertNull( wp_get_chrome_major_version() ); + } + + public function test_returns_null_for_safari() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'; + $this->assertNull( wp_get_chrome_major_version() ); + } + + public function test_returns_version_for_chrome() { + $_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->assertSame( 137, wp_get_chrome_major_version() ); + } + + public function test_returns_version_for_edge() { + $_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 Edg/137.0.0.0'; + $this->assertSame( 137, wp_get_chrome_major_version() ); + } + + public function test_returns_version_for_opera() { + $_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 OPR/122.0.0.0'; + $this->assertSame( 136, wp_get_chrome_major_version() ); + } + + public function test_returns_version_for_older_chrome() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36'; + $this->assertSame( 100, wp_get_chrome_major_version() ); + } +} diff --git a/tests/phpunit/tests/media/wpSetUpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpSetUpCrossOriginIsolation.php new file mode 100644 index 0000000000000..4116614c2ca2d --- /dev/null +++ b/tests/phpunit/tests/media/wpSetUpCrossOriginIsolation.php @@ -0,0 +1,71 @@ +original_get = $_GET; + } + + public function tear_down() { + $_GET = $this->original_get; + remove_all_filters( 'wp_client_side_media_processing_enabled' ); + parent::tear_down(); + } + + public function test_returns_early_when_client_side_processing_disabled() { + add_filter( 'wp_client_side_media_processing_enabled', '__return_false' ); + + // Should not error or start an output buffer. + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after ); + } + + public function test_returns_early_when_no_screen() { + // No screen is set, so it should return early. + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after ); + } + + public function test_skips_for_third_party_editor_action() { + $_GET['action'] = 'third_party_editor'; + + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after, 'Should skip when action is not "edit".' ); + } + + public function test_does_not_skip_for_edit_action() { + $_GET['action'] = 'edit'; + + // Still won't start the buffer because no screen is set, + // but confirms the action check doesn't block 'edit'. + $level_before = ob_get_level(); + wp_set_up_cross_origin_isolation(); + $level_after = ob_get_level(); + + // Returns early at the screen check, not the action check. + $this->assertSame( $level_before, $level_after ); + } +} diff --git a/tests/phpunit/tests/media/wpStartCrossOriginIsolationOutputBuffer.php b/tests/phpunit/tests/media/wpStartCrossOriginIsolationOutputBuffer.php new file mode 100644 index 0000000000000..2d318927c9581 --- /dev/null +++ b/tests/phpunit/tests/media/wpStartCrossOriginIsolationOutputBuffer.php @@ -0,0 +1,103 @@ +original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null; + } + + public function tear_down() { + if ( null === $this->original_user_agent ) { + unset( $_SERVER['HTTP_USER_AGENT'] ); + } else { + $_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent; + } + + // Clean up any output buffers started during tests. + while ( ob_get_level() > 1 ) { + ob_end_clean(); + } + + parent::tear_down(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_starts_output_buffer_for_chrome_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'; + + $level_before = ob_get_level(); + wp_start_cross_origin_isolation_output_buffer(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before + 1, $level_after, 'Output buffer should be started for Chrome 137.' ); + + ob_end_clean(); + } + + public function test_does_not_start_output_buffer_for_chrome_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'; + + $level_before = ob_get_level(); + wp_start_cross_origin_isolation_output_buffer(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Chrome < 137.' ); + } + + public function test_does_not_start_output_buffer_for_firefox() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0'; + + $level_before = ob_get_level(); + wp_start_cross_origin_isolation_output_buffer(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Firefox.' ); + } + + public function test_does_not_start_output_buffer_for_safari() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'; + + $level_before = ob_get_level(); + wp_start_cross_origin_isolation_output_buffer(); + $level_after = ob_get_level(); + + $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Safari.' ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_output_buffer_adds_crossorigin_attributes() { + $_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'; + + // Start an outer buffer to capture the callback-processed output. + ob_start(); + + wp_start_cross_origin_isolation_output_buffer(); + echo ''; + + // Flush the inner buffer to trigger the callback, sending processed output to the outer buffer. + ob_end_flush(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'crossorigin="anonymous"', $output ); + } +}