PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/media/load.php +50 -74 23.6.0 → trunk View file →
@@ -346,9 +346,9 @@
346 346 if ( ! user_can( $user_id, 'upload_files' ) ) {
347 347 return;
348 348 }
349 349
350 - gutenberg_start_cross_origin_isolation_output_buffer();
350 + gutenberg_send_document_isolation_policy_header();
351 351 }
352 352
353 353 add_action( 'load-post.php', 'gutenberg_set_up_cross_origin_isolation' );
354 354 add_action( 'load-post-new.php', 'gutenberg_set_up_cross_origin_isolation' );
@@ -364,11 +364,18 @@
364 364
365 365 /**
366 366 * Sends the Document-Isolation-Policy header for cross-origin isolation.
367 367 *
368 - * Uses an output buffer to add crossorigin="anonymous" where needed.
368 + * `isolate-and-credentialless` loads cross-origin subresources without
369 + * credentials instead of blocking them, so no `crossorigin` attribute is
370 + * needed on scripts, styles, images, audio, or video for the page to work.
371 + * Forcing `crossorigin="anonymous"` would turn those into CORS requests
372 + * and break any resource served without `Access-Control-Allow-Origin`,
373 + * such as media offloaded to a CDN.
374 + *
375 + * @return bool Whether the header was sent.
369 376 */
370 -function gutenberg_start_cross_origin_isolation_output_buffer(): void {
377 +function gutenberg_send_document_isolation_policy_header(): bool {
371 378 $chromium_version = gutenberg_get_chromium_major_version();
372 379
373 380 /**
374 381 * Filters whether to use Document-Isolation-Policy for cross-origin isolation.
@@ -386,90 +393,69 @@
386 393 null !== $chromium_version && $chromium_version >= 137
387 394 );
388 395
389 396 if ( ! $use_dip ) {
390 - return;
397 + return false;
391 398 }
392 399
393 - ob_start(
394 - function ( string $output ): string {
395 - header( 'Document-Isolation-Policy: isolate-and-credentialless' );
400 + header( 'Document-Isolation-Policy: isolate-and-credentialless' );
396 401
397 - return gutenberg_add_crossorigin_attributes( $output );
398 - }
399 - );
402 + return true;
400 403 }
401 404
402 405 /**
403 - * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
406 + * Removes `crossorigin` attributes from the printed media templates.
404 407 *
405 - * @param string $html HTML input.
408 + * WordPress 7.1 forces `crossorigin="anonymous"` onto the AUDIO and VIDEO
409 + * tags inside the Backbone `<script type="text/html">` templates whenever
410 + * client-side media processing is enabled. Under
411 + * `Document-Isolation-Policy: isolate-and-credentialless` the attribute is
412 + * not needed to play cross-origin media, and it turns the load into a CORS
413 + * request that fails for media served without CORS headers, such as media
414 + * offloaded to a CDN. See https://core.trac.wordpress.org/ticket/65930.
406 415 *
407 - * @return string Modified HTML.
416 + * @param string $html The printed media templates.
417 + *
418 + * @return string Modified media templates.
408 419 */
409 -function gutenberg_add_crossorigin_attributes( string $html ): string {
410 - $site_url = site_url();
411 -
412 - $processor = new WP_HTML_Tag_Processor( $html );
413 -
414 - // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
415 - $tags = array(
416 - 'AUDIO' => 'src',
417 - 'LINK' => 'href',
418 - 'SCRIPT' => 'src',
419 - 'VIDEO' => 'src',
420 - 'SOURCE' => 'src',
421 - );
422 -
423 - $tag_names = array_keys( $tags );
424 -
425 - while ( $processor->next_tag() ) {
426 - $tag = $processor->get_tag();
427 -
428 - if ( ! in_array( $tag, $tag_names, true ) ) {
420 +function gutenberg_remove_media_template_crossorigin_attributes( string $html ): string {
421 + /*
422 + * The media templates are inside <script type="text/html"> tags,
423 + * whose content is treated as raw text by the HTML Tag Processor.
424 + * Extract each script block's content, process it separately,
425 + * then reassemble the full output.
426 + */
427 + $script_processor = new WP_HTML_Tag_Processor( $html );
428 + while ( $script_processor->next_tag( 'SCRIPT' ) ) {
429 + if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {
429 430 continue;
430 431 }
431 -
432 - if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
433 - $processor->set_bookmark( 'audio-video-parent' );
434 - }
435 -
436 - $processor->set_bookmark( 'resume' );
437 -
438 - $sought = false;
439 -
440 - $crossorigin = $processor->get_attribute( 'crossorigin' );
441 -
442 - $url = $processor->get_attribute( $tags[ $tag ] );
443 -
444 - if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) {
445 - if ( 'SOURCE' === $tag ) {
446 - $sought = $processor->seek( 'audio-video-parent' );
447 -
448 - if ( $sought ) {
449 - $processor->set_attribute( 'crossorigin', 'anonymous' );
450 - }
451 - } else {
452 - $processor->set_attribute( 'crossorigin', 'anonymous' );
432 + $template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );
433 + while ( $template_processor->next_tag() ) {
434 + if (
435 + in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true )
436 + && 'anonymous' === $template_processor->get_attribute( 'crossorigin' )
437 + ) {
438 + $template_processor->remove_attribute( 'crossorigin' );
453 439 }
454 -
455 - if ( $sought ) {
456 - $processor->seek( 'resume' );
457 - $processor->release_bookmark( 'audio-video-parent' );
458 - }
459 440 }
441 + $script_processor->set_modifiable_text( $template_processor->get_updated_html() );
460 442 }
461 443
462 - return $processor->get_updated_html();
444 + return $script_processor->get_updated_html();
463 445 }
464 446
465 447 /**
466 448 * Overrides templates from wp_print_media_templates with custom ones.
467 449 *
468 - * Adds `crossorigin` attribute to all tags that
469 - * could have assets loaded from a different domain.
450 + * Only needed on WordPress 7.1, the one release whose
451 + * `wp_print_media_templates()` injects `crossorigin="anonymous"` itself.
470 452 */
471 453 function gutenberg_override_media_templates(): void {
454 + if ( ! function_exists( 'wp_add_crossorigin_attributes' ) || function_exists( 'wp_send_document_isolation_policy_header' ) ) {
455 + return;
456 + }
457 +
472 458 remove_action( 'admin_footer', 'wp_print_media_templates' );
473 459 add_action(
474 460 'admin_footer',
475 461 static function (): void {
@@ -476,19 +462,9 @@
476 462 ob_start();
477 463 wp_print_media_templates();
478 464 $html = (string) ob_get_clean();
479 465
480 - $tags = array(
481 - 'audio',
482 - 'img',
483 - 'video',
484 - );
485 -
486 - foreach ( $tags as $tag ) {
487 - $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html );
488 - }
489 -
490 - echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
466 + echo gutenberg_remove_media_template_crossorigin_attributes( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
491 467 }
492 468 );
493 469 }
494 470