PluginProbe
Gutenberg / 21.6.0
Gutenberg v21.6.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
gutenberg / lib / experimental / media / class-gutenberg-rest-attachments-controller.php

class-gutenberg-rest-attachments-controller.php in Gutenberg 21.6.0, at lib/experimental/media/class-gutenberg-rest-attachments-controller.php

368 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Gutenberg_REST_Attachments_Controller.
4 *
5 * @package MediaExperiments
6 */
7
8 /**
9 * Class Gutenberg_REST_Attachments_Controller.
10 */
11 class Gutenberg_REST_Attachments_Controller extends WP_REST_Attachments_Controller {
12 /**
13 * Registers the routes for attachments.
14 *
15 * @see register_rest_route()
16 */
17 public function register_routes(): void {
18 parent::register_routes();
19
20 $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );
21
22 // Special case to set 'original_image' in attachment metadata.
23 $valid_image_sizes[] = 'original';
24 // Used for PDF thumbnails.
25 $valid_image_sizes[] = 'full';
26
27 register_rest_route(
28 $this->namespace,
29 '/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',
30 array(
31 array(
32 'methods' => WP_REST_Server::CREATABLE,
33 'callback' => array( $this, 'sideload_item' ),
34 'permission_callback' => array( $this, 'sideload_item_permissions_check' ),
35 'args' => array(
36 'id' => array(
37 'description' => __( 'Unique identifier for the attachment.', 'gutenberg' ),
38 'type' => 'integer',
39 ),
40 'image_size' => array(
41 'description' => __( 'Image size.', 'gutenberg' ),
42 'type' => 'string',
43 'enum' => $valid_image_sizes,
44 'required' => true,
45 ),
46 ),
47 ),
48 'allow_batch' => $this->allow_batch,
49 'schema' => array( $this, 'get_public_item_schema' ),
50 )
51 );
52 }
53
54 /**
55 * Retrieves an array of endpoint arguments from the item schema for the controller.
56 *
57 * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
58 * checked for required values and may fall-back to a given default, this is not done
59 * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
60 * @return array Endpoint arguments.
61 */
62 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
63 $args = rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method );
64
65 if ( WP_REST_Server::CREATABLE === $method ) {
66 $args['generate_sub_sizes'] = array(
67 'type' => 'boolean',
68 'default' => true,
69 'description' => __( 'Whether to generate image sub sizes.', 'gutenberg' ),
70 );
71 $args['convert_format'] = array(
72 'type' => 'boolean',
73 'default' => true,
74 'description' => __( 'Whether to convert image formats.', 'gutenberg' ),
75 );
76 }
77
78 return $args;
79 }
80
81 /**
82 * Prepares a single attachment output for response.
83 *
84 * Ensures 'missing_image_sizes' is set for PDFs and not just images.
85 *
86 * @param WP_Post $item Attachment object.
87 * @param WP_REST_Request $request Request object.
88 * @return WP_REST_Response Response object.
89 */
90 public function prepare_item_for_response( $item, $request ): WP_REST_Response {
91 $response = parent::prepare_item_for_response( $item, $request );
92
93 $data = $response->get_data();
94
95 // Handle missing image sizes for PDFs.
96
97 $fields = $this->get_fields_for_response( $request );
98
99 if (
100 rest_is_field_included( 'missing_image_sizes', $fields ) &&
101 empty( $data['missing_image_sizes'] )
102 ) {
103 $mime_type = get_post_mime_type( $item );
104
105 if ( 'application/pdf' === $mime_type ) {
106 $metadata = wp_get_attachment_metadata( $item->ID, true );
107
108 if ( ! is_array( $metadata ) ) {
109 $metadata = array();
110 }
111
112 $metadata['sizes'] = $metadata['sizes'] ?? array();
113
114 $fallback_sizes = array(
115 'thumbnail',
116 'medium',
117 'large',
118 );
119
120 // The filter might have been added by ::create_item().
121 remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
122
123 /** This filter is documented in wp-admin/includes/image.php */
124 $fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
125
126 $registered_sizes = wp_get_registered_image_subsizes();
127 $merged_sizes = array_keys( array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) ) );
128
129 $missing_image_sizes = array_diff( $merged_sizes, array_keys( $metadata['sizes'] ) );
130 $data['missing_image_sizes'] = $missing_image_sizes;
131 }
132 }
133
134 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
135 $data = $this->add_additional_fields_to_object( $data, $request );
136 $data = $this->filter_response_by_context( $data, $context );
137
138 $links = $response->get_links();
139
140 $response = rest_ensure_response( $data );
141
142 foreach ( $links as $rel => $rel_links ) {
143 foreach ( $rel_links as $link ) {
144 $response->add_link( $rel, $link['href'], $link['attributes'] );
145 }
146 }
147
148 return $response;
149 }
150
151 /**
152 * Creates a single attachment.
153 *
154 * @param WP_REST_Request $request Full details about the request.
155 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
156 */
157 public function create_item( $request ) {
158 if ( ! $request['generate_sub_sizes'] ) {
159 add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
160 add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
161
162 }
163
164 if ( ! $request['convert_format'] ) {
165 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
166 }
167
168 $response = parent::create_item( $request );
169
170 remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
171 remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
172 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
173
174 return $response;
175 }
176
177
178 /**
179 * Checks if a given request has access to sideload a file.
180 *
181 * Sideloading a file for an existing attachment
182 * requires both update and create permissions.
183 *
184 * @param WP_REST_Request $request Full details about the request.
185 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
186 */
187 public function sideload_item_permissions_check( $request ) {
188 return $this->edit_media_item_permissions_check( $request );
189 }
190
191 /**
192 * Filters {@see 'wp_unique_filename'} during sideloads.
193 *
194 * {@see wp_unique_filename()} will always add numeric suffix if the name looks like a sub-size to avoid conflicts.
195 *
196 * Adding this closure to the filter helps work around this safeguard.
197 *
198 * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg,
199 * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg
200 * However, here it is desired not to add the suffix in order to maintain the same
201 * naming convention as if the file was uploaded regularly.
202 *
203 * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582
204 *
205 * @param string $filename Unique file name.
206 * @param string $ext File extension. Example: ".png".
207 * @param string $dir Directory path.
208 * @param callable|null $unique_filename_callback Callback function that generates the unique file name.
209 * @param string[] $alt_filenames Array of alternate file names that were checked for collisions.
210 * @param int|string $number The highest number that was used to make the file name unique
211 * or an empty string if unused.
212 * @return string Filtered file name.
213 */
214 private function filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ) {
215 if ( empty( $number ) || ! $attachment_filename ) {
216 return $filename;
217 }
218
219 $ext = pathinfo( $filename, PATHINFO_EXTENSION );
220 $name = pathinfo( $filename, PATHINFO_FILENAME );
221 $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME );
222
223 if ( ! $ext || ! $name ) {
224 return $filename;
225 }
226
227 $matches = array();
228 if ( preg_match( '/(.*)(-\d+x\d+)-' . $number . '$/', $name, $matches ) ) {
229 $filename_without_suffix = $matches[1] . $matches[2] . ".$ext";
230 if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) {
231 return $filename_without_suffix;
232 }
233 }
234
235 return $filename;
236 }
237
238 /**
239 * Side-loads a media file without creating an attachment.
240 *
241 * @param WP_REST_Request $request Full details about the request.
242 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
243 */
244 public function sideload_item( WP_REST_Request $request ) {
245 $attachment_id = $request['id'];
246
247 $post = $this->get_post( $attachment_id );
248
249 if ( is_wp_error( $post ) ) {
250 return $post;
251 }
252
253 if (
254 ! wp_attachment_is_image( $post ) &&
255 ! wp_attachment_is( 'pdf', $post )
256 ) {
257 return new WP_Error(
258 'rest_post_invalid_id',
259 __( 'Invalid post ID, only images and PDFs can be sideloaded.', 'gutenberg' ),
260 array( 'status' => 400 )
261 );
262 }
263
264 if ( ! $request['convert_format'] ) {
265 // Prevent image conversion as that is done client-side.
266 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
267 }
268
269 // Get the file via $_FILES or raw data.
270 $files = $request->get_file_params();
271 $headers = $request->get_headers();
272
273 /*
274 * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.
275 * See https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582
276 * With the following filter we can work around this safeguard.
277 */
278
279 $attachment_filename = get_attached_file( $attachment_id, true );
280 $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null;
281
282 /**
283 * @param string $filename Unique file name.
284 * @param string $ext File extension. Example: ".png".
285 * @param string $dir Directory path.
286 * @param callable|null $unique_filename_callback Callback function that generates the unique file name.
287 * @param string[] $alt_filenames Array of alternate file names that were checked for collisions.
288 * @param int|string $number The highest number that was used to make the file name unique
289 * or an empty string if unused.
290 * @return string Filtered file name.
291 */
292 $filter_filename = function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {
293 return $this->filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename );
294 };
295
296 add_filter( 'wp_unique_filename', $filter_filename, 10, 6 );
297
298 $parent_post = get_post_parent( $attachment_id );
299
300 $time = null;
301
302 // Matches logic in media_handle_upload().
303 // The post date doesn't usually matter for pages, so don't backdate this upload.
304 if ( $parent_post && 'page' !== $parent_post->post_type && substr( $parent_post->post_date, 0, 4 ) > 0 ) {
305 $time = $parent_post->post_date;
306 }
307
308 if ( ! empty( $files ) ) {
309 $file = $this->upload_from_file( $files, $headers, $time );
310 } else {
311 $file = $this->upload_from_data( $request->get_body(), $headers, $time );
312 }
313
314 remove_filter( 'wp_unique_filename', $filter_filename );
315 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
316
317 if ( is_wp_error( $file ) ) {
318 return $file;
319 }
320
321 $type = $file['type'];
322 $path = $file['file'];
323
324 $image_size = $request['image_size'];
325
326 $metadata = wp_get_attachment_metadata( $attachment_id, true );
327
328 if ( ! $metadata ) {
329 $metadata = array();
330 }
331
332 if ( 'original' === $image_size ) {
333 $metadata['original_image'] = wp_basename( $path );
334 } else {
335 $metadata['sizes'] = $metadata['sizes'] ?? array();
336
337 $size = wp_getimagesize( $path );
338
339 $metadata['sizes'][ $image_size ] = array(
340 'width' => $size ? $size[0] : 0,
341 'height' => $size ? $size[1] : 0,
342 'file' => wp_basename( $path ),
343 'mime-type' => $type,
344 'filesize' => wp_filesize( $path ),
345 );
346 }
347
348 wp_update_attachment_metadata( $attachment_id, $metadata );
349
350 $response_request = new WP_REST_Request(
351 WP_REST_Server::READABLE,
352 rest_get_route_for_post( $attachment_id )
353 );
354
355 $response_request['context'] = 'edit';
356
357 if ( isset( $request['_fields'] ) ) {
358 $response_request['_fields'] = $request['_fields'];
359 }
360
361 $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );
362
363 $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );
364
365 return $response;
366 }
367 }
368