PluginProbe
Gutenberg / 22.7.0
Gutenberg v22.7.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 12.6.0 All 403 releases
← All changes | lib/media/class-gutenberg-rest-attachments-controller.php +56 -961 23.6.2 → 22.7.0 View file →
@@ -12,62 +12,8 @@
12 12 * functionality including sideload support and sub-size generation control.
13 13 */
14 14 class Gutenberg_REST_Attachments_Controller extends WP_REST_Attachments_Controller {
15 15 /**
16 - * Image size token for the source-format original preserved alongside a
17 - * client-generated derivative (e.g. the HEIC file kept next to its JPEG).
18 - *
19 - * Used both in the `/sideload` route schema and when dispatching the
20 - * sideloaded file to its metadata key, so the two never drift apart.
21 - *
22 - * @var string
23 - */
24 - const IMAGE_SIZE_SOURCE_ORIGINAL = 'source_original';
25 -
26 - /**
27 - * Metadata key holding the basename of the source-format original.
28 - *
29 - * Deliberately specific so it never collides with the generic `original`
30 - * or `original_image` keys other flows write to.
31 - *
32 - * @var string
33 - */
34 - const META_KEY_SOURCE_IMAGE = 'source_image';
35 -
36 - /**
37 - * Image size token for the video transcoded from an animated GIF, sideloaded
38 - * as a companion of the GIF attachment.
39 - *
40 - * Paired with META_KEY_ANIMATED_VIDEO: used both in the `/sideload` route
41 - * and when writing the sideloaded file to its metadata key. Both use the
42 - * underscore convention so the size token and meta key stay consistent.
43 - *
44 - * @var string
45 - */
46 - const IMAGE_SIZE_ANIMATED_VIDEO = 'animated_video';
47 -
48 - /**
49 - * Image size token for the static first-frame poster of a converted GIF.
50 - *
51 - * @var string
52 - */
53 - const IMAGE_SIZE_ANIMATED_VIDEO_POSTER = 'animated_video_poster';
54 -
55 - /**
56 - * Metadata key holding the basename of the converted animated-GIF video.
57 - *
58 - * @var string
59 - */
60 - const META_KEY_ANIMATED_VIDEO = 'animated_video';
61 -
62 - /**
63 - * Metadata key holding the basename of the converted GIF's poster image.
64 - *
65 - * @var string
66 - */
67 - const META_KEY_ANIMATED_VIDEO_POSTER = 'animated_video_poster';
68 -
69 - /**
70 16 * Registers the routes for attachments.
71 17 *
72 18 * @see register_rest_route()
73 19 */
@@ -73,8 +19,20 @@
73 19 */
74 20 public function register_routes(): void {
75 21 parent::register_routes();
76 22
23 + // Override the parent's sideload route so that 'scaled' is included
24 + // in the image_size enum. Without the override, core's handler
25 + // validates first and rejects 'scaled' before ours is tried.
26 + $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );
27 +
28 + // Special case to set 'original_image' in attachment metadata.
29 + $valid_image_sizes[] = 'original';
30 + // Client-side big image threshold: sideload the scaled version.
31 + $valid_image_sizes[] = 'scaled';
32 + // Used for PDF thumbnails.
33 + $valid_image_sizes[] = 'full';
34 +
77 35 register_rest_route(
78 36 $this->namespace,
79 37 '/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',
80 38 array(
@@ -82,62 +40,18 @@
82 40 'methods' => WP_REST_Server::CREATABLE,
83 41 'callback' => array( $this, 'sideload_item' ),
84 42 'permission_callback' => array( $this, 'sideload_item_permissions_check' ),
85 43 'args' => array(
86 - 'id' => array(
44 + 'id' => array(
87 45 'description' => __( 'Unique identifier for the attachment.', 'gutenberg' ),
88 46 'type' => 'integer',
89 47 ),
90 - 'image_size' => array(
91 - 'description' => __( 'Image size. Can be a single size name or an array of size names to register the same file under multiple sizes.', 'gutenberg' ),
92 - 'type' => array( 'string', 'array' ),
93 - 'items' => array(
94 - 'type' => 'string',
95 - ),
96 - 'required' => true,
97 - // A custom callback is used instead of the default `rest_validate_request_arg`
98 - // because WordPress's `rest_is_array()` treats scalar strings as single-element
99 - // lists (via wp_parse_list), so a oneOf with both a string and array schema
100 - // matches a plain string twice and validation fails with "matches more than one
101 - // of the expected formats". The callback validates the enum per-item using the
102 - // current list of registered sizes, which reflects any sizes added after the
103 - // route was registered (e.g. via add_image_size() in tests).
104 - 'validate_callback' => static function ( $value, $request, $param ) {
105 - $valid_sizes = array_keys( wp_get_registered_image_subsizes() );
106 - $valid_sizes[] = 'original';
107 - $valid_sizes[] = self::IMAGE_SIZE_SOURCE_ORIGINAL;
108 - $valid_sizes[] = self::IMAGE_SIZE_ANIMATED_VIDEO;
109 - $valid_sizes[] = self::IMAGE_SIZE_ANIMATED_VIDEO_POSTER;
110 - $valid_sizes[] = 'scaled';
111 - $valid_sizes[] = 'full';
112 -
113 - $items = is_string( $value ) ? array( $value ) : ( is_array( $value ) ? $value : null );
114 - if ( null === $items ) {
115 - return new WP_Error(
116 - 'rest_invalid_type',
117 - /* translators: %s: Parameter name. */
118 - sprintf( __( '%s must be a string or an array of strings.', 'gutenberg' ), $param )
119 - );
120 - }
121 -
122 - foreach ( $items as $item ) {
123 - if ( ! is_string( $item ) || ! in_array( $item, $valid_sizes, true ) ) {
124 - return new WP_Error(
125 - 'rest_not_in_enum',
126 - /* translators: %s: Parameter name. */
127 - sprintf( __( '%s contains an invalid image size.', 'gutenberg' ), $param )
128 - );
129 - }
130 - }
131 -
132 - return true;
133 - },
48 + 'image_size' => array(
49 + 'description' => __( 'Image size.', 'gutenberg' ),
50 + 'type' => 'string',
51 + 'enum' => $valid_image_sizes,
52 + 'required' => true,
134 53 ),
135 - 'convert_format' => array(
136 - 'description' => __( 'Whether to convert image formats.', 'gutenberg' ),
137 - 'type' => 'boolean',
138 - 'default' => true,
139 - ),
140 54 ),
141 55 ),
142 56 'allow_batch' => $this->allow_batch,
143 57 'schema' => array( $this, 'get_public_item_schema' ),
@@ -143,133 +57,17 @@
143 57 'schema' => array( $this, 'get_public_item_schema' ),
144 58 ),
145 59 true // Override core's route so 'scaled' is included in the enum.
146 60 );
147 -
148 - register_rest_route(
149 - $this->namespace,
150 - '/' . $this->rest_base . '/(?P<id>[\d]+)/finalize',
151 - array(
152 - array(
153 - 'methods' => WP_REST_Server::CREATABLE,
154 - 'callback' => array( $this, 'finalize_item' ),
155 - 'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
156 - 'args' => array(
157 - 'id' => array(
158 - 'description' => __( 'Unique identifier for the attachment.', 'gutenberg' ),
159 - 'type' => 'integer',
160 - ),
161 - 'sub_sizes' => array(
162 - 'description' => __( 'Array of sub-size metadata collected from sideload responses.', 'gutenberg' ),
163 - 'type' => 'array',
164 - 'default' => array(),
165 - 'items' => array(
166 - 'type' => 'object',
167 - 'properties' => array(
168 - 'image_size' => array(
169 - // Uses a multi-type schema instead of `oneOf` because WordPress's
170 - // `rest_is_array()` treats scalar strings as single-element lists,
171 - // so both a `{type: string}` and `{type: array}` oneOf schema would
172 - // match a plain string and trigger a "matches more than one"
173 - // validation error.
174 - 'description' => __( 'Size name, or an array of size names when a single file is registered under multiple sizes with matching dimensions.', 'gutenberg' ),
175 - 'type' => array( 'string', 'array' ),
176 - 'items' => array(
177 - 'type' => 'string',
178 - ),
179 - 'required' => true,
180 - ),
181 - 'width' => array(
182 - 'type' => 'integer',
183 - 'minimum' => 1,
184 - ),
185 - 'height' => array(
186 - 'type' => 'integer',
187 - 'minimum' => 1,
188 - ),
189 - 'file' => array(
190 - 'type' => 'string',
191 - 'minLength' => 1,
192 - ),
193 - 'mime_type' => array(
194 - 'type' => 'string',
195 - 'pattern' => '^image/.*',
196 - ),
197 - 'filesize' => array(
198 - 'type' => 'integer',
199 - 'minimum' => 1,
200 - ),
201 - 'original_image' => array(
202 - 'type' => 'string',
203 - 'minLength' => 1,
204 - ),
205 - ),
206 - ),
207 - ),
208 - ),
209 - ),
210 - 'allow_batch' => $this->allow_batch,
211 - 'schema' => array( $this, 'get_public_item_schema' ),
212 - )
213 - );
214 61 }
215 62
216 63 /**
217 - * Checks if a given request has access to create an attachment.
218 - *
219 - * Skips the server-side image type support check when the client
220 - * will handle image processing (generate_sub_sizes is false). Still
221 - * HEIC/HEIF uploads always skip the check, since the browser's canvas
222 - * fallback can decode them even when the server cannot.
223 - *
224 - * @param WP_REST_Request $request Full details about the request.
225 - * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
226 - */
227 - public function create_item_permissions_check( $request ) {
228 - $bypass_mime_check = false === $request['generate_sub_sizes'];
229 -
230 - /*
231 - * Always allow still HEIC/HEIF uploads through even if the server's
232 - * image editor doesn't support them. The client-side canvas fallback
233 - * handles processing using the browser's native HEVC decoder.
234 - *
235 - * The '-sequence' variants (multi-frame Live Photos) are deliberately
236 - * excluded: neither the server nor the browser fallback can process
237 - * them yet, so they should fall through to the standard unsupported
238 - * mime-type error rather than be stored unprocessable.
239 - */
240 - if ( ! $bypass_mime_check ) {
241 - $still_heic_mime_types = array( 'image/heic', 'image/heif' );
242 - $files = $request->get_file_params();
243 -
244 - if (
245 - ! empty( $files['file']['type'] ) &&
246 - in_array( $files['file']['type'], $still_heic_mime_types, true )
247 - ) {
248 - $bypass_mime_check = true;
249 - }
250 - }
251 -
252 - if ( $bypass_mime_check ) {
253 - add_filter( 'wp_prevent_unsupported_mime_type_uploads', '__return_false' );
254 - }
255 -
256 - $result = parent::create_item_permissions_check( $request );
257 -
258 - if ( $bypass_mime_check ) {
259 - remove_filter( 'wp_prevent_unsupported_mime_type_uploads', '__return_false' );
260 - }
261 -
262 - return $result;
263 - }
264 -
265 - /**
266 64 * Retrieves an array of endpoint arguments from the item schema for the controller.
267 65 *
268 66 * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
269 67 * checked for required values and may fall-back to a given default, this is not done
270 68 * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
271 - * @return array<string, array<string, mixed>> Endpoint arguments keyed by argument name.
69 + * @return array Endpoint arguments.
272 70 */
273 71 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
274 72 $args = rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method );
275 73
@@ -283,41 +81,8 @@
283 81 'type' => 'boolean',
284 82 'default' => true,
285 83 'description' => __( 'Whether to convert image formats.', 'gutenberg' ),
286 84 );
287 - $args['url'] = array(
288 - 'type' => 'string',
289 - 'format' => 'uri',
290 - 'description' => __( 'URL of an external image to sideload into the media library, instead of uploading a file.', 'gutenberg' ),
291 - 'sanitize_callback' => 'sanitize_url',
292 - 'validate_callback' => static function ( $url, WP_REST_Request $request, string $param ) {
293 - /*
294 - * A custom validate_callback replaces the default
295 - * rest_validate_request_arg(), so re-apply it first to keep
296 - * the schema checks (string type, uri format) enforced.
297 - */
298 - $valid = rest_validate_request_arg( $url, $request, $param );
299 - if ( is_wp_error( $valid ) ) {
300 - return $valid;
301 - }
302 - /** @var non-empty-string $url */
303 -
304 - /*
305 - * Reject URLs that are not safe to request server-side. wp_http_validate_url()
306 - * enforces an HTTP(S) scheme and blocks private, local, and otherwise
307 - * disallowed hosts, guarding the sideload against SSRF.
308 - */
309 - if ( false === wp_http_validate_url( $url ) ) {
310 - return new WP_Error(
311 - 'rest_invalid_url',
312 - __( 'Invalid URL. Provide a valid, publicly reachable HTTP or HTTPS image URL.', 'gutenberg' ),
313 - array( 'status' => 400 )
314 - );
315 - }
316 -
317 - return true;
318 - },
319 - );
320 85 }
321 86
322 87 return $args;
323 88 }
@@ -338,51 +103,8 @@
338 103 'context' => array( 'edit' ),
339 104 'readonly' => true,
340 105 );
341 106
342 - $schema['properties']['image_output_format'] = array(
343 - 'description' => __( 'The output MIME type this image should be converted to, based on the image_editor_output_format filter. Null if no conversion is needed.', 'gutenberg' ),
344 - 'type' => array( 'string', 'null' ),
345 - 'context' => array( 'edit' ),
346 - 'readonly' => true,
347 - );
348 -
349 - $schema['properties']['image_save_progressive'] = array(
350 - 'description' => __( 'Whether to use progressive/interlaced encoding when saving this image.', 'gutenberg' ),
351 - 'type' => 'boolean',
352 - 'context' => array( 'edit' ),
353 - 'readonly' => true,
354 - );
355 -
356 - // Enumerate the registered sub-sizes so the schema documents exactly which
357 - // keys may appear under "sizes".
358 - $size_quality_properties = array();
359 - foreach ( array_keys( wp_get_registered_image_subsizes() ) as $size_name ) {
360 - $size_quality_properties[ $size_name ] = array(
361 - 'type' => 'integer',
362 - 'minimum' => 1,
363 - 'maximum' => 100,
364 - );
365 - }
366 -
367 - $schema['properties']['image_quality'] = array(
368 - 'description' => __( 'Encode quality (1-100) from the wp_editor_set_quality filter, resolved against the output MIME type. "default" applies to the full-size image; "sizes" lists per-registered-size overrides where the filtered value differs from "default".', 'gutenberg' ),
369 - 'type' => 'object',
370 - 'context' => array( 'edit' ),
371 - 'readonly' => true,
372 - 'properties' => array(
373 - 'default' => array(
374 - 'type' => 'integer',
375 - 'minimum' => 1,
376 - 'maximum' => 100,
377 - ),
378 - 'sizes' => array(
379 - 'type' => 'object',
380 - 'properties' => $size_quality_properties,
381 - ),
382 - ),
383 - );
384 -
385 107 return $schema;
386 108 }
387 109
388 110 /**
@@ -425,95 +147,8 @@
425 147 $data['exif_orientation'] = $orientation;
426 148 }
427 149 }
428 150
429 - // Add per-file output format for images.
430 - if ( rest_is_field_included( 'image_output_format', $fields ) ) {
431 - if ( wp_attachment_is_image( $item ) ) {
432 - $mime_type = get_post_mime_type( $item );
433 - $filename = get_attached_file( $item->ID );
434 -
435 - /** This filter is documented in wp-includes/class-wp-image-editor.php */
436 - $output_formats = apply_filters(
437 - 'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
438 - array( $mime_type => $mime_type ),
439 - $filename ? $filename : '',
440 - $mime_type
441 - );
442 -
443 - $output_mime = $output_formats[ $mime_type ] ?? $mime_type;
444 - $data['image_output_format'] = ( $output_mime !== $mime_type ) ? $output_mime : null;
445 - }
446 - }
447 -
448 - // Add progressive/interlaced encoding setting for images.
449 - if ( rest_is_field_included( 'image_save_progressive', $fields ) ) {
450 - if ( wp_attachment_is_image( $item ) ) {
451 - $mime_type = get_post_mime_type( $item );
452 -
453 - /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
454 - $data['image_save_progressive'] = (bool) apply_filters(
455 - 'image_save_progressive', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
456 - false,
457 - $mime_type
458 - );
459 - }
460 - }
461 -
462 - // Add per-file, size-aware encode quality for images.
463 - if ( rest_is_field_included( 'image_quality', $fields ) ) {
464 - if ( wp_attachment_is_image( $item ) ) {
465 - $mime_type = (string) get_post_mime_type( $item );
466 - $filename = get_attached_file( $item->ID );
467 -
468 - // Resolve the output MIME type the same way core's
469 - // WP_Image_Editor::set_quality() does: quality is filtered
470 - // against the format the file will actually be saved as.
471 - /** This filter is documented in wp-includes/class-wp-image-editor.php */
472 - $output_formats = apply_filters(
473 - 'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
474 - array( $mime_type => $mime_type ),
475 - $filename ? $filename : '',
476 - $mime_type
477 - );
478 - $output_mime = $output_formats[ $mime_type ] ?? $mime_type;
479 -
480 - $metadata = wp_get_attachment_metadata( $item->ID, true );
481 - $full_width = max( 0, ( is_array( $metadata ) && isset( $metadata['width'] ) ) ? (int) $metadata['width'] : 0 );
482 - $full_height = max( 0, ( is_array( $metadata ) && isset( $metadata['height'] ) ) ? (int) $metadata['height'] : 0 );
483 -
484 - $full_quality = $this->get_image_encode_quality(
485 - $output_mime,
486 - array(
487 - 'width' => $full_width,
488 - 'height' => $full_height,
489 - )
490 - );
491 -
492 - $size_quality = array();
493 - foreach ( wp_get_registered_image_subsizes() as $size_name => $size_data ) {
494 - $quality = $this->get_image_encode_quality(
495 - $output_mime,
496 - array(
497 - 'width' => (int) $size_data['width'],
498 - 'height' => (int) $size_data['height'],
499 - )
500 - );
501 -
502 - // Only report sizes that diverge from the full-size value
503 - // to keep the response payload small.
504 - if ( $quality !== $full_quality ) {
505 - $size_quality[ $size_name ] = $quality;
506 - }
507 - }
508 -
509 - $data['image_quality'] = array(
510 - 'default' => $full_quality,
511 - 'sizes' => $size_quality,
512 - );
513 - }
514 - }
515 -
516 151 if (
517 152 rest_is_field_included( 'missing_image_sizes', $fields ) &&
518 153 empty( $data['missing_image_sizes'] )
519 154 ) {
@@ -585,19 +220,9 @@
585 220 if ( ! $request['convert_format'] ) {
586 221 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
587 222 }
588 223
589 - /*
590 - * When a URL is supplied instead of an uploaded file, sideload the
591 - * remote image on the server. This avoids a cross-origin browser fetch,
592 - * which fails under cross-origin isolation. The sub-size and scaling
593 - * filters applied above still govern whether derivatives are generated.
594 - */
595 - if ( ! empty( $request['url'] ) ) {
596 - $response = $this->create_item_from_url( $request );
597 - } else {
598 - $response = parent::create_item( $request );
599 - }
224 + $response = parent::create_item( $request );
600 225
601 226 remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
602 227 remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
603 228 remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );
@@ -603,280 +228,13 @@
603 228 remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );
604 229 remove_filter( 'big_image_size_threshold', '__return_zero', 100 );
605 230 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
606 231
607 - // Recompute image_output_format now that __return_empty_array is removed.
608 - if ( ! is_wp_error( $response ) ) {
609 - $data = $response->get_data();
610 - if ( ! empty( $data['id'] ) && wp_attachment_is_image( $data['id'] ) ) {
611 - $mime_type = get_post_mime_type( $data['id'] );
612 - $filename = get_attached_file( $data['id'] );
613 -
614 - /** This filter is documented in wp-includes/class-wp-image-editor.php */
615 - $output_formats = apply_filters(
616 - 'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
617 - array( $mime_type => $mime_type ),
618 - $filename ? $filename : '',
619 - $mime_type
620 - );
621 -
622 - $output_mime = $output_formats[ $mime_type ] ?? $mime_type;
623 - $data['image_output_format'] = ( $output_mime !== $mime_type ) ? $output_mime : null;
624 -
625 - /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
626 - $data['image_save_progressive'] = (bool) apply_filters(
627 - 'image_save_progressive', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
628 - false,
629 - $mime_type
630 - );
631 -
632 - $response->set_data( $data );
633 - }
634 - }
635 -
636 232 return $response;
637 233 }
638 234
639 - /**
640 - * Sideloads an external image from a URL into the media library.
641 - *
642 - * Downloads the remote file on the server, avoiding a cross-origin browser
643 - * fetch that fails under cross-origin isolation. Whether sub-sizes are
644 - * generated is governed by the filters applied in create_item().
645 - *
646 - * @param WP_REST_Request $request Full details about the request.
647 - * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
648 - */
649 - protected function create_item_from_url( $request ) {
650 - // Sideloading downloads and stores a file, so require the upload capability.
651 - if ( ! current_user_can( 'upload_files' ) ) {
652 - return new WP_Error(
653 - 'rest_cannot_create',
654 - __( 'Sorry, you are not allowed to upload media on this site.', 'gutenberg' ),
655 - array( 'status' => rest_authorization_required_code() )
656 - );
657 - }
658 235
659 - require_once ABSPATH . 'wp-admin/includes/file.php';
660 - require_once ABSPATH . 'wp-admin/includes/media.php';
661 - require_once ABSPATH . 'wp-admin/includes/image.php';
662 -
663 - $url = $request['url'];
664 - $post_id = ! empty( $request['post'] ) ? (int) $request['post'] : 0;
665 -
666 - // Derive the filename from the URL path before downloading anything.
667 - $url_path = wp_parse_url( $url, PHP_URL_PATH );
668 - $filename = $url_path ? wp_basename( $url_path ) : '';
669 - if ( '' === $filename ) {
670 - return new WP_Error(
671 - 'rest_invalid_url',
672 - __( 'Could not determine a filename from the provided URL.', 'gutenberg' ),
673 - array( 'status' => 400 )
674 - );
675 - }
676 -
677 - /*
678 - * Only download URLs whose extension maps to an allowed image MIME type.
679 - * The sideload handler would reject other types anyway (via
680 - * wp_check_filetype_and_ext()), but checking first avoids downloading
681 - * files that can never be accepted, such as PHP scripts.
682 - */
683 - $filetype = wp_check_filetype( $filename );
684 - if ( ! $filetype['type'] || ! str_starts_with( $filetype['type'], 'image/' ) ) {
685 - return new WP_Error(
686 - 'rest_invalid_url',
687 - __( 'The provided URL does not point to a supported image file.', 'gutenberg' ),
688 - array( 'status' => 400 )
689 - );
690 - }
691 -
692 - /*
693 - * Download the remote file with WordPress's HTTP API, which validates
694 - * the host and blocks requests to private or local addresses. This is
695 - * the same primitive core's media_sideload_image() relies on.
696 - */
697 - $tmp_file = download_url( $url );
698 - if ( is_wp_error( $tmp_file ) ) {
699 - return $tmp_file;
700 - }
701 -
702 - $file_array = array(
703 - 'name' => $filename,
704 - 'tmp_name' => $tmp_file,
705 - );
706 -
707 - $attachment_id = media_handle_sideload( $file_array, $post_id );
708 -
709 - if ( is_wp_error( $attachment_id ) ) {
710 - /*
711 - * media_handle_sideload() deletes the temp file on success; remove
712 - * it explicitly when the sideload fails.
713 - */
714 - if ( file_exists( $tmp_file ) ) {
715 - wp_delete_file( $tmp_file );
716 - }
717 - return $attachment_id;
718 - }
719 -
720 - $attachment = get_post( $attachment_id );
721 -
722 - $request->set_param( 'context', 'edit' );
723 -
724 - /*
725 - * media_handle_sideload() fires the standard insert hooks (including
726 - * wp_after_insert_post), but not the REST-specific action, so fire it
727 - * here for parity with the uploaded-file path in create_item().
728 - */
729 - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
730 - do_action( 'rest_after_insert_attachment', $attachment, $request, true );
731 -
732 - $response = $this->prepare_item_for_response( $attachment, $request );
733 - $response->set_status( 201 );
734 - $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );
735 -
736 - return $response;
737 - }
738 -
739 236 /**
740 - * Finalizes an attachment after client-side media processing.
741 - *
742 - * Triggers the {@see 'wp_generate_attachment_metadata'} filter so that
743 - * server-side plugins can process the attachment after all client-side
744 - * operations (upload, thumbnail generation, sideloads) are complete.
745 - *
746 - * @param WP_REST_Request $request Full details about the request.
747 - * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
748 - */
749 - public function finalize_item( WP_REST_Request $request ) {
750 - $attachment_id = $request['id'];
751 -
752 - $post = $this->get_post( $attachment_id );
753 -
754 - if ( is_wp_error( $post ) ) {
755 - return $post;
756 - }
757 -
758 - $metadata = wp_get_attachment_metadata( $attachment_id );
759 -
760 - if ( ! is_array( $metadata ) ) {
761 - $metadata = array();
762 - }
763 -
764 - // Apply all sub-size metadata collected from sideload responses.
765 - $sub_sizes = $request['sub_sizes'] ?? array();
766 -
767 - foreach ( $sub_sizes as $sub_size ) {
768 - $image_size = $sub_size['image_size'];
769 -
770 - // When multiple size names share identical dimensions the client
771 - // sends a single sub-size entry with an array of names. Register the
772 - // same file under each name. Arrays only contain regular sizes.
773 - if ( is_array( $image_size ) ) {
774 - $metadata['sizes'] = $metadata['sizes'] ?? array();
775 -
776 - foreach ( $image_size as $name ) {
777 - $metadata['sizes'][ $name ] = array(
778 - 'width' => $sub_size['width'] ?? 0,
779 - 'height' => $sub_size['height'] ?? 0,
780 - 'file' => $sub_size['file'] ?? '',
781 - 'mime-type' => $sub_size['mime_type'] ?? '',
782 - 'filesize' => $sub_size['filesize'] ?? 0,
783 - );
784 - }
785 - continue;
786 - }
787 -
788 - if ( 'original' === $image_size || 'scaled' === $image_size ) {
789 - // Skip malformed entries so a bad payload cannot blank out the
790 - // main file metadata.
791 - if ( empty( $sub_size['file'] ) ) {
792 - continue;
793 - }
794 -
795 - // Record the supplied full-size image (from sideload_item()) as
796 - // the main file, keeping the current attached file as
797 - // `original_image`. A 'scaled' image is downsized and an
798 - // 'original' image is rotated; both have any EXIF orientation
799 - // already applied by the client.
800 - if ( ! empty( $sub_size['original_image'] ) ) {
801 - $metadata['original_image'] = $sub_size['original_image'];
802 - }
803 - $metadata['width'] = $sub_size['width'] ?? 0;
804 - $metadata['height'] = $sub_size['height'] ?? 0;
805 - $metadata['filesize'] = $sub_size['filesize'] ?? 0;
806 - $metadata['file'] = $sub_size['file'];
807 -
808 - // The supplied image has its orientation applied already, so
809 - // reset the stored value (from the upload) to 1, as
810 - // wp_create_image_subsizes() does for both its scale and rotate
811 - // paths. Otherwise exif_orientation would still report the
812 - // pre-rotation value and the client would rotate the image
813 - // again on a re-fetch.
814 - if ( ! empty( $metadata['image_meta']['orientation'] ) ) {
815 - $metadata['image_meta']['orientation'] = 1;
816 - }
817 - } elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
818 - // Source-format original: stored under its own meta key so the
819 - // scaled-sideload flow (which writes 'original_image') cannot
820 - // clobber it. 'original_image' keeps pointing at the
821 - // web-viewable JPEG derivative. Cleanup on attachment delete
822 - // is handled by a delete_attachment hook that reads this key.
823 - $metadata[ self::META_KEY_SOURCE_IMAGE ] = $sub_size['file'];
824 - } elseif ( self::IMAGE_SIZE_ANIMATED_VIDEO === $image_size ) {
825 - // Converted video companion of an animated GIF. Stored
826 - // under its own key; the GIF stays the attachment. The
827 - // editor reads this key to switch the block to a video;
828 - // companion cleanup lives in lib/media/animated-gif-to-video.php.
829 - $metadata[ self::META_KEY_ANIMATED_VIDEO ] = $sub_size['file'];
830 - } elseif ( self::IMAGE_SIZE_ANIMATED_VIDEO_POSTER === $image_size ) {
831 - // Static first-frame poster for the converted video. Used as
832 - // the video block's poster and deleted alongside the video.
833 - // See lib/media/animated-gif-to-video.php.
834 - $metadata[ self::META_KEY_ANIMATED_VIDEO_POSTER ] = $sub_size['file'];
835 - } else {
836 - $metadata['sizes'] = $metadata['sizes'] ?? array();
837 -
838 - $metadata['sizes'][ $image_size ] = array(
839 - 'width' => $sub_size['width'] ?? 0,
840 - 'height' => $sub_size['height'] ?? 0,
841 - 'file' => $sub_size['file'] ?? '',
842 - 'mime-type' => $sub_size['mime_type'] ?? '',
843 - 'filesize' => $sub_size['filesize'] ?? 0,
844 - );
845 - }
846 - }
847 -
848 - /**
849 - * Filters the attachment metadata after client-side processing.
850 - *
851 - * This re-applies the wp_generate_attachment_metadata filter so that
852 - * server-side plugins (e.g. those adding custom image sizes or
853 - * processing metadata) can run after client-side uploads are complete.
854 - *
855 - * @param array $metadata Attachment metadata.
856 - * @param int $attachment_id Attachment ID.
857 - * @param string $context Context: 'create' or 'update'.
858 - */
859 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
860 - $metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );
861 -
862 - wp_update_attachment_metadata( $attachment_id, $metadata );
863 -
864 - $response_request = new WP_REST_Request(
865 - WP_REST_Server::READABLE,
866 - rest_get_route_for_post( $attachment_id )
867 - );
868 -
869 - $response_request['context'] = 'edit';
870 -
871 - if ( isset( $request['_fields'] ) ) {
872 - $response_request['_fields'] = $request['_fields'];
873 - }
874 -
875 - return $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );
876 - }
877 -
878 - /**
879 237 * Checks if a given request has access to sideload a file.
880 238 *
881 239 * Sideloading a file for an existing attachment
882 240 * requires both update and create permissions.
@@ -924,9 +282,9 @@
924 282
925 283 $matches = array();
926 284 if ( preg_match( '/(.*)(-\d+x\d+|-scaled)-' . $number . '$/', $name, $matches ) ) {
927 285 $filename_without_suffix = $matches[1] . $matches[2] . ".$ext";
928 - if ( $matches[1] === $orig_name ) {
286 + if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) {
929 287 return $filename_without_suffix;
930 288 }
931 289 }
932 290
@@ -933,148 +291,8 @@
933 291 return $filename;
934 292 }
935 293
936 294 /**
937 - * Validates that uploaded image dimensions are appropriate for the specified image size.
938 - *
939 - * @param int $width Uploaded image width.
940 - * @param int $height Uploaded image height.
941 - * @param string|array $image_size The target image size name, or an array
942 - * of names that share the same dimensions.
943 - * @param int $attachment_id The attachment ID.
944 - * @return true|WP_Error True if valid, WP_Error if invalid.
945 - */
946 - private function validate_image_dimensions( int $width, int $height, $image_size, int $attachment_id ) {
947 - // 'animated_video' companion file: video, not an image. Skip *all*
948 - // dimension checks (the caller passes (0, 0) for this case so the
949 - // positive-dimension assertion below would otherwise fire).
950 - if ( self::IMAGE_SIZE_ANIMATED_VIDEO === $image_size ) {
951 - return true;
952 - }
953 -
954 - // Source-format original companion file: no dimension constraint, and
955 - // the caller passes (0, 0) because the source format (e.g. HEIC) may
956 - // not be readable by wp_getimagesize() at all.
957 - if ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
958 - return true;
959 - }
960 -
961 - // Dimensions must be positive for all sizes.
962 - if ( $width <= 0 || $height <= 0 ) {
963 - return new WP_Error(
964 - 'rest_upload_invalid_dimensions',
965 - __( 'Uploaded image must have positive dimensions.', 'gutenberg' ),
966 - array( 'status' => 400 )
967 - );
968 - }
969 -
970 - // Arrays only contain regular sub-size names that share dimensions.
971 - // Validate each one against its registered constraints.
972 - if ( is_array( $image_size ) ) {
973 - foreach ( $image_size as $name ) {
974 - $result = $this->validate_image_dimensions( $width, $height, $name, $attachment_id );
975 - if ( is_wp_error( $result ) ) {
976 - return $result;
977 - }
978 - }
979 - return true;
980 - }
981 -
982 - // 'animated_video_poster' companion: a static poster image for the
983 - // converted video. It is a real image (so it has positive dimensions)
984 - // but is not a registered sub-size, so it has no dimension constraint.
985 - if ( self::IMAGE_SIZE_ANIMATED_VIDEO_POSTER === $image_size ) {
986 - return true;
987 - }
988 -
989 - // 'original' size: the full-size image that replaces the main file (see
990 - // sideload_item()/finalize_item()). The endpoint expects any EXIF
991 - // orientation to be applied to the image already, which can swap width
992 - // and height, so the dimensions must match the stored dimensions or be
993 - // their transpose.
994 - if ( 'original' === $image_size ) {
995 - $metadata = wp_get_attachment_metadata( $attachment_id, true );
996 - if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) {
997 - $expected_width = (int) $metadata['width'];
998 - $expected_height = (int) $metadata['height'];
999 -
1000 - $matches_dimensions = $width === $expected_width && $height === $expected_height;
1001 - $transposes_dimensions = $width === $expected_height && $height === $expected_width;
1002 -
1003 - if ( ! $matches_dimensions && ! $transposes_dimensions ) {
1004 - return new WP_Error(
1005 - 'rest_upload_dimension_mismatch',
1006 - sprintf(
1007 - /* translators: 1: actual width, 2: actual height, 3: expected width, 4: expected height */
1008 - __( 'Uploaded image dimensions (%1$dx%2$d) do not match original image dimensions (%3$dx%4$d).', 'gutenberg' ),
1009 - $width,
1010 - $height,
1011 - $expected_width,
1012 - $expected_height
1013 - ),
1014 - array( 'status' => 400 )
1015 - );
1016 - }
1017 - }
1018 - return true;
1019 - }
1020 -
1021 - // 'full' size (PDF thumbnails) and 'scaled': no further constraints.
1022 - if ( 'full' === $image_size || 'scaled' === $image_size ) {
1023 - return true;
1024 - }
1025 -
1026 - // Regular image sizes: validate against registered size constraints.
1027 - $registered_sizes = wp_get_registered_image_subsizes();
1028 -
1029 - if ( ! isset( $registered_sizes[ $image_size ] ) ) {
1030 - return new WP_Error(
1031 - 'rest_upload_unknown_size',
1032 - __( 'Unknown image size.', 'gutenberg' ),
1033 - array( 'status' => 400 )
1034 - );
1035 - }
1036 -
1037 - $size_data = $registered_sizes[ $image_size ];
1038 - $max_width = (int) $size_data['width'];
1039 - $max_height = (int) $size_data['height'];
1040 -
1041 - // Validate dimensions don't exceed the registered size maximums.
1042 - // Allow 1px tolerance for rounding differences.
1043 - $tolerance = 1;
1044 -
1045 - if ( $max_width > 0 && $width > $max_width + $tolerance ) {
1046 - return new WP_Error(
1047 - 'rest_upload_dimension_mismatch',
1048 - sprintf(
1049 - /* translators: 1: image size name, 2: max width, 3: actual width */
1050 - __( 'Uploaded image width (%3$d) exceeds maximum for "%1$s" size (%2$d).', 'gutenberg' ),
1051 - $image_size,
1052 - $max_width,
1053 - $width
1054 - ),
1055 - array( 'status' => 400 )
1056 - );
1057 - }
1058 -
1059 - if ( $max_height > 0 && $height > $max_height + $tolerance ) {
1060 - return new WP_Error(
1061 - 'rest_upload_dimension_mismatch',
1062 - sprintf(
1063 - /* translators: 1: image size name, 2: max height, 3: actual height */
1064 - __( 'Uploaded image height (%3$d) exceeds maximum for "%1$s" size (%2$d).', 'gutenberg' ),
1065 - $image_size,
1066 - $max_height,
1067 - $height
1068 - ),
1069 - array( 'status' => 400 )
1070 - );
1071 - }
1072 -
1073 - return true;
1074 - }
1075 -
1076 - /**
1077 295 * Side-loads a media file without creating an attachment.
1078 296 *
1079 297 * @param WP_REST_Request $request Full details about the request.
1080 298 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
@@ -1160,183 +378,60 @@
1160 378 $path = $file['file'];
1161 379
1162 380 $image_size = $request['image_size'];
1163 381
1164 - // Read dimensions once up-front. Needed both for early-error handling
1165 - // (corrupted/unsupported files) and for populating the sub-size payload
1166 - // below. 'original' and 'scaled' both replace the main file, so their
1167 - // dimensions are written to metadata; 'original' is additionally
1168 - // validated against the stored attachment size (it must match it or be
1169 - // its transpose).
1170 - //
1171 - // 'animated_video' companions are video files (MP4/WebM); the image
1172 - // helpers can't read their dimensions and would falsely report the
1173 - // upload as "corrupted or unsupported". Source-format originals
1174 - // ('source_original', e.g. the HEIC kept next to its JPEG derivative)
1175 - // are exempt for the same reason: their dimensions are neither
1176 - // validated nor recorded, and wp_getimagesize() may not be able to
1177 - // read the source format at all on servers without HEIC/HEIF support.
1178 - // Skip the read for both cases; validate_image_dimensions() also
1179 - // short-circuits them below.
1180 - $skip_dimension_read =
1181 - self::IMAGE_SIZE_ANIMATED_VIDEO === $image_size ||
1182 - self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size;
382 + $metadata = wp_get_attachment_metadata( $attachment_id, true );
1183 383
1184 - $size = $skip_dimension_read ? array( 0, 0 ) : wp_getimagesize( $path );
1185 -
1186 - if ( ! $size ) {
1187 - // Could not determine dimensions (corrupted file, unsupported format).
1188 - wp_delete_file( $path );
1189 - return new WP_Error(
1190 - 'rest_upload_invalid_image',
1191 - __( 'Could not read image dimensions. The file may be corrupted or an unsupported format.', 'gutenberg' ),
1192 - array( 'status' => 400 )
1193 - );
384 + if ( ! $metadata ) {
385 + $metadata = array();
1194 386 }
1195 387
1196 - $validation = $this->validate_image_dimensions( $size[0], $size[1], $image_size, $attachment_id );
1197 - if ( is_wp_error( $validation ) ) {
1198 - // Clean up the uploaded file.
1199 - wp_delete_file( $path );
1200 - return $validation;
1201 - }
388 + if ( 'original' === $image_size ) {
389 + $metadata['original_image'] = wp_basename( $path );
390 + } elseif ( 'scaled' === $image_size ) {
391 + // The current attached file is the original; record it as original_image.
392 + $current_file = get_attached_file( $attachment_id, true );
393 + $metadata['original_image'] = wp_basename( $current_file );
1202 394
1203 - // Build sub-size data to return to the client.
1204 - // The client accumulates these and sends them all to the finalize endpoint.
1205 - // `image_size` may be a single string or an array of names that share the
1206 - // same dimensions and therefore reuse a single sideloaded file. Arrays
1207 - // only carry regular sub-sizes; the special keys below ('original',
1208 - // 'scaled', and the source-format original) are always scalar strings.
1209 - $sub_size_data = array(
1210 - 'image_size' => $image_size,
1211 - );
395 + // Update the attached file to point to the scaled version.
396 + update_attached_file( $attachment_id, $path );
1212 397
1213 - if ( is_array( $image_size ) ) {
1214 - $sub_size_data['width'] = $size[0];
1215 - $sub_size_data['height'] = $size[1];
1216 - $sub_size_data['file'] = wp_basename( $path );
1217 - $sub_size_data['mime_type'] = $type;
1218 - $sub_size_data['filesize'] = wp_filesize( $path );
1219 - } elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) {
1220 - // Source-format original. finalize_item() writes the filename to
1221 - // $metadata[ self::META_KEY_SOURCE_IMAGE ] (separate from
1222 - // 'original_image', which the scaled-sideload flow owns). Cleanup on
1223 - // attachment delete is handled by a delete_attachment hook that reads
1224 - // this key.
1225 - $sub_size_data['file'] = wp_basename( $path );
1226 - } elseif ( self::IMAGE_SIZE_ANIMATED_VIDEO === $image_size ) {
1227 - // Converted animated-GIF video companion. finalize_item()
1228 - // writes the filename to $metadata['animated_video']; the editor
1229 - // reads it to switch the block to a video, and a delete_attachment
1230 - // hook removes it. See lib/media/animated-gif-to-video.php.
1231 - $sub_size_data['file'] = wp_basename( $path );
1232 - } elseif ( self::IMAGE_SIZE_ANIMATED_VIDEO_POSTER === $image_size ) {
1233 - // Static poster for the converted video. finalize_item() writes
1234 - // the filename to $metadata['animated_video_poster']; used as the
1235 - // video block's poster and deleted with the video.
1236 - $sub_size_data['file'] = wp_basename( $path );
1237 - } elseif ( 'scaled' === $image_size || 'original' === $image_size ) {
1238 - // 'scaled' and 'original' both replace the attachment's main file
1239 - // with the supplied image and keep the file being replaced as
1240 - // `original_image`, which is the untouched upload. A 'scaled' image is
1241 - // downsized and an 'original' image has any EXIF orientation already
1242 - // applied. This is the same swap WordPress makes when it scales or
1243 - // rotates an image on upload. See core's _wp_image_meta_replace_original().
1244 - $current_file = get_attached_file( $attachment_id, true );
398 + $size = wp_getimagesize( $path );
1245 399
1246 - if ( ! $current_file ) {
1247 - return new WP_Error(
1248 - 'rest_sideload_no_attached_file',
1249 - __( 'Unable to retrieve the attached file for this attachment.', 'gutenberg' ),
1250 - array( 'status' => 404 )
1251 - );
1252 - }
1253 -
1254 - $sub_size_data['original_image'] = wp_basename( $current_file );
1255 -
1256 - // Update the attached file to point to the supplied image.
1257 - // This writes to _wp_attached_file meta, not _wp_attachment_metadata.
1258 - // Guard against a failed update so a stale original is not recorded.
1259 - if (
1260 - get_attached_file( $attachment_id, true ) !== $path &&
1261 - ! update_attached_file( $attachment_id, $path )
1262 - ) {
1263 - return new WP_Error(
1264 - 'rest_sideload_update_attached_file_failed',
1265 - __( 'Unable to update the attached file for this attachment.', 'gutenberg' ),
1266 - array( 'status' => 500 )
1267 - );
1268 - }
1269 -
1270 - $sub_size_data['width'] = $size[0];
1271 - $sub_size_data['height'] = $size[1];
1272 - $sub_size_data['filesize'] = wp_filesize( $path );
1273 - $sub_size_data['file'] = _wp_relative_upload_path( $path );
400 + $metadata['width'] = $size ? $size[0] : 0;
401 + $metadata['height'] = $size ? $size[1] : 0;
402 + $metadata['filesize'] = wp_filesize( $path );
403 + $metadata['file'] = _wp_relative_upload_path( $path );
1274 404 } else {
1275 - $sub_size_data['width'] = $size[0];
1276 - $sub_size_data['height'] = $size[1];
1277 - $sub_size_data['file'] = wp_basename( $path );
1278 - $sub_size_data['mime_type'] = $type;
1279 - $sub_size_data['filesize'] = wp_filesize( $path );
1280 - }
405 + $metadata['sizes'] = $metadata['sizes'] ?? array();
1281 406
1282 - return rest_ensure_response( $sub_size_data );
1283 - }
407 + $size = wp_getimagesize( $path );
1284 408
1285 - /**
1286 - * Resolves the encode quality WordPress would use for an image.
1287 - *
1288 - * Prefers the core wp_get_image_encode_quality() helper when available, and
1289 - * otherwise mirrors WP_Image_Editor::set_quality() inline for WordPress
1290 - * versions that predate it: per-format default, the wp_editor_set_quality
1291 - * filter, the jpeg_quality filter for JPEG output, then resets non-numeric
1292 - * or out-of-range values to the default and squashes 0 to 1.
1293 - *
1294 - * wp_get_image_encode_quality() is proposed for WordPress core in
1295 - * https://github.com/WordPress/wordpress-develop/pull/11856; until it lands
1296 - * the function_exists() guard falls back to the inline implementation below.
1297 - *
1298 - * @param non-empty-string $mime_type The output image MIME type, e.g. 'image/jpeg'.
1299 - * @param array{ width?: non-negative-int, height?: non-negative-int } $size Dimensions ('width', 'height') for the wp_editor_set_quality filter.
1300 - * @return int<1, 100> Encode quality between 1 and 100.
1301 - */
1302 - private function get_image_encode_quality( string $mime_type, array $size = array() ): int {
1303 - if ( function_exists( 'wp_get_image_encode_quality' ) ) {
1304 - return wp_get_image_encode_quality( $mime_type, $size );
409 + $metadata['sizes'][ $image_size ] = array(
410 + 'width' => $size ? $size[0] : 0,
411 + 'height' => $size ? $size[1] : 0,
412 + 'file' => wp_basename( $path ),
413 + 'mime-type' => $type,
414 + 'filesize' => wp_filesize( $path ),
415 + );
1305 416 }
1306 417
1307 - // Mirror WP_Image_Editor::get_default_quality(): WebP defaults to 86,
1308 - // everything else to 82.
1309 - $default_quality = ( 'image/webp' === $mime_type ) ? 86 : 82;
418 + wp_update_attachment_metadata( $attachment_id, $metadata );
1310 419
1311 - /** This filter is documented in wp-includes/class-wp-image-editor.php */
1312 - $quality = apply_filters(
1313 - 'wp_editor_set_quality', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
1314 - $default_quality,
1315 - $mime_type,
1316 - $size
420 + $response_request = new WP_REST_Request(
421 + WP_REST_Server::READABLE,
422 + rest_get_route_for_post( $attachment_id )
1317 423 );
1318 424
1319 - if ( 'image/jpeg' === $mime_type ) {
1320 - /** This filter is documented in wp-includes/class-wp-image-editor.php */
1321 - $quality = apply_filters( 'jpeg_quality', $quality, 'image_resize' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
1322 - }
425 + $response_request['context'] = 'edit';
1323 426
1324 - if ( ! is_numeric( $quality ) ) {
1325 - $quality = $default_quality;
1326 - } else {
1327 - $quality = (int) $quality;
427 + if ( isset( $request['_fields'] ) ) {
428 + $response_request['_fields'] = $request['_fields'];
1328 429 }
1329 430
1330 - // Reset out-of-range values to the default, matching WP_Image_Editor::set_quality().
1331 - if ( $quality < 0 || $quality > 100 ) {
1332 - $quality = $default_quality;
1333 - }
431 + $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );
1334 432
1335 - // Allow 0, but squash to 1, matching WP_Image_Editor::set_quality().
1336 - if ( 0 === $quality ) {
1337 - $quality = 1;
1338 - }
433 + $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );
1339 434
1340 - return $quality;
435 + return $response;
1341 436 }
1342 437 }