Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
64 changes: 61 additions & 3 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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;
}

Expand Down
174 changes: 170 additions & 4 deletions tests/phpunit/tests/media/wpCrossOriginIsolation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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.'
);
}
}
Loading