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 +108 -105 23.4.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,48 +198,64 @@
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 - * Deletes the HEIC companion file when its attachment is deleted.
220 + * Deletes the source-format companion file when its attachment is deleted.
194 221 *
195 - * The HEIC is sideloaded alongside a JPEG derivative and recorded in
196 - * $metadata['original']. WordPress core's wp_delete_attachment_files()
197 - * only knows about 'original_image', so without this hook the HEIC
198 - * would linger on disk after the attachment is deleted.
222 + * When the client-side media flow sideloads a source-format original (such as
223 + * a HEIC file) alongside a web-viewable derivative, the original's filename is
224 + * recorded in the 'source_image' metadata key. WordPress only tracks
225 + * 'original_image' in wp_delete_attachment_files(), so without this hook the
226 + * companion file would linger on disk after the attachment is deleted.
199 227 *
200 228 * @param int $post_id Attachment ID being deleted.
229 + * @return bool Whether a companion file was deleted.
201 230 */
202 -function gutenberg_delete_heic_companion_file( int $post_id ): void {
231 +function gutenberg_delete_heic_companion_file( int $post_id ): bool {
203 232 $metadata = wp_get_attachment_metadata( $post_id, true );
204 233
205 - if ( empty( $metadata['original'] ) || ! is_string( $metadata['original'] ) ) {
206 - return;
234 + $source_image = $metadata['source_image'] ?? null;
235 + if ( ! is_string( $source_image ) || '' === $source_image ) {
236 + return false;
207 237 }
208 238
209 239 $attached_file = get_attached_file( $post_id, true );
210 240
211 241 if ( ! $attached_file ) {
212 - return;
242 + return false;
213 243 }
214 244
215 - $heic_path = path_join( dirname( $attached_file ), $metadata['original'] );
245 + $uploads = wp_get_upload_dir();
216 246
217 - if ( file_exists( $heic_path ) ) {
218 - wp_delete_file( $heic_path );
247 + if ( empty( $uploads['basedir'] ) ) {
248 + return false;
219 249 }
250 +
251 + $companion_path = path_join( dirname( $attached_file ), wp_basename( $source_image ) );
252 +
253 + if ( ! file_exists( $companion_path ) ) {
254 + return false;
255 + }
256 +
257 + return wp_delete_file_from_directory( $companion_path, $uploads['basedir'] );
220 258 }
221 259
222 260 add_action( 'delete_attachment', 'gutenberg_delete_heic_companion_file' );
223 261
@@ -225,19 +263,8 @@
225 263 // Everything below requires cross-origin isolation (Document-Isolation-Policy)
226 264 // and SharedArrayBuffer support, which is only available in Chromium 137+.
227 265
228 266 /**
229 - * Sets a global JS variable to indicate that client-side media processing is enabled.
230 - */
231 -function gutenberg_set_client_side_media_processing_flag() {
232 - if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
233 - return;
234 - }
235 - wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
236 -}
237 -add_action( 'admin_init', 'gutenberg_set_client_side_media_processing_flag' );
238 -
239 -/**
240 267 * Filters the list of rewrite rules formatted for output to an .htaccess file.
241 268 *
242 269 * Adds support for serving wasm-vips locally.
243 270 *
@@ -319,9 +346,9 @@
319 346 if ( ! user_can( $user_id, 'upload_files' ) ) {
320 347 return;
321 348 }
322 349
323 - gutenberg_start_cross_origin_isolation_output_buffer();
350 + gutenberg_send_document_isolation_policy_header();
324 351 }
325 352
326 353 add_action( 'load-post.php', 'gutenberg_set_up_cross_origin_isolation' );
327 354 add_action( 'load-post-new.php', 'gutenberg_set_up_cross_origin_isolation' );
@@ -337,11 +364,18 @@
337 364
338 365 /**
339 366 * Sends the Document-Isolation-Policy header for cross-origin isolation.
340 367 *
341 - * 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.
342 376 */
343 -function gutenberg_start_cross_origin_isolation_output_buffer(): void {
377 +function gutenberg_send_document_isolation_policy_header(): bool {
344 378 $chromium_version = gutenberg_get_chromium_major_version();
345 379
346 380 /**
347 381 * Filters whether to use Document-Isolation-Policy for cross-origin isolation.
@@ -359,90 +393,69 @@
359 393 null !== $chromium_version && $chromium_version >= 137
360 394 );
361 395
362 396 if ( ! $use_dip ) {
363 - return;
397 + return false;
364 398 }
365 399
366 - ob_start(
367 - function ( string $output ): string {
368 - header( 'Document-Isolation-Policy: isolate-and-credentialless' );
400 + header( 'Document-Isolation-Policy: isolate-and-credentialless' );
369 401
370 - return gutenberg_add_crossorigin_attributes( $output );
371 - }
372 - );
402 + return true;
373 403 }
374 404
375 405 /**
376 - * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
406 + * Removes `crossorigin` attributes from the printed media templates.
377 407 *
378 - * @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.
379 415 *
380 - * @return string Modified HTML.
416 + * @param string $html The printed media templates.
417 + *
418 + * @return string Modified media templates.
381 419 */
382 -function gutenberg_add_crossorigin_attributes( string $html ): string {
383 - $site_url = site_url();
384 -
385 - $processor = new WP_HTML_Tag_Processor( $html );
386 -
387 - // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
388 - $tags = array(
389 - 'AUDIO' => 'src',
390 - 'LINK' => 'href',
391 - 'SCRIPT' => 'src',
392 - 'VIDEO' => 'src',
393 - 'SOURCE' => 'src',
394 - );
395 -
396 - $tag_names = array_keys( $tags );
397 -
398 - while ( $processor->next_tag() ) {
399 - $tag = $processor->get_tag();
400 -
401 - 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' ) ) {
402 430 continue;
403 431 }
404 -
405 - if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
406 - $processor->set_bookmark( 'audio-video-parent' );
407 - }
408 -
409 - $processor->set_bookmark( 'resume' );
410 -
411 - $sought = false;
412 -
413 - $crossorigin = $processor->get_attribute( 'crossorigin' );
414 -
415 - $url = $processor->get_attribute( $tags[ $tag ] );
416 -
417 - if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) {
418 - if ( 'SOURCE' === $tag ) {
419 - $sought = $processor->seek( 'audio-video-parent' );
420 -
421 - if ( $sought ) {
422 - $processor->set_attribute( 'crossorigin', 'anonymous' );
423 - }
424 - } else {
425 - $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' );
426 439 }
427 -
428 - if ( $sought ) {
429 - $processor->seek( 'resume' );
430 - $processor->release_bookmark( 'audio-video-parent' );
431 - }
432 440 }
441 + $script_processor->set_modifiable_text( $template_processor->get_updated_html() );
433 442 }
434 443
435 - return $processor->get_updated_html();
444 + return $script_processor->get_updated_html();
436 445 }
437 446
438 447 /**
439 448 * Overrides templates from wp_print_media_templates with custom ones.
440 449 *
441 - * Adds `crossorigin` attribute to all tags that
442 - * 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.
443 452 */
444 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 +
445 458 remove_action( 'admin_footer', 'wp_print_media_templates' );
446 459 add_action(
447 460 'admin_footer',
448 461 static function (): void {
@@ -449,19 +462,9 @@
449 462 ob_start();
450 463 wp_print_media_templates();
451 464 $html = (string) ob_get_clean();
452 465
453 - $tags = array(
454 - 'audio',
455 - 'img',
456 - 'video',
457 - );
458 -
459 - foreach ( $tags as $tag ) {
460 - $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html );
461 - }
462 -
463 - echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
466 + echo gutenberg_remove_media_template_crossorigin_attributes( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
464 467 }
465 468 );
466 469 }
467 470