Make WordPress Core

Changeset 64489


Ignore:
Timestamp:
10/18/2026 10:32:14 PM (29 hours ago)
Author:
adamsilverstein
Message:

Media: stop adding crossorigin attributes in the editor.

Fix an issue where editing a post in Chromium 137+ could end in a fatal error and a blank screen when the page loaded any cross-origin resource. wp_add_crossorigin_attributes() ran WP_HTML_Tag_Processor over the whole document inside an ob_start() display handler, where errors cannot be caught. The injected crossorigin="anonymous" was not needed in the first place: Document-Isolation-Policy: isolate-and-credentialless loads cross-origin scripts, stylesheets, audio and video without credentials instead of blocking them, and forcing a CORS request broke any host that does not send Access-Control-Allow-Origin, such as media offloaded to a CDN.

The header is now sent directly by the new wp_send_document_isolation_policy_header(), and the editor page is no longer buffered or rewritten. wp_start_cross_origin_isolation_output_buffer() and wp_add_crossorigin_attributes() are deprecated; the first only sends the header and the second returns the HTML unchanged. wp_print_media_templates() no longer adds the attribute to AUDIO and VIDEO tags. Cross-origin isolation and client-side media processing are unaffected.

Follow-up to [61844], [62048], [62819].

Props b0b3k, khokansardar, arkaprabhachowdhury, westonruter, andrewserong, algorithmsunlocks, sabernhardt, swissspidy, adamsilverstein.
Fixes #65930. See #65673.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/deprecated.php

    r64436 r64489  
    66016601        return wp_truncate_slug( $slug, $length );
    66026602}
     6603
     6604/**
     6605 * Starts an output buffer that sends the Document-Isolation-Policy header
     6606 * and adds crossorigin="anonymous" to cross-origin resources.
     6607 *
     6608 * The attribute injection was dropped because `isolate-and-credentialless`
     6609 * loads cross-origin resources without it, and forcing CORS mode broke
     6610 * resources served without `Access-Control-Allow-Origin`. Only the header
     6611 * is sent now.
     6612 *
     6613 * @since 7.1.0
     6614 * @deprecated 7.2.0 Use wp_send_document_isolation_policy_header() instead.
     6615 * @see wp_send_document_isolation_policy_header()
     6616 */
     6617function wp_start_cross_origin_isolation_output_buffer(): void {
     6618        _deprecated_function( __FUNCTION__, '7.2.0', 'wp_send_document_isolation_policy_header()' );
     6619
     6620        wp_send_document_isolation_policy_header();
     6621}
     6622
     6623/**
     6624 * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
     6625 *
     6626 * No longer modifies the HTML. Under `Document-Isolation-Policy:
     6627 * isolate-and-credentialless` cross-origin resources load without the
     6628 * attribute, and adding it broke resources served without CORS headers.
     6629 *
     6630 * @since 7.1.0
     6631 * @deprecated 7.2.0
     6632 *
     6633 * @param string $html HTML input.
     6634 * @return string The unmodified HTML.
     6635 */
     6636function wp_add_crossorigin_attributes( string $html ): string {
     6637        _deprecated_function( __FUNCTION__, '7.2.0' );
     6638
     6639        return $html;
     6640}
  • trunk/src/wp-includes/media-template.php

    r64241 r64489  
    157157        $class = 'media-modal wp-core-ui';
    158158
    159         $is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled();
    160 
    161         if ( $is_cross_origin_isolation_enabled ) {
    162                 ob_start();
    163         }
    164 
    165159        $alt_text_description = sprintf(
    166160                /* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */
    … …  
    15971591         */
    15981592        do_action( 'print_media_templates' );
    1599 
    1600         if ( $is_cross_origin_isolation_enabled ) {
    1601                 $html = (string) ob_get_clean();
    1602 
    1603                 /*
    1604                  * The media templates are inside <script type="text/html"> tags,
    1605                  * whose content is treated as raw text by the HTML Tag Processor.
    1606                  * Extract each script block's content, process it separately,
    1607                  * then reassemble the full output.
    1608                  */
    1609                 $script_processor = new WP_HTML_Tag_Processor( $html );
    1610                 while ( $script_processor->next_tag( 'SCRIPT' ) ) {
    1611                         if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {
    1612                                 continue;
    1613                         }
    1614                         /*
    1615                          * Unlike wp_add_crossorigin_attributes(), this does not check whether
    1616                          * URLs are actually cross-origin. Media templates use Underscore.js
    1617                          * template expressions (e.g. {{ data.url }}) as placeholder URLs,
    1618                          * so actual URLs are not available at parse time.
    1619                          * The crossorigin attribute is added unconditionally to all relevant
    1620                          * media tags to ensure cross-origin isolation works regardless of
    1621                          * the final URL value at render time.
    1622                          *
    1623                          * IMG is intentionally excluded, matching wp_add_crossorigin_attributes().
    1624                          * Under Document-Isolation-Policy: isolate-and-credentialless the browser
    1625                          * loads cross-origin images in credentialless mode without CORS headers,
    1626                          * so adding crossorigin="anonymous" would force a CORS request and break
    1627                          * previews of images served without Access-Control-Allow-Origin headers.
    1628                          */
    1629                         $template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );
    1630                         while ( $template_processor->next_tag() ) {
    1631                                 if (
    1632                                         in_array( $template_processor->get_tag(), array( 'AUDIO', 'VIDEO' ), true )
    1633                                         && ! is_string( $template_processor->get_attribute( 'crossorigin' ) )
    1634                                 ) {
    1635                                         $template_processor->set_attribute( 'crossorigin', 'anonymous' );
    1636                                 }
    1637                         }
    1638                         $script_processor->set_modifiable_text( $template_processor->get_updated_html() );
    1639                 }
    1640 
    1641                 echo $script_processor->get_updated_html();
    1642         }
    16431593}
  • trunk/src/wp-includes/media.php

    r64436 r64489  
    66496649 *
    66506650 * @since 7.1.0
    6651  */
    6652 function wp_set_up_cross_origin_isolation(): void {
     6651 * @since 7.2.0 Returns whether the header was sent.
     6652 *
     6653 * @return bool Whether the Document-Isolation-Policy header was sent.
     6654 */
     6655function wp_set_up_cross_origin_isolation(): bool {
    66536656        if ( ! wp_is_client_side_media_processing_enabled() ) {
    6654                 return;
     6657                return false;
    66556658        }
    66566659
    … …  
    66586661
    66596662        if ( ! $screen ) {
    6660                 return;
     6663                return false;
    66616664        }
    66626665
    66636666        if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
    6664                 return;
     6667                return false;
    66656668        }
    66666669
    … …  
    66786681        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    66796682        if ( 'site-editor.php' === $pagenow && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) {
    6680                 return;
     6683                return false;
    66816684        }
    66826685
    … …  
    66876690         */
    66886691        if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
    6689                 return;
     6692                return false;
    66906693        }
    66916694
    66926695        // Cross-origin isolation is not needed if users can't upload files anyway.
    66936696        if ( ! current_user_can( 'upload_files' ) ) {
    6694                 return;
    6695         }
    6696 
    6697         wp_start_cross_origin_isolation_output_buffer();
     6697                return false;
     6698        }
     6699
     6700        return wp_send_document_isolation_policy_header();
    66986701}
    66996702
    … …  
    67016704 * Sends the Document-Isolation-Policy header for cross-origin isolation.
    67026705 *
    6703  * Uses an output buffer to add crossorigin="anonymous" where needed.
    6704  *
    6705  * @since 7.1.0
    6706  */
    6707 function wp_start_cross_origin_isolation_output_buffer(): void {
     6706 * The `isolate-and-credentialless` directive loads cross-origin subresources
     6707 * without credentials instead of blocking them, so no `crossorigin` attribute
     6708 * is needed on scripts, styles, images, audio, or video for the page to work.
     6709 * Forcing `crossorigin="anonymous"` would turn those into CORS requests
     6710 * and break any resource served without `Access-Control-Allow-Origin`.
     6711 *
     6712 * @since 7.2.0
     6713 *
     6714 * @return bool Whether the header was sent. False if the browser does not
     6715 *              support Document-Isolation-Policy or headers were already sent.
     6716 */
     6717function wp_send_document_isolation_policy_header(): bool {
     6718        if ( headers_sent() ) {
     6719                return false;
     6720        }
     6721
    67086722        $chromium_version = wp_get_chromium_major_version();
    67096723
    67106724        if ( null === $chromium_version || $chromium_version < 137 ) {
    6711                 return;
    6712         }
    6713 
    6714         ob_start(
    6715                 static function ( string $output ): string {
    6716                         header( 'Document-Isolation-Policy: isolate-and-credentialless' );
    6717 
    6718                         return wp_add_crossorigin_attributes( $output );
    6719                 }
    6720         );
    6721 }
    6722 
    6723 /**
    6724  * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
    6725  *
    6726  * @since 7.1.0
    6727  *
    6728  * @param string $html HTML input.
    6729  * @return string Modified HTML.
    6730  */
    6731 function wp_add_crossorigin_attributes( string $html ): string {
    6732         $site_url = site_url();
    6733 
    6734         $processor = new WP_HTML_Tag_Processor( $html );
    6735 
    6736         // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
    6737         $cross_origin_tag_attributes = array(
    6738                 'AUDIO'  => array( 'src' ),
    6739                 'LINK'   => array( 'href' ),
    6740                 'SCRIPT' => array( 'src' ),
    6741                 'VIDEO'  => array( 'src', 'poster' ),
    6742                 'SOURCE' => array( 'src' ),
    6743         );
    6744 
    6745         while ( $processor->next_tag() ) {
    6746                 $tag = $processor->get_tag();
    6747 
    6748                 if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
    6749                         continue;
    6750                 }
    6751                 $crossorigin = $processor->get_attribute( 'crossorigin' );
    6752                 if ( null !== $crossorigin ) {
    6753                         continue;
    6754                 }
    6755 
    6756                 if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
    6757                         $processor->set_bookmark( 'audio-video-parent' );
    6758                 }
    6759 
    6760                 $processor->set_bookmark( 'resume' );
    6761 
    6762                 $sought = false;
    6763 
    6764                 $is_cross_origin = false;
    6765 
    6766                 foreach ( $cross_origin_tag_attributes[ $tag ] as $attr ) {
    6767                         $url = $processor->get_attribute( $attr );
    6768                         if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
    6769                                 $is_cross_origin = true;
    6770                         }
    6771 
    6772                         if ( $is_cross_origin ) {
    6773                                 break;
    6774                         }
    6775                 }
    6776 
    6777                 if ( $is_cross_origin ) {
    6778                         if ( 'SOURCE' === $tag ) {
    6779                                 $sought = $processor->seek( 'audio-video-parent' );
    6780 
    6781                                 if ( $sought ) {
    6782                                         $processor->set_attribute( 'crossorigin', 'anonymous' );
    6783                                 }
    6784                         } else {
    6785                                 $processor->set_attribute( 'crossorigin', 'anonymous' );
    6786                         }
    6787 
    6788                         if ( $sought ) {
    6789                                 $processor->seek( 'resume' );
    6790                                 $processor->release_bookmark( 'audio-video-parent' );
    6791                         }
    6792                 }
    6793         }
    6794 
    6795         return $processor->get_updated_html();
    6796 }
    6797 
     6725                return false;
     6726        }
     6727
     6728        header( 'Document-Isolation-Policy: isolate-and-credentialless' );
     6729
     6730        return true;
     6731}
     6732
  • trunk/tests/phpunit/tests/media/wpCrossOriginIsolation.php

    r62819 r64489  
    66 * @group media
    77 * @covers ::wp_set_up_cross_origin_isolation
    8  * @covers ::wp_start_cross_origin_isolation_output_buffer
     8 * @covers ::wp_send_document_isolation_policy_header
    99 * @covers ::wp_is_client_side_media_processing_enabled
    1010 */
    … …  
    8282                }
    8383
    84                 // Clean up any output buffers started during tests.
    85                 while ( ob_get_level() > 1 ) {
    86                         ob_end_clean();
    87                 }
    88 
    8984                if ( null === $this->original_pagenow ) {
    9085                        unset( $GLOBALS['pagenow'] );
    … …  
    105100                add_filter( 'wp_client_side_media_processing_enabled', '__return_false' );
    106101
    107                 // Should not error or start an output buffer.
    108                 $level_before = ob_get_level();
    109                 wp_set_up_cross_origin_isolation();
    110                 $level_after = ob_get_level();
    111 
    112                 $this->assertSame( $level_before, $level_after );
     102                $this->assertFalse( wp_set_up_cross_origin_isolation() );
    113103        }
    114104
    … …  
    117107         */
    118108        public function test_returns_early_when_no_screen() {
    119                 // No screen is set, so it should return early.
    120                 $level_before = ob_get_level();
    121                 wp_set_up_cross_origin_isolation();
    122                 $level_after = ob_get_level();
    123 
    124                 $this->assertSame( $level_before, $level_after );
    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          *
     109                $this->assertFalse( wp_set_up_cross_origin_isolation() );
     110        }
     111
     112        /**
    132113         * @ticket 64766
    133114         *
    … …  
    135116         * @preserveGlobalState disabled
    136117         */
    137         public function test_starts_output_buffer_for_chrome_137() {
    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                 $level_before = ob_get_level();
    141                 wp_start_cross_origin_isolation_output_buffer();
    142                 $level_after = ob_get_level();
    143 
    144                 $this->assertSame( $level_before + 1, $level_after, 'Output buffer should be started for Chrome 137.' );
    145 
    146                 ob_end_clean();
    147         }
    148 
    149         /**
    150          * @ticket 64766
    151          */
    152         public function test_does_not_start_output_buffer_for_chrome_136() {
     118        public function test_sends_header_for_chrome_137() {
     119                $_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';
     120
     121                $this->assertTrue( wp_send_document_isolation_policy_header(), 'The Document-Isolation-Policy header should be sent for Chrome 137.' );
     122        }
     123
     124        /**
     125         * @ticket 64766
     126         */
     127        public function test_does_not_send_header_for_chrome_136() {
    153128                $_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';
    154129
    155                 $level_before = ob_get_level();
    156                 wp_start_cross_origin_isolation_output_buffer();
    157                 $level_after = ob_get_level();
    158 
    159                 $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Chrome < 137.' );
    160         }
    161 
    162         /**
    163          * @ticket 64766
    164          */
    165         public function test_does_not_start_output_buffer_for_firefox() {
     130                $this->assertFalse( wp_send_document_isolation_policy_header(), 'The Document-Isolation-Policy header should not be sent for Chrome < 137.' );
     131        }
     132
     133        /**
     134         * @ticket 64766
     135         */
     136        public function test_does_not_send_header_for_firefox() {
    166137                $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0';
    167138
    168                 $level_before = ob_get_level();
    169                 wp_start_cross_origin_isolation_output_buffer();
    170                 $level_after = ob_get_level();
    171 
    172                 $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Firefox.' );
    173         }
    174 
    175         /**
    176          * @ticket 64766
    177          */
    178         public function test_does_not_start_output_buffer_for_safari() {
     139                $this->assertFalse( wp_send_document_isolation_policy_header(), 'The Document-Isolation-Policy header should not be sent for Firefox.' );
     140        }
     141
     142        /**
     143         * @ticket 64766
     144         */
     145        public function test_does_not_send_header_for_safari() {
    179146                $_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';
    180147
    181                 $level_before = ob_get_level();
    182                 wp_start_cross_origin_isolation_output_buffer();
    183                 $level_after = ob_get_level();
    184 
    185                 $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Safari.' );
     148                $this->assertFalse( wp_send_document_isolation_policy_header(), 'The Document-Isolation-Policy header should not be sent for Safari.' );
     149        }
     150
     151        /**
     152         * Runs in the main PHPUnit process, where output has already started.
     153         *
     154         * @ticket 65930
     155         */
     156        public function test_does_not_send_header_when_headers_already_sent() {
     157                if ( ! headers_sent() ) {
     158                        $this->markTestSkipped( 'Requires output to have already been sent.' );
     159                }
     160
     161                $_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';
     162
     163                $this->assertFalse( wp_send_document_isolation_policy_header(), 'The Document-Isolation-Policy header cannot be sent once headers are sent.' );
    186164        }
    187165
    … …  
    212190                }
    213191
    214                 $level_before = ob_get_level();
    215                 wp_set_up_cross_origin_isolation();
    216                 $level_after = ob_get_level();
    217 
    218                 $this->assertSame( $level_before, $level_after, 'DIP should be skipped on the classic-theme site editor home route.' );
     192                $this->assertFalse( wp_set_up_cross_origin_isolation(), 'DIP should be skipped on the classic-theme site editor home route.' );
    219193        }
    220194
    … …  
    251225                $_GET['p'] = '/page/about';
    252226
    253                 $level_before = ob_get_level();
    254                 wp_set_up_cross_origin_isolation();
    255                 $level_after = ob_get_level();
    256 
    257                 $this->assertSame( $level_before + 1, $level_after, 'DIP should be set up on a non-home site editor route.' );
    258 
    259                 ob_end_clean();
     227                $this->assertTrue( wp_set_up_cross_origin_isolation(), 'DIP should be set up on a non-home site editor route.' );
    260228        }
    261229
    … …  
    281249                unset( $_GET['p'] );
    282250
    283                 $level_before = ob_get_level();
    284                 wp_set_up_cross_origin_isolation();
    285                 $level_after = ob_get_level();
    286 
    287                 $this->assertSame( $level_before + 1, $level_after, 'DIP should be set up on the block-theme site editor home route.' );
    288 
    289                 ob_end_clean();
     251                $this->assertTrue( wp_set_up_cross_origin_isolation(), 'DIP should be set up on the block-theme site editor home route.' );
    290252        }
    291253
    … …  
    363325
    364326        /**
    365          * Verifies that cross-origin elements get crossorigin="anonymous" added.
    366          *
    367          * @ticket 64766
     327         * The deprecated output buffer helper only sends the header now.
     328         *
     329         * @ticket 65930
     330         *
     331         * @expectedDeprecated wp_start_cross_origin_isolation_output_buffer
    368332         *
    369333         * @runInSeparateProcess
    370334         * @preserveGlobalState disabled
    371          *
    372          * @dataProvider data_elements_that_should_get_crossorigin
     335         */
     336        public function test_deprecated_output_buffer_helper_does_not_buffer() {
     337                $_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';
     338
     339                $level_before = ob_get_level();
     340                wp_start_cross_origin_isolation_output_buffer();
     341
     342                $this->assertSame( $level_before, ob_get_level(), 'No output buffer should be started.' );
     343        }
     344
     345        /**
     346         * The deprecated attribute injector leaves the HTML untouched.
     347         *
     348         * Under Document-Isolation-Policy: isolate-and-credentialless, cross-origin
     349         * scripts, styles, images, audio, and video load without a crossorigin
     350         * attribute. Adding one forces a CORS request that fails for resources
     351         * served without Access-Control-Allow-Origin headers.
     352         *
     353         * @ticket 65930
     354         *
     355         * @expectedDeprecated wp_add_crossorigin_attributes
     356         *
     357         * @dataProvider data_cross_origin_elements
    373358         *
    374359         * @param string $html HTML input to process.
    375360         */
    376         public function test_output_buffer_adds_crossorigin( $html ) {
    377                 $_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';
    378 
    379                 ob_start();
    380 
    381                 wp_start_cross_origin_isolation_output_buffer();
    382                 echo $html;
    383 
    384                 ob_end_flush();
    385                 $output = ob_get_clean();
    386 
    387                 $this->assertStringContainsString( 'crossorigin="anonymous"', $output );
    388         }
    389 
    390         /**
    391          * Data provider for elements that should receive crossorigin="anonymous".
    392          *
    393          * @return array[]
    394          */
    395         public function data_elements_that_should_get_crossorigin() {
     361        public function test_deprecated_attribute_injector_returns_html_unchanged( string $html ) {
     362                $this->assertSame( $html, wp_add_crossorigin_attributes( $html ) );
     363        }
     364
     365        /**
     366         * Data provider of cross-origin elements that used to receive crossorigin="anonymous".
     367         *
     368         * @return array<non-falsy-string, array{ non-falsy-string }>
     369         */
     370        public function data_cross_origin_elements(): array {
    396371                return array(
    397372                        'cross-origin script'              => array(
    … …  
    414389
    415390        /**
    416          * Verifies that certain elements do not get crossorigin="anonymous" added.
    417          *
    418          * Images are excluded because under Document-Isolation-Policy:
    419          * isolate-and-credentialless, the browser handles cross-origin images
    420          * in credentialless mode without needing explicit CORS headers.
    421          *
    422          * @ticket 64766
    423          *
    424          * @runInSeparateProcess
    425          * @preserveGlobalState disabled
    426          *
    427          * @dataProvider data_elements_that_should_not_get_crossorigin
    428          *
    429          * @param string $html HTML input to process.
    430          */
    431         public function test_output_buffer_does_not_add_crossorigin( $html ) {
    432                 $_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';
    433 
    434                 ob_start();
    435 
    436                 wp_start_cross_origin_isolation_output_buffer();
    437                 echo $html;
    438 
    439                 ob_end_flush();
    440                 $output = ob_get_clean();
    441 
    442                 $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );
    443         }
    444 
    445         /**
    446          * Data provider for elements that should not receive crossorigin="anonymous".
    447          *
    448          * @return array[]
    449          */
    450         public function data_elements_that_should_not_get_crossorigin() {
    451                 return array(
    452                         'cross-origin img'                        => array(
    453                                 '<img src="https://external.example.com/image.jpg" />',
    454                         ),
    455                         'cross-origin img with srcset'            => array(
    456                                 '<img src="https://external.example.com/image.jpg" srcset="https://external.example.com/image-2x.jpg 2x" />',
    457                         ),
    458                         'link with cross-origin imagesrcset only' => array(
    459                                 '<link rel="preload" as="image" imagesrcset="https://external.example.com/image.jpg 1x" href="/local-fallback.jpg" />',
    460                         ),
    461                         'relative URL script'                     => array(
    462                                 '<script src="/wp-includes/js/wp-embed.min.js"></script>',
    463                         ),
    464                 );
    465         }
    466 
    467         /**
    468          * Same-origin URLs should not get crossorigin="anonymous".
    469          *
    470          * Uses site_url() at runtime since the test domain varies by CI config.
    471          *
    472          * @ticket 64766
    473          *
    474          * @runInSeparateProcess
    475          * @preserveGlobalState disabled
    476          */
    477         public function test_output_buffer_does_not_add_crossorigin_to_same_origin() {
    478                 $_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';
    479 
    480                 ob_start();
    481 
    482                 wp_start_cross_origin_isolation_output_buffer();
    483                 echo '<script src="' . site_url( '/wp-includes/js/wp-embed.min.js' ) . '"></script>';
    484 
    485                 ob_end_flush();
    486                 $output = ob_get_clean();
    487 
    488                 $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );
    489         }
    490 
    491         /**
    492          * Elements that already have a crossorigin attribute should not be modified.
    493          *
    494          * @ticket 64766
    495          *
    496          * @runInSeparateProcess
    497          * @preserveGlobalState disabled
    498          */
    499         public function test_output_buffer_does_not_override_existing_crossorigin() {
    500                 $_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';
    501 
    502                 ob_start();
    503 
    504                 wp_start_cross_origin_isolation_output_buffer();
    505                 echo '<script src="https://external.example.com/script.js" crossorigin="use-credentials"></script>';
    506 
    507                 ob_end_flush();
    508                 $output = ob_get_clean();
    509 
    510                 $this->assertStringContainsString( 'crossorigin="use-credentials"', $output, 'Existing crossorigin attribute should not be overridden.' );
    511                 $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );
    512         }
    513 
    514         /**
    515          * Multiple tags in the same output should each be handled correctly.
    516          *
    517          * @ticket 64766
    518          *
    519          * @runInSeparateProcess
    520          * @preserveGlobalState disabled
    521          */
    522         public function test_output_buffer_handles_mixed_tags() {
    523                 $_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';
    524 
    525                 ob_start();
    526 
    527                 wp_start_cross_origin_isolation_output_buffer();
    528                 echo '<img src="https://external.example.com/image.jpg" />';
    529                 echo '<script src="https://external.example.com/script.js"></script>';
    530                 echo '<audio src="https://external.example.com/audio.mp3"></audio>';
    531 
    532                 ob_end_flush();
    533                 $output = ob_get_clean();
    534 
    535                 // IMG should NOT have crossorigin.
    536                 $this->assertStringContainsString( '<img src="https://external.example.com/image.jpg" />', $output, 'IMG should not be modified.' );
    537 
    538                 // Script and audio should have crossorigin.
    539                 $this->assertSame( 2, substr_count( $output, 'crossorigin="anonymous"' ), 'Script and audio should both get crossorigin, but not img.' );
    540         }
    541 
    542         /**
    543          * IMG tags in the media manager templates must not receive
    544          * crossorigin="anonymous", matching wp_add_crossorigin_attributes().
    545          *
    546          * Adding the attribute forces a CORS request that breaks previews of
    547          * images served without Access-Control-Allow-Origin headers, such as
    548          * media offloaded to a CDN.
     391         * The media manager templates must not carry crossorigin="anonymous".
     392         *
     393         * Adding the attribute forces a CORS request that breaks previews and
     394         * playback of media served without Access-Control-Allow-Origin headers,
     395         * such as media offloaded to a CDN.
    549396         *
    550397         * @ticket 65673
     398         * @ticket 65930
    551399         *
    552400         * @covers ::wp_print_media_templates
    553401         */
    554         public function test_print_media_templates_does_not_add_crossorigin_to_img() {
     402        public function test_print_media_templates_does_not_add_crossorigin() {
    555403                require_once ABSPATH . WPINC . '/media-template.php';
    556404
    … …  
    561409                $output = ob_get_clean();
    562410
    563                 $this->assertMatchesRegularExpression( '/<img\b/i', $output, 'Expected the media templates to contain IMG tags.' );
    564                 $this->assertDoesNotMatchRegularExpression( '/<img\b[^>]*\bcrossorigin\b/i', $output, 'IMG tags in the media templates must not receive a crossorigin attribute.' );
    565                 $this->assertMatchesRegularExpression( '/<(?:audio|video)\b[^>]*crossorigin="anonymous"/i', $output, 'AUDIO and VIDEO tags in the media templates should still receive crossorigin="anonymous".' );
     411                $this->assertMatchesRegularExpression( '/<(?:img|audio|video)\b/i', $output, 'Expected the media templates to contain media tags.' );
     412                $this->assertDoesNotMatchRegularExpression( '/<(?:img|audio|video)\b[^>]*\bcrossorigin\b/i', $output, 'Media tags in the media templates must not receive a crossorigin attribute.' );
    566413        }
    567414}
Note: See TracChangeset for help on using the changeset viewer.