diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index efb82399ed688..bfd2e58487429 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' ); + $chromium_version = wp_get_chromium_major_version(); + + if ( null !== $chromium_version && $chromium_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_chromium_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 (Chromium 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() ) { @@ -6439,26 +6463,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; + $chromium_version = wp_get_chromium_major_version(); - $coep = $is_safari ? 'require-corp' : 'credentialless'; + if ( null === $chromium_version || $chromium_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/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php new file mode 100644 index 0000000000000..31f2e85975ee0 --- /dev/null +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -0,0 +1,152 @@ +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(); + } + + remove_all_filters( 'wp_client_side_media_processing_enabled' ); + parent::tear_down(); + } + + /** + * @ticket 64766 + */ + 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 ); + } + + /** + * @ticket 64766 + */ + 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 ); + } + + /** + * This test must run in a separate process because the output buffer + * callback sends HTTP headers via header(), which would fail in the + * main PHPUnit process where output has already started. + * + * @ticket 64766 + * + * @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(); + } + + /** + * @ticket 64766 + */ + 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.' ); + } + + /** + * @ticket 64766 + */ + 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.' ); + } + + /** + * @ticket 64766 + */ + 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.' ); + } + + /** + * This test must run in a separate process because the output buffer + * callback sends HTTP headers via header(), which would fail in the + * main PHPUnit process where output has already started. + * + * @ticket 64766 + * + * @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 ); + } +} diff --git a/tests/phpunit/tests/media/wpGetChromiumMajorVersion.php b/tests/phpunit/tests/media/wpGetChromiumMajorVersion.php new file mode 100644 index 0000000000000..7249d9b91b665 --- /dev/null +++ b/tests/phpunit/tests/media/wpGetChromiumMajorVersion.php @@ -0,0 +1,69 @@ +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(); + } + + /** + * @ticket 64766 + */ + public function test_returns_null_when_no_user_agent() { + unset( $_SERVER['HTTP_USER_AGENT'] ); + $this->assertNull( wp_get_chromium_major_version() ); + } + + /** + * @ticket 64766 + * + * @dataProvider data_user_agents + * + * @param string $user_agent The user agent string. + * @param int|null $expected The expected Chromium major version, or null. + */ + public function test_returns_expected_version( $user_agent, $expected ) { + $_SERVER['HTTP_USER_AGENT'] = $user_agent; + $this->assertSame( $expected, wp_get_chromium_major_version() ); + } + + /** + * Data provider for test_returns_expected_version. + * + * @return array[] + */ + public function data_user_agents() { + return array( + 'empty user agent' => array( '', null ), + 'Firefox' => array( 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0', null ), + 'Safari' => array( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15', null ), + 'Chrome 137' => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', 137 ), + 'Edge 137' => array( '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', 137 ), + 'Opera (Chrome 136)' => array( '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', 136 ), + 'Chrome 100' => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36', 100 ), + ); + } +}