PluginProbe
Gutenberg / 22.8.2
Gutenberg v22.8.2
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 12.6.0 All 403 releases
gutenberg / lib / media / load.php

load.php in Gutenberg 22.8.2, at lib/media/load.php

428 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Adds media-related functionality for client-side media processing.
4 *
5 * @package gutenberg
6 */
7
8 if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
9 return;
10 }
11
12 /**
13 * Sets a global JS variable to indicate that client-side media processing is enabled.
14 */
15 function gutenberg_set_client_side_media_processing_flag() {
16 if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
17 return;
18 }
19 wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
20 }
21 add_action( 'admin_init', 'gutenberg_set_client_side_media_processing_flag' );
22
23 /**
24 * Returns a list of all available image sizes.
25 *
26 * @return array Existing image sizes.
27 */
28 function gutenberg_get_all_image_sizes(): array {
29 $sizes = wp_get_registered_image_subsizes();
30
31 foreach ( $sizes as $name => &$size ) {
32 $size['height'] = (int) $size['height'];
33 $size['width'] = (int) $size['width'];
34 $size['name'] = $name;
35 }
36 unset( $size );
37
38 return $sizes;
39 }
40
41 /**
42 * Returns the default output format mapping for the supported image formats.
43 *
44 * @return array<string,string> Map of input formats to output formats.
45 */
46 function gutenberg_get_default_image_output_formats() {
47 $input_formats = array(
48 'image/jpeg',
49 'image/png',
50 'image/gif',
51 'image/webp',
52 'image/avif',
53 'image/heic',
54 );
55
56 $output_formats = array();
57
58 foreach ( $input_formats as $mime_type ) {
59 /** This filter is documented in wp-includes/media.php */
60 $output_formats = apply_filters(
61 'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
62 $output_formats,
63 '',
64 $mime_type
65 );
66 }
67
68 return $output_formats;
69 }
70
71 /**
72 * Filters the REST API root index data to add custom settings.
73 *
74 * @param WP_REST_Response $response Response data.
75 */
76 function gutenberg_media_processing_filter_rest_index( WP_REST_Response $response ) {
77 /** This filter is documented in wp-admin/includes/images.php */
78 $image_size_threshold = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
79
80 $default_image_output_formats = gutenberg_get_default_image_output_formats();
81
82 /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
83 $jpeg_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
84 /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
85 $png_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/png' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
86 /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
87 $gif_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
88
89 if ( current_user_can( 'upload_files' ) ) {
90 $response->data['image_sizes'] = gutenberg_get_all_image_sizes();
91 $response->data['image_size_threshold'] = $image_size_threshold;
92 $response->data['image_output_formats'] = (object) $default_image_output_formats;
93 $response->data['jpeg_interlaced'] = $jpeg_interlaced;
94 $response->data['png_interlaced'] = $png_interlaced;
95 $response->data['gif_interlaced'] = $gif_interlaced;
96 }
97
98 return $response;
99 }
100
101 add_filter( 'rest_index', 'gutenberg_media_processing_filter_rest_index' );
102
103
104 /**
105 * Overrides the REST controller for the attachment post type.
106 *
107 * @param array $args Array of arguments for registering a post type.
108 * See the register_post_type() function for accepted arguments.
109 * @param string $post_type Post type key.
110 */
111 function gutenberg_filter_attachment_post_type_args( array $args, string $post_type ): array {
112 if ( 'attachment' === $post_type ) {
113 require_once __DIR__ . '/class-gutenberg-rest-attachments-controller.php';
114
115 $args['rest_controller_class'] = Gutenberg_REST_Attachments_Controller::class;
116 }
117
118 return $args;
119 }
120
121 add_filter( 'register_post_type_args', 'gutenberg_filter_attachment_post_type_args', 10, 2 );
122
123
124 /**
125 * Registers additional REST fields for attachments.
126 */
127 function gutenberg_media_processing_register_rest_fields(): void {
128 register_rest_field(
129 'attachment',
130 'filename',
131 array(
132 'schema' => array(
133 'description' => __( 'Original attachment file name', 'gutenberg' ),
134 'type' => 'string',
135 'context' => array( 'view', 'edit' ),
136 ),
137 'get_callback' => 'gutenberg_rest_get_attachment_filename',
138 )
139 );
140
141 register_rest_field(
142 'attachment',
143 'filesize',
144 array(
145 'schema' => array(
146 'description' => __( 'Attachment file size', 'gutenberg' ),
147 'type' => 'number',
148 'context' => array( 'view', 'edit' ),
149 ),
150 'get_callback' => 'gutenberg_rest_get_attachment_filesize',
151 )
152 );
153 }
154
155 add_action( 'rest_api_init', 'gutenberg_media_processing_register_rest_fields' );
156
157 /**
158 * Returns the attachment's original file name.
159 *
160 * @param array $post Post data.
161 * @return string|null Attachment file name.
162 */
163 function gutenberg_rest_get_attachment_filename( array $post ): ?string {
164 $path = wp_get_original_image_path( $post['id'] );
165
166 if ( $path ) {
167 return basename( $path );
168 }
169
170 $path = get_attached_file( $post['id'] );
171
172 if ( $path ) {
173 return basename( $path );
174 }
175
176 return null;
177 }
178
179 /**
180 * Returns the attachment's file size in bytes.
181 *
182 * @param array $post Post data.
183 * @return int|null Attachment file size.
184 */
185 function gutenberg_rest_get_attachment_filesize( array $post ): ?int {
186 $attachment_id = $post['id'];
187
188 $meta = wp_get_attachment_metadata( $attachment_id );
189
190 if ( isset( $meta['filesize'] ) ) {
191 return $meta['filesize'];
192 }
193
194 $original_path = wp_get_original_image_path( $attachment_id );
195 $attached_file = $original_path ? $original_path : get_attached_file( $attachment_id );
196
197 if ( is_string( $attached_file ) && file_exists( $attached_file ) ) {
198 return wp_filesize( $attached_file );
199 }
200
201 return null;
202 }
203
204 /**
205 * Filters the list of rewrite rules formatted for output to an .htaccess file.
206 *
207 * Adds support for serving wasm-vips locally.
208 *
209 * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess.
210 * @return string Filtered rewrite rules.
211 */
212 function gutenberg_filter_mod_rewrite_rules( string $rules ): string {
213 $rules .= "\n# BEGIN Gutenberg client-side media processing\n" .
214 "AddType application/wasm wasm\n" .
215 "# END Gutenberg client-side media processing\n";
216
217 return $rules;
218 }
219
220 add_filter( 'mod_rewrite_rules', 'gutenberg_filter_mod_rewrite_rules' );
221
222 /**
223 * Returns the major Chromium version from the current request's User-Agent.
224 *
225 * Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave).
226 *
227 * @return int|null The major Chromium version, or null if not a Chromium browser.
228 */
229 function gutenberg_get_chromium_major_version(): ?int {
230 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
231 return null;
232 }
233 if ( preg_match( '/Chrome\/(\d+)/', $_SERVER['HTTP_USER_AGENT'], $matches ) ) {
234 return (int) $matches[1];
235 }
236 return null;
237 }
238
239 /**
240 * Enables cross-origin isolation in the block editor.
241 *
242 * Required for enabling SharedArrayBuffer for WebAssembly-based
243 * media processing in the editor. Uses Document-Isolation-Policy
244 * on supported browsers (Chromium 137+).
245 */
246 function gutenberg_set_up_cross_origin_isolation() {
247 // Re-check the filter at action time, since other plugins (loaded after Gutenberg)
248 // may have added a filter to disable client-side media processing.
249 if ( ! gutenberg_is_client_side_media_processing_enabled() ) {
250 return;
251 }
252
253 $screen = get_current_screen();
254
255 if ( ! $screen ) {
256 return;
257 }
258
259 if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
260 return;
261 }
262
263 // Skip when a third-party page builder overrides the block editor.
264 // DIP isolates the document into its own agent cluster,
265 // which blocks same-origin iframe access that these editors rely on.
266 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
267 if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
268 return;
269 }
270
271 $user_id = get_current_user_id();
272 if ( ! $user_id ) {
273 return;
274 }
275
276 // Cross-origin isolation is not needed if users can't upload files anyway.
277 if ( ! user_can( $user_id, 'upload_files' ) ) {
278 return;
279 }
280
281 gutenberg_start_cross_origin_isolation_output_buffer();
282 }
283
284 add_action( 'load-post.php', 'gutenberg_set_up_cross_origin_isolation' );
285 add_action( 'load-post-new.php', 'gutenberg_set_up_cross_origin_isolation' );
286 add_action( 'load-site-editor.php', 'gutenberg_set_up_cross_origin_isolation' );
287 add_action( 'load-widgets.php', 'gutenberg_set_up_cross_origin_isolation' );
288
289 // Remove core's COEP/COOP-based cross-origin isolation in favor of
290 // Gutenberg's DIP-based approach, which also skips third-party editors.
291 remove_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' );
292 remove_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' );
293 remove_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' );
294 remove_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' );
295
296 /**
297 * Sends the Document-Isolation-Policy header for cross-origin isolation.
298 *
299 * Uses an output buffer to add crossorigin="anonymous" where needed.
300 */
301 function gutenberg_start_cross_origin_isolation_output_buffer(): void {
302 $chromium_version = gutenberg_get_chromium_major_version();
303
304 /**
305 * Filters whether to use Document-Isolation-Policy for cross-origin isolation.
306 *
307 * Document-Isolation-Policy provides per-document cross-origin isolation
308 * without affecting other iframes on the page, avoiding breakage of plugins
309 * whose iframes lose credentials/DOM access.
310 *
311 * @since 21.8.0
312 *
313 * @param bool $use_dip Whether DIP is supported and should be used.
314 */
315 $use_dip = apply_filters(
316 'gutenberg_use_document_isolation_policy',
317 null !== $chromium_version && $chromium_version >= 137
318 );
319
320 if ( ! $use_dip ) {
321 return;
322 }
323
324 ob_start(
325 function ( string $output ): string {
326 header( 'Document-Isolation-Policy: isolate-and-credentialless' );
327
328 return gutenberg_add_crossorigin_attributes( $output );
329 }
330 );
331 }
332
333 /**
334 * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
335 *
336 * @param string $html HTML input.
337 *
338 * @return string Modified HTML.
339 */
340 function gutenberg_add_crossorigin_attributes( string $html ): string {
341 $site_url = site_url();
342
343 $processor = new WP_HTML_Tag_Processor( $html );
344
345 // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
346 $tags = array(
347 'AUDIO' => 'src',
348 'IMG' => 'src',
349 'LINK' => 'href',
350 'SCRIPT' => 'src',
351 'VIDEO' => 'src',
352 'SOURCE' => 'src',
353 );
354
355 $tag_names = array_keys( $tags );
356
357 while ( $processor->next_tag() ) {
358 $tag = $processor->get_tag();
359
360 if ( ! in_array( $tag, $tag_names, true ) ) {
361 continue;
362 }
363
364 if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
365 $processor->set_bookmark( 'audio-video-parent' );
366 }
367
368 $processor->set_bookmark( 'resume' );
369
370 $sought = false;
371
372 $crossorigin = $processor->get_attribute( 'crossorigin' );
373
374 $url = $processor->get_attribute( $tags[ $tag ] );
375
376 if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) {
377 if ( 'SOURCE' === $tag ) {
378 $sought = $processor->seek( 'audio-video-parent' );
379
380 if ( $sought ) {
381 $processor->set_attribute( 'crossorigin', 'anonymous' );
382 }
383 } else {
384 $processor->set_attribute( 'crossorigin', 'anonymous' );
385 }
386
387 if ( $sought ) {
388 $processor->seek( 'resume' );
389 $processor->release_bookmark( 'audio-video-parent' );
390 }
391 }
392 }
393
394 return $processor->get_updated_html();
395 }
396
397 /**
398 * Overrides templates from wp_print_media_templates with custom ones.
399 *
400 * Adds `crossorigin` attribute to all tags that
401 * could have assets loaded from a different domain.
402 */
403 function gutenberg_override_media_templates(): void {
404 remove_action( 'admin_footer', 'wp_print_media_templates' );
405 add_action(
406 'admin_footer',
407 static function (): void {
408 ob_start();
409 wp_print_media_templates();
410 $html = (string) ob_get_clean();
411
412 $tags = array(
413 'audio',
414 'img',
415 'video',
416 );
417
418 foreach ( $tags as $tag ) {
419 $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html );
420 }
421
422 echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
423 }
424 );
425 }
426
427 add_action( 'wp_enqueue_media', 'gutenberg_override_media_templates' );
428