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 +85 -93 23.5.0 → trunk View file →
@@ -18,8 +18,14 @@
18 18 if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
19 19 return;
20 20 }
21 21
22 +// Animated GIF → video: clean up the sideloaded companion video and
23 +// poster when their GIF attachment is deleted. The GIF→video swap itself
24 +// happens in the editor (the converted block is a real core/video), so no
25 +// render-time filtering is needed.
26 +require_once __DIR__ . '/animated-gif-to-video.php';
27 +
22 28 // ── Tier 1: HEIC infrastructure (always loaded) ─────────────────────
23 29
24 30 /**
25 31 * Registers HEIC/HEIF as allowed upload MIME types.
@@ -165,11 +171,27 @@
165 171 function gutenberg_media_processing_filter_rest_index( WP_REST_Response $response ) {
166 172 /** This filter is documented in wp-admin/includes/image.php */
167 173 $image_size_threshold = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
168 174
175 + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
176 + $image_strip_meta = (bool) apply_filters( 'image_strip_meta', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
177 +
178 + /*
179 + * On the server, this filter receives the decoded image's actual bit depth.
180 + * The client path never decodes the image on the server, so the filter is
181 + * applied with 16 (the maximum depth vips can produce) as both the value
182 + * and the current depth. The client caps its output bit depth at the
183 + * filtered value, so a plugin lowering it (e.g. to 8) takes effect on
184 + * client-generated images too.
185 + */
186 + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
187 + $image_max_bit_depth = (int) apply_filters( 'image_max_bit_depth', 16, 16 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
188 +
169 189 if ( current_user_can( 'upload_files' ) ) {
170 190 $response->data['image_sizes'] = gutenberg_get_all_image_sizes();
171 191 $response->data['image_size_threshold'] = $image_size_threshold;
192 + $response->data['image_strip_meta'] = $image_strip_meta;
193 + $response->data['image_max_bit_depth'] = $image_max_bit_depth;
172 194 }
173 195
174 196 return $response;
175 197 }
@@ -176,19 +198,24 @@
176 198
177 199 add_filter( 'rest_index', 'gutenberg_media_processing_filter_rest_index' );
178 200
179 201 /**
180 - * Sets a global JS variable to indicate that HEIC canvas-based upload support is available.
202 + * Sets a global JS variable to indicate that client-side media processing is enabled.
181 203 *
182 - * This flag is set whenever the media processing feature is enabled,
183 - * regardless of whether the browser supports full VIPS-based processing.
184 - * Browsers like Safari can use createImageBitmap() to decode HEIC images
185 - * and convert them to JPEG for server-side sub-size generation.
204 + * The flag gates both processing modes: the full VIPS/WASM pipeline (browsers
205 + * that pass feature detection) and the HEIC canvas fallback used by browsers
206 + * such as Safari that can decode HEIC via createImageBitmap() but lack
207 + * SharedArrayBuffer support. The browser-capability check happens client-side.
186 208 */
187 -function gutenberg_set_heic_upload_support_flag() {
188 - wp_add_inline_script( 'wp-block-editor', 'window.__heicUploadSupport = true', 'before' );
209 +function gutenberg_set_client_side_media_processing_flag() {
210 + // Re-check the filter at action time, since other plugins (loaded after Gutenberg)
211 + // may have added a filter to disable client-side media processing.
212 + if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
213 + return;
214 + }
215 + wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
189 216 }
190 -add_action( 'admin_init', 'gutenberg_set_heic_upload_support_flag' );
217 +add_action( 'admin_init', 'gutenberg_set_client_side_media_processing_flag' );
191 218
192 219 /**
193 220 * Deletes the source-format companion file when its attachment is deleted.
194 221 *
@@ -236,19 +263,8 @@
236 263 // Everything below requires cross-origin isolation (Document-Isolation-Policy)
237 264 // and SharedArrayBuffer support, which is only available in Chromium 137+.
238 265
239 266 /**
240 - * Sets a global JS variable to indicate that client-side media processing is enabled.
241 - */
242 -function gutenberg_set_client_side_media_processing_flag() {
243 - if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
244 - return;
245 - }
246 - wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
247 -}
248 -add_action( 'admin_init', 'gutenberg_set_client_side_media_processing_flag' );
249 -
250 -/**
251 267 * Filters the list of rewrite rules formatted for output to an .htaccess file.
252 268 *
253 269 * Adds support for serving wasm-vips locally.
254 270 *
@@ -330,9 +346,9 @@
330 346 if ( ! user_can( $user_id, 'upload_files' ) ) {
331 347 return;
332 348 }
333 349
334 - gutenberg_start_cross_origin_isolation_output_buffer();
350 + gutenberg_send_document_isolation_policy_header();
335 351 }
336 352
337 353 add_action( 'load-post.php', 'gutenberg_set_up_cross_origin_isolation' );
338 354 add_action( 'load-post-new.php', 'gutenberg_set_up_cross_origin_isolation' );
@@ -348,11 +364,18 @@
348 364
349 365 /**
350 366 * Sends the Document-Isolation-Policy header for cross-origin isolation.
351 367 *
352 - * 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.
353 376 */
354 -function gutenberg_start_cross_origin_isolation_output_buffer(): void {
377 +function gutenberg_send_document_isolation_policy_header(): bool {
355 378 $chromium_version = gutenberg_get_chromium_major_version();
356 379
357 380 /**
358 381 * Filters whether to use Document-Isolation-Policy for cross-origin isolation.
@@ -370,90 +393,69 @@
370 393 null !== $chromium_version && $chromium_version >= 137
371 394 );
372 395
373 396 if ( ! $use_dip ) {
374 - return;
397 + return false;
375 398 }
376 399
377 - ob_start(
378 - function ( string $output ): string {
379 - header( 'Document-Isolation-Policy: isolate-and-credentialless' );
400 + header( 'Document-Isolation-Policy: isolate-and-credentialless' );
380 401
381 - return gutenberg_add_crossorigin_attributes( $output );
382 - }
383 - );
402 + return true;
384 403 }
385 404
386 405 /**
387 - * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
406 + * Removes `crossorigin` attributes from the printed media templates.
388 407 *
389 - * @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.
390 415 *
391 - * @return string Modified HTML.
416 + * @param string $html The printed media templates.
417 + *
418 + * @return string Modified media templates.
392 419 */
393 -function gutenberg_add_crossorigin_attributes( string $html ): string {
394 - $site_url = site_url();
395 -
396 - $processor = new WP_HTML_Tag_Processor( $html );
397 -
398 - // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
399 - $tags = array(
400 - 'AUDIO' => 'src',
401 - 'LINK' => 'href',
402 - 'SCRIPT' => 'src',
403 - 'VIDEO' => 'src',
404 - 'SOURCE' => 'src',
405 - );
406 -
407 - $tag_names = array_keys( $tags );
408 -
409 - while ( $processor->next_tag() ) {
410 - $tag = $processor->get_tag();
411 -
412 - 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' ) ) {
413 430 continue;
414 431 }
415 -
416 - if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
417 - $processor->set_bookmark( 'audio-video-parent' );
418 - }
419 -
420 - $processor->set_bookmark( 'resume' );
421 -
422 - $sought = false;
423 -
424 - $crossorigin = $processor->get_attribute( 'crossorigin' );
425 -
426 - $url = $processor->get_attribute( $tags[ $tag ] );
427 -
428 - if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) {
429 - if ( 'SOURCE' === $tag ) {
430 - $sought = $processor->seek( 'audio-video-parent' );
431 -
432 - if ( $sought ) {
433 - $processor->set_attribute( 'crossorigin', 'anonymous' );
434 - }
435 - } else {
436 - $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' );
437 439 }
438 -
439 - if ( $sought ) {
440 - $processor->seek( 'resume' );
441 - $processor->release_bookmark( 'audio-video-parent' );
442 - }
443 440 }
441 + $script_processor->set_modifiable_text( $template_processor->get_updated_html() );
444 442 }
445 443
446 - return $processor->get_updated_html();
444 + return $script_processor->get_updated_html();
447 445 }
448 446
449 447 /**
450 448 * Overrides templates from wp_print_media_templates with custom ones.
451 449 *
452 - * Adds `crossorigin` attribute to all tags that
453 - * 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.
454 452 */
455 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 +
456 458 remove_action( 'admin_footer', 'wp_print_media_templates' );
457 459 add_action(
458 460 'admin_footer',
459 461 static function (): void {
@@ -460,19 +462,9 @@
460 462 ob_start();
461 463 wp_print_media_templates();
462 464 $html = (string) ob_get_clean();
463 465
464 - $tags = array(
465 - 'audio',
466 - 'img',
467 - 'video',
468 - );
469 -
470 - foreach ( $tags as $tag ) {
471 - $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html );
472 - }
473 -
474 - echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
466 + echo gutenberg_remove_media_template_crossorigin_attributes( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
475 467 }
476 468 );
477 469 }
478 470