Skip to content

Commit 9bf1cdd

Browse files
Media: Use Document-Isolation-Policy for cross-origin isolation.
Replace COEP/COOP headers with Document-Isolation-Policy (DIP) for cross-origin isolation in the block editor. DIP enables sharedBufferArray while avoiding the breakage COEP/COOP caused for third-party plugins whose iframes lost credentials and DOM access. Non supporting browsers have the client-side media feature disabled by default - falling back to the existing server side processing - to avoid a degraded editor experience. Developed in WordPress#11098 Props adamsilverstein, westonruter, manhar, swissspidy, mukesh27. Fixes #64766. git-svn-id: https://develop.svn.wordpress.org/trunk@61844 602fd350-edb4-49c9-b593-d223f7449a82
1 parent e7d40e7 commit 9bf1cdd

3 files changed

Lines changed: 256 additions & 15 deletions

File tree

src/wp-includes/media.php

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6393,6 +6393,12 @@ function wp_set_client_side_media_processing_flag(): void {
63936393

63946394
wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
63956395

6396+
$chromium_version = wp_get_chromium_major_version();
6397+
6398+
if ( null !== $chromium_version && $chromium_version >= 137 ) {
6399+
wp_add_inline_script( 'wp-block-editor', 'window.__documentIsolationPolicy = true;', 'before' );
6400+
}
6401+
63966402
/*
63976403
* Register the @wordpress/vips/worker script module as a dynamic dependency
63986404
* 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 {
64056411
);
64066412
}
64076413

6414+
/**
6415+
* Returns the major Chrome/Chromium version from the current request's User-Agent.
6416+
*
6417+
* Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave).
6418+
*
6419+
* @since 7.0.0
6420+
*
6421+
* @return int|null The major Chrome version, or null if not a Chromium browser.
6422+
*/
6423+
function wp_get_chromium_major_version(): ?int {
6424+
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
6425+
return null;
6426+
}
6427+
if ( preg_match( '#Chrome/(\d+)#', $_SERVER['HTTP_USER_AGENT'], $matches ) ) {
6428+
return (int) $matches[1];
6429+
}
6430+
return null;
6431+
}
6432+
64086433
/**
64096434
* Enables cross-origin isolation in the block editor.
64106435
*
64116436
* Required for enabling SharedArrayBuffer for WebAssembly-based
6412-
* media processing in the editor.
6437+
* media processing in the editor. Uses Document-Isolation-Policy
6438+
* on supported browsers (Chromium 137+).
64136439
*
64146440
* @since 7.0.0
6415-
*
6416-
* @link https://web.dev/coop-coep/
64176441
*/
64186442
function wp_set_up_cross_origin_isolation(): void {
64196443
if ( ! wp_is_client_side_media_processing_enabled() ) {
@@ -6439,26 +6463,22 @@ function wp_set_up_cross_origin_isolation(): void {
64396463
}
64406464

64416465
/**
6442-
* Starts an output buffer to send cross-origin isolation headers.
6466+
* Sends the Document-Isolation-Policy header for cross-origin isolation.
64436467
*
6444-
* Sends headers and uses an output buffer to add crossorigin="anonymous"
6445-
* attributes where needed.
6468+
* Uses an output buffer to add crossorigin="anonymous" where needed.
64466469
*
64476470
* @since 7.0.0
6448-
*
6449-
* @link https://web.dev/coop-coep/
6450-
*
6451-
* @global bool $is_safari
64526471
*/
64536472
function wp_start_cross_origin_isolation_output_buffer(): void {
6454-
global $is_safari;
6473+
$chromium_version = wp_get_chromium_major_version();
64556474

6456-
$coep = $is_safari ? 'require-corp' : 'credentialless';
6475+
if ( null === $chromium_version || $chromium_version < 137 ) {
6476+
return;
6477+
}
64576478

64586479
ob_start(
6459-
static function ( string $output ) use ( $coep ): string {
6460-
header( 'Cross-Origin-Opener-Policy: same-origin' );
6461-
header( "Cross-Origin-Embedder-Policy: $coep" );
6480+
static function ( string $output ): string {
6481+
header( 'Document-Isolation-Policy: isolate-and-credentialless' );
64626482

64636483
return wp_add_crossorigin_attributes( $output );
64646484
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
/**
4+
* Tests for cross-origin isolation functions.
5+
*
6+
* @group media
7+
* @covers ::wp_set_up_cross_origin_isolation
8+
* @covers ::wp_start_cross_origin_isolation_output_buffer
9+
*/
10+
class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase {
11+
12+
/**
13+
* Original HTTP_USER_AGENT value.
14+
*
15+
* @var string|null
16+
*/
17+
private $original_user_agent;
18+
19+
public function set_up() {
20+
parent::set_up();
21+
$this->original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
22+
}
23+
24+
public function tear_down() {
25+
if ( null === $this->original_user_agent ) {
26+
unset( $_SERVER['HTTP_USER_AGENT'] );
27+
} else {
28+
$_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent;
29+
}
30+
31+
// Clean up any output buffers started during tests.
32+
while ( ob_get_level() > 1 ) {
33+
ob_end_clean();
34+
}
35+
36+
remove_all_filters( 'wp_client_side_media_processing_enabled' );
37+
parent::tear_down();
38+
}
39+
40+
/**
41+
* @ticket 64766
42+
*/
43+
public function test_returns_early_when_client_side_processing_disabled() {
44+
add_filter( 'wp_client_side_media_processing_enabled', '__return_false' );
45+
46+
// Should not error or start an output buffer.
47+
$level_before = ob_get_level();
48+
wp_set_up_cross_origin_isolation();
49+
$level_after = ob_get_level();
50+
51+
$this->assertSame( $level_before, $level_after );
52+
}
53+
54+
/**
55+
* @ticket 64766
56+
*/
57+
public function test_returns_early_when_no_screen() {
58+
// No screen is set, so it should return early.
59+
$level_before = ob_get_level();
60+
wp_set_up_cross_origin_isolation();
61+
$level_after = ob_get_level();
62+
63+
$this->assertSame( $level_before, $level_after );
64+
}
65+
66+
/**
67+
* This test must run in a separate process because the output buffer
68+
* callback sends HTTP headers via header(), which would fail in the
69+
* main PHPUnit process where output has already started.
70+
*
71+
* @ticket 64766
72+
*
73+
* @runInSeparateProcess
74+
* @preserveGlobalState disabled
75+
*/
76+
public function test_starts_output_buffer_for_chrome_137() {
77+
$_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';
78+
79+
$level_before = ob_get_level();
80+
wp_start_cross_origin_isolation_output_buffer();
81+
$level_after = ob_get_level();
82+
83+
$this->assertSame( $level_before + 1, $level_after, 'Output buffer should be started for Chrome 137.' );
84+
85+
ob_end_clean();
86+
}
87+
88+
/**
89+
* @ticket 64766
90+
*/
91+
public function test_does_not_start_output_buffer_for_chrome_136() {
92+
$_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';
93+
94+
$level_before = ob_get_level();
95+
wp_start_cross_origin_isolation_output_buffer();
96+
$level_after = ob_get_level();
97+
98+
$this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Chrome < 137.' );
99+
}
100+
101+
/**
102+
* @ticket 64766
103+
*/
104+
public function test_does_not_start_output_buffer_for_firefox() {
105+
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0';
106+
107+
$level_before = ob_get_level();
108+
wp_start_cross_origin_isolation_output_buffer();
109+
$level_after = ob_get_level();
110+
111+
$this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Firefox.' );
112+
}
113+
114+
/**
115+
* @ticket 64766
116+
*/
117+
public function test_does_not_start_output_buffer_for_safari() {
118+
$_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';
119+
120+
$level_before = ob_get_level();
121+
wp_start_cross_origin_isolation_output_buffer();
122+
$level_after = ob_get_level();
123+
124+
$this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Safari.' );
125+
}
126+
127+
/**
128+
* This test must run in a separate process because the output buffer
129+
* callback sends HTTP headers via header(), which would fail in the
130+
* main PHPUnit process where output has already started.
131+
*
132+
* @ticket 64766
133+
*
134+
* @runInSeparateProcess
135+
* @preserveGlobalState disabled
136+
*/
137+
public function test_output_buffer_adds_crossorigin_attributes() {
138+
$_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';
139+
140+
// Start an outer buffer to capture the callback-processed output.
141+
ob_start();
142+
143+
wp_start_cross_origin_isolation_output_buffer();
144+
echo '<img src="https://external.example.com/image.jpg" />';
145+
146+
// Flush the inner buffer to trigger the callback, sending processed output to the outer buffer.
147+
ob_end_flush();
148+
$output = ob_get_clean();
149+
150+
$this->assertStringContainsString( 'crossorigin="anonymous"', $output );
151+
}
152+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/**
4+
* Tests for the `wp_get_chromium_major_version()` function.
5+
*
6+
* @group media
7+
* @covers ::wp_get_chromium_major_version
8+
*/
9+
class Tests_Media_wpGetChromiumMajorVersion extends WP_UnitTestCase {
10+
11+
/**
12+
* Original HTTP_USER_AGENT value.
13+
*
14+
* @var string|null
15+
*/
16+
private $original_user_agent;
17+
18+
public function set_up() {
19+
parent::set_up();
20+
$this->original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
21+
}
22+
23+
public function tear_down() {
24+
if ( null === $this->original_user_agent ) {
25+
unset( $_SERVER['HTTP_USER_AGENT'] );
26+
} else {
27+
$_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent;
28+
}
29+
parent::tear_down();
30+
}
31+
32+
/**
33+
* @ticket 64766
34+
*/
35+
public function test_returns_null_when_no_user_agent() {
36+
unset( $_SERVER['HTTP_USER_AGENT'] );
37+
$this->assertNull( wp_get_chromium_major_version() );
38+
}
39+
40+
/**
41+
* @ticket 64766
42+
*
43+
* @dataProvider data_user_agents
44+
*
45+
* @param string $user_agent The user agent string.
46+
* @param int|null $expected The expected Chromium major version, or null.
47+
*/
48+
public function test_returns_expected_version( $user_agent, $expected ) {
49+
$_SERVER['HTTP_USER_AGENT'] = $user_agent;
50+
$this->assertSame( $expected, wp_get_chromium_major_version() );
51+
}
52+
53+
/**
54+
* Data provider for test_returns_expected_version.
55+
*
56+
* @return array[]
57+
*/
58+
public function data_user_agents() {
59+
return array(
60+
'empty user agent' => array( '', null ),
61+
'Firefox' => array( 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0', null ),
62+
'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 ),
63+
'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 ),
64+
'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 ),
65+
'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 ),
66+
'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 ),
67+
);
68+
}
69+
}

0 commit comments

Comments
 (0)