|
1 <?php |
|
2 /** |
|
3 * REST API: WP_REST_Attachments_Controller class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage REST_API |
|
7 * @since 4.7.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Core controller used to access attachments via the REST API. |
|
12 * |
|
13 * @since 4.7.0 |
|
14 * |
|
15 * @see WP_REST_Posts_Controller |
|
16 */ |
|
17 class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { |
|
18 |
|
19 /** |
|
20 * Determines the allowed query_vars for a get_items() response and |
|
21 * prepares for WP_Query. |
|
22 * |
|
23 * @since 4.7.0 |
|
24 * |
|
25 * @param array $prepared_args Optional. Array of prepared arguments. Default empty array. |
|
26 * @param WP_REST_Request $request Optional. Request to prepare items for. |
|
27 * @return array Array of query arguments. |
|
28 */ |
|
29 protected function prepare_items_query( $prepared_args = array(), $request = null ) { |
|
30 $query_args = parent::prepare_items_query( $prepared_args, $request ); |
|
31 |
|
32 if ( empty( $query_args['post_status'] ) ) { |
|
33 $query_args['post_status'] = 'inherit'; |
|
34 } |
|
35 |
|
36 $media_types = $this->get_media_types(); |
|
37 |
|
38 if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) { |
|
39 $query_args['post_mime_type'] = $media_types[ $request['media_type'] ]; |
|
40 } |
|
41 |
|
42 if ( ! empty( $request['mime_type'] ) ) { |
|
43 $parts = explode( '/', $request['mime_type'] ); |
|
44 if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) { |
|
45 $query_args['post_mime_type'] = $request['mime_type']; |
|
46 } |
|
47 } |
|
48 |
|
49 // Filter query clauses to include filenames. |
|
50 if ( isset( $query_args['s'] ) ) { |
|
51 add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); |
|
52 } |
|
53 |
|
54 return $query_args; |
|
55 } |
|
56 |
|
57 /** |
|
58 * Checks if a given request has access to create an attachment. |
|
59 * |
|
60 * @since 4.7.0 |
|
61 * |
|
62 * @param WP_REST_Request $request Full details about the request. |
|
63 * @return WP_Error|true Boolean true if the attachment may be created, or a WP_Error if not. |
|
64 */ |
|
65 public function create_item_permissions_check( $request ) { |
|
66 $ret = parent::create_item_permissions_check( $request ); |
|
67 |
|
68 if ( ! $ret || is_wp_error( $ret ) ) { |
|
69 return $ret; |
|
70 } |
|
71 |
|
72 if ( ! current_user_can( 'upload_files' ) ) { |
|
73 return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to upload media on this site.' ), array( 'status' => 400 ) ); |
|
74 } |
|
75 |
|
76 // Attaching media to a post requires ability to edit said post. |
|
77 if ( ! empty( $request['post'] ) ) { |
|
78 $parent = get_post( (int) $request['post'] ); |
|
79 $post_parent_type = get_post_type_object( $parent->post_type ); |
|
80 |
|
81 if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) { |
|
82 return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to upload media to this post.' ), array( 'status' => rest_authorization_required_code() ) ); |
|
83 } |
|
84 } |
|
85 |
|
86 return true; |
|
87 } |
|
88 |
|
89 /** |
|
90 * Creates a single attachment. |
|
91 * |
|
92 * @since 4.7.0 |
|
93 * |
|
94 * @param WP_REST_Request $request Full details about the request. |
|
95 * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure. |
|
96 */ |
|
97 public function create_item( $request ) { |
|
98 |
|
99 if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) { |
|
100 return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) ); |
|
101 } |
|
102 |
|
103 // Get the file via $_FILES or raw data. |
|
104 $files = $request->get_file_params(); |
|
105 $headers = $request->get_headers(); |
|
106 |
|
107 if ( ! empty( $files ) ) { |
|
108 $file = $this->upload_from_file( $files, $headers ); |
|
109 } else { |
|
110 $file = $this->upload_from_data( $request->get_body(), $headers ); |
|
111 } |
|
112 |
|
113 if ( is_wp_error( $file ) ) { |
|
114 return $file; |
|
115 } |
|
116 |
|
117 $name = basename( $file['file'] ); |
|
118 $name_parts = pathinfo( $name ); |
|
119 $name = trim( substr( $name, 0, -(1 + strlen( $name_parts['extension'] ) ) ) ); |
|
120 |
|
121 $url = $file['url']; |
|
122 $type = $file['type']; |
|
123 $file = $file['file']; |
|
124 |
|
125 // use image exif/iptc data for title and caption defaults if possible |
|
126 $image_meta = wp_read_image_metadata( $file ); |
|
127 |
|
128 if ( ! empty( $image_meta ) ) { |
|
129 if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { |
|
130 $request['title'] = $image_meta['title']; |
|
131 } |
|
132 |
|
133 if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) { |
|
134 $request['caption'] = $image_meta['caption']; |
|
135 } |
|
136 } |
|
137 |
|
138 $attachment = $this->prepare_item_for_database( $request ); |
|
139 $attachment->post_mime_type = $type; |
|
140 $attachment->guid = $url; |
|
141 |
|
142 if ( empty( $attachment->post_title ) ) { |
|
143 $attachment->post_title = preg_replace( '/\.[^.]+$/', '', basename( $file ) ); |
|
144 } |
|
145 |
|
146 // $post_parent is inherited from $attachment['post_parent']. |
|
147 $id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true ); |
|
148 |
|
149 if ( is_wp_error( $id ) ) { |
|
150 if ( 'db_update_error' === $id->get_error_code() ) { |
|
151 $id->add_data( array( 'status' => 500 ) ); |
|
152 } else { |
|
153 $id->add_data( array( 'status' => 400 ) ); |
|
154 } |
|
155 return $id; |
|
156 } |
|
157 |
|
158 $attachment = get_post( $id ); |
|
159 |
|
160 /** |
|
161 * Fires after a single attachment is created or updated via the REST API. |
|
162 * |
|
163 * @since 4.7.0 |
|
164 * |
|
165 * @param WP_Post $attachment Inserted or updated attachment |
|
166 * object. |
|
167 * @param WP_REST_Request $request The request sent to the API. |
|
168 * @param bool $creating True when creating an attachment, false when updating. |
|
169 */ |
|
170 do_action( 'rest_insert_attachment', $attachment, $request, true ); |
|
171 |
|
172 // Include admin functions to get access to wp_generate_attachment_metadata(). |
|
173 require_once ABSPATH . 'wp-admin/includes/admin.php'; |
|
174 |
|
175 wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) ); |
|
176 |
|
177 if ( isset( $request['alt_text'] ) ) { |
|
178 update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) ); |
|
179 } |
|
180 |
|
181 $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); |
|
182 |
|
183 if ( is_wp_error( $fields_update ) ) { |
|
184 return $fields_update; |
|
185 } |
|
186 |
|
187 $request->set_param( 'context', 'edit' ); |
|
188 $response = $this->prepare_item_for_response( $attachment, $request ); |
|
189 $response = rest_ensure_response( $response ); |
|
190 $response->set_status( 201 ); |
|
191 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) ); |
|
192 |
|
193 return $response; |
|
194 } |
|
195 |
|
196 /** |
|
197 * Updates a single attachment. |
|
198 * |
|
199 * @since 4.7.0 |
|
200 * |
|
201 * @param WP_REST_Request $request Full details about the request. |
|
202 * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure. |
|
203 */ |
|
204 public function update_item( $request ) { |
|
205 if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) { |
|
206 return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) ); |
|
207 } |
|
208 |
|
209 $response = parent::update_item( $request ); |
|
210 |
|
211 if ( is_wp_error( $response ) ) { |
|
212 return $response; |
|
213 } |
|
214 |
|
215 $response = rest_ensure_response( $response ); |
|
216 $data = $response->get_data(); |
|
217 |
|
218 if ( isset( $request['alt_text'] ) ) { |
|
219 update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] ); |
|
220 } |
|
221 |
|
222 $attachment = get_post( $request['id'] ); |
|
223 |
|
224 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */ |
|
225 do_action( 'rest_insert_attachment', $data, $request, false ); |
|
226 |
|
227 $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); |
|
228 |
|
229 if ( is_wp_error( $fields_update ) ) { |
|
230 return $fields_update; |
|
231 } |
|
232 |
|
233 $request->set_param( 'context', 'edit' ); |
|
234 $response = $this->prepare_item_for_response( $attachment, $request ); |
|
235 $response = rest_ensure_response( $response ); |
|
236 |
|
237 return $response; |
|
238 } |
|
239 |
|
240 /** |
|
241 * Prepares a single attachment for create or update. |
|
242 * |
|
243 * @since 4.7.0 |
|
244 * |
|
245 * @param WP_REST_Request $request Request object. |
|
246 * @return WP_Error|stdClass $prepared_attachment Post object. |
|
247 */ |
|
248 protected function prepare_item_for_database( $request ) { |
|
249 $prepared_attachment = parent::prepare_item_for_database( $request ); |
|
250 |
|
251 // Attachment caption (post_excerpt internally) |
|
252 if ( isset( $request['caption'] ) ) { |
|
253 if ( is_string( $request['caption'] ) ) { |
|
254 $prepared_attachment->post_excerpt = $request['caption']; |
|
255 } elseif ( isset( $request['caption']['raw'] ) ) { |
|
256 $prepared_attachment->post_excerpt = $request['caption']['raw']; |
|
257 } |
|
258 } |
|
259 |
|
260 // Attachment description (post_content internally) |
|
261 if ( isset( $request['description'] ) ) { |
|
262 if ( is_string( $request['description'] ) ) { |
|
263 $prepared_attachment->post_content = $request['description']; |
|
264 } elseif ( isset( $request['description']['raw'] ) ) { |
|
265 $prepared_attachment->post_content = $request['description']['raw']; |
|
266 } |
|
267 } |
|
268 |
|
269 if ( isset( $request['post'] ) ) { |
|
270 $prepared_attachment->post_parent = (int) $request['post']; |
|
271 } |
|
272 |
|
273 return $prepared_attachment; |
|
274 } |
|
275 |
|
276 /** |
|
277 * Prepares a single attachment output for response. |
|
278 * |
|
279 * @since 4.7.0 |
|
280 * |
|
281 * @param WP_Post $post Attachment object. |
|
282 * @param WP_REST_Request $request Request object. |
|
283 * @return WP_REST_Response Response object. |
|
284 */ |
|
285 public function prepare_item_for_response( $post, $request ) { |
|
286 $response = parent::prepare_item_for_response( $post, $request ); |
|
287 $fields = $this->get_fields_for_response( $request ); |
|
288 $data = $response->get_data(); |
|
289 |
|
290 if ( in_array( 'description', $fields, true ) ) { |
|
291 $data['description'] = array( |
|
292 'raw' => $post->post_content, |
|
293 /** This filter is documented in wp-includes/post-template.php */ |
|
294 'rendered' => apply_filters( 'the_content', $post->post_content ), |
|
295 ); |
|
296 } |
|
297 |
|
298 if ( in_array( 'caption', $fields, true ) ) { |
|
299 /** This filter is documented in wp-includes/post-template.php */ |
|
300 $caption = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) ); |
|
301 $data['caption'] = array( |
|
302 'raw' => $post->post_excerpt, |
|
303 'rendered' => $caption, |
|
304 ); |
|
305 } |
|
306 |
|
307 if ( in_array( 'alt_text', $fields, true ) ) { |
|
308 $data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ); |
|
309 } |
|
310 |
|
311 if ( in_array( 'media_type', $fields, true ) ) { |
|
312 $data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file'; |
|
313 } |
|
314 |
|
315 if ( in_array( 'mime_type', $fields, true ) ) { |
|
316 $data['mime_type'] = $post->post_mime_type; |
|
317 } |
|
318 |
|
319 if ( in_array( 'media_details', $fields, true ) ) { |
|
320 $data['media_details'] = wp_get_attachment_metadata( $post->ID ); |
|
321 |
|
322 // Ensure empty details is an empty object. |
|
323 if ( empty( $data['media_details'] ) ) { |
|
324 $data['media_details'] = new stdClass; |
|
325 } elseif ( ! empty( $data['media_details']['sizes'] ) ) { |
|
326 |
|
327 foreach ( $data['media_details']['sizes'] as $size => &$size_data ) { |
|
328 |
|
329 if ( isset( $size_data['mime-type'] ) ) { |
|
330 $size_data['mime_type'] = $size_data['mime-type']; |
|
331 unset( $size_data['mime-type'] ); |
|
332 } |
|
333 |
|
334 // Use the same method image_downsize() does. |
|
335 $image_src = wp_get_attachment_image_src( $post->ID, $size ); |
|
336 if ( ! $image_src ) { |
|
337 continue; |
|
338 } |
|
339 |
|
340 $size_data['source_url'] = $image_src[0]; |
|
341 } |
|
342 |
|
343 $full_src = wp_get_attachment_image_src( $post->ID, 'full' ); |
|
344 |
|
345 if ( ! empty( $full_src ) ) { |
|
346 $data['media_details']['sizes']['full'] = array( |
|
347 'file' => wp_basename( $full_src[0] ), |
|
348 'width' => $full_src[1], |
|
349 'height' => $full_src[2], |
|
350 'mime_type' => $post->post_mime_type, |
|
351 'source_url' => $full_src[0], |
|
352 ); |
|
353 } |
|
354 } else { |
|
355 $data['media_details']['sizes'] = new stdClass; |
|
356 } |
|
357 } |
|
358 |
|
359 if ( in_array( 'post', $fields, true ) ) { |
|
360 $data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null; |
|
361 } |
|
362 |
|
363 if ( in_array( 'source_url', $fields, true ) ) { |
|
364 $data['source_url'] = wp_get_attachment_url( $post->ID ); |
|
365 } |
|
366 |
|
367 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; |
|
368 |
|
369 $data = $this->filter_response_by_context( $data, $context ); |
|
370 |
|
371 $links = $response->get_links(); |
|
372 |
|
373 // Wrap the data in a response object. |
|
374 $response = rest_ensure_response( $data ); |
|
375 $response->add_links( $links ); |
|
376 |
|
377 /** |
|
378 * Filters an attachment returned from the REST API. |
|
379 * |
|
380 * Allows modification of the attachment right before it is returned. |
|
381 * |
|
382 * @since 4.7.0 |
|
383 * |
|
384 * @param WP_REST_Response $response The response object. |
|
385 * @param WP_Post $post The original attachment post. |
|
386 * @param WP_REST_Request $request Request used to generate the response. |
|
387 */ |
|
388 return apply_filters( 'rest_prepare_attachment', $response, $post, $request ); |
|
389 } |
|
390 |
|
391 /** |
|
392 * Retrieves the attachment's schema, conforming to JSON Schema. |
|
393 * |
|
394 * @since 4.7.0 |
|
395 * |
|
396 * @return array Item schema as an array. |
|
397 */ |
|
398 public function get_item_schema() { |
|
399 |
|
400 $schema = parent::get_item_schema(); |
|
401 |
|
402 $schema['properties']['alt_text'] = array( |
|
403 'description' => __( 'Alternative text to display when attachment is not displayed.' ), |
|
404 'type' => 'string', |
|
405 'context' => array( 'view', 'edit', 'embed' ), |
|
406 'arg_options' => array( |
|
407 'sanitize_callback' => 'sanitize_text_field', |
|
408 ), |
|
409 ); |
|
410 |
|
411 $schema['properties']['caption'] = array( |
|
412 'description' => __( 'The attachment caption.' ), |
|
413 'type' => 'object', |
|
414 'context' => array( 'view', 'edit', 'embed' ), |
|
415 'arg_options' => array( |
|
416 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() |
|
417 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() |
|
418 ), |
|
419 'properties' => array( |
|
420 'raw' => array( |
|
421 'description' => __( 'Caption for the attachment, as it exists in the database.' ), |
|
422 'type' => 'string', |
|
423 'context' => array( 'edit' ), |
|
424 ), |
|
425 'rendered' => array( |
|
426 'description' => __( 'HTML caption for the attachment, transformed for display.' ), |
|
427 'type' => 'string', |
|
428 'context' => array( 'view', 'edit', 'embed' ), |
|
429 'readonly' => true, |
|
430 ), |
|
431 ), |
|
432 ); |
|
433 |
|
434 $schema['properties']['description'] = array( |
|
435 'description' => __( 'The attachment description.' ), |
|
436 'type' => 'object', |
|
437 'context' => array( 'view', 'edit' ), |
|
438 'arg_options' => array( |
|
439 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() |
|
440 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() |
|
441 ), |
|
442 'properties' => array( |
|
443 'raw' => array( |
|
444 'description' => __( 'Description for the object, as it exists in the database.' ), |
|
445 'type' => 'string', |
|
446 'context' => array( 'edit' ), |
|
447 ), |
|
448 'rendered' => array( |
|
449 'description' => __( 'HTML description for the object, transformed for display.' ), |
|
450 'type' => 'string', |
|
451 'context' => array( 'view', 'edit' ), |
|
452 'readonly' => true, |
|
453 ), |
|
454 ), |
|
455 ); |
|
456 |
|
457 $schema['properties']['media_type'] = array( |
|
458 'description' => __( 'Attachment type.' ), |
|
459 'type' => 'string', |
|
460 'enum' => array( 'image', 'file' ), |
|
461 'context' => array( 'view', 'edit', 'embed' ), |
|
462 'readonly' => true, |
|
463 ); |
|
464 |
|
465 $schema['properties']['mime_type'] = array( |
|
466 'description' => __( 'The attachment MIME type.' ), |
|
467 'type' => 'string', |
|
468 'context' => array( 'view', 'edit', 'embed' ), |
|
469 'readonly' => true, |
|
470 ); |
|
471 |
|
472 $schema['properties']['media_details'] = array( |
|
473 'description' => __( 'Details about the media file, specific to its type.' ), |
|
474 'type' => 'object', |
|
475 'context' => array( 'view', 'edit', 'embed' ), |
|
476 'readonly' => true, |
|
477 ); |
|
478 |
|
479 $schema['properties']['post'] = array( |
|
480 'description' => __( 'The ID for the associated post of the attachment.' ), |
|
481 'type' => 'integer', |
|
482 'context' => array( 'view', 'edit' ), |
|
483 ); |
|
484 |
|
485 $schema['properties']['source_url'] = array( |
|
486 'description' => __( 'URL to the original attachment file.' ), |
|
487 'type' => 'string', |
|
488 'format' => 'uri', |
|
489 'context' => array( 'view', 'edit', 'embed' ), |
|
490 'readonly' => true, |
|
491 ); |
|
492 |
|
493 unset( $schema['properties']['password'] ); |
|
494 |
|
495 return $schema; |
|
496 } |
|
497 |
|
498 /** |
|
499 * Handles an upload via raw POST data. |
|
500 * |
|
501 * @since 4.7.0 |
|
502 * |
|
503 * @param array $data Supplied file data. |
|
504 * @param array $headers HTTP headers from the request. |
|
505 * @return array|WP_Error Data from wp_handle_sideload(). |
|
506 */ |
|
507 protected function upload_from_data( $data, $headers ) { |
|
508 if ( empty( $data ) ) { |
|
509 return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) ); |
|
510 } |
|
511 |
|
512 if ( empty( $headers['content_type'] ) ) { |
|
513 return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) ); |
|
514 } |
|
515 |
|
516 if ( empty( $headers['content_disposition'] ) ) { |
|
517 return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) ); |
|
518 } |
|
519 |
|
520 $filename = self::get_filename_from_disposition( $headers['content_disposition'] ); |
|
521 |
|
522 if ( empty( $filename ) ) { |
|
523 return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) ); |
|
524 } |
|
525 |
|
526 if ( ! empty( $headers['content_md5'] ) ) { |
|
527 $content_md5 = array_shift( $headers['content_md5'] ); |
|
528 $expected = trim( $content_md5 ); |
|
529 $actual = md5( $data ); |
|
530 |
|
531 if ( $expected !== $actual ) { |
|
532 return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) ); |
|
533 } |
|
534 } |
|
535 |
|
536 // Get the content-type. |
|
537 $type = array_shift( $headers['content_type'] ); |
|
538 |
|
539 /** Include admin functions to get access to wp_tempnam() and wp_handle_sideload() */ |
|
540 require_once ABSPATH . 'wp-admin/includes/admin.php'; |
|
541 |
|
542 // Save the file. |
|
543 $tmpfname = wp_tempnam( $filename ); |
|
544 |
|
545 $fp = fopen( $tmpfname, 'w+' ); |
|
546 |
|
547 if ( ! $fp ) { |
|
548 return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) ); |
|
549 } |
|
550 |
|
551 fwrite( $fp, $data ); |
|
552 fclose( $fp ); |
|
553 |
|
554 // Now, sideload it in. |
|
555 $file_data = array( |
|
556 'error' => null, |
|
557 'tmp_name' => $tmpfname, |
|
558 'name' => $filename, |
|
559 'type' => $type, |
|
560 ); |
|
561 |
|
562 $size_check = self::check_upload_size( $file_data ); |
|
563 if ( is_wp_error( $size_check ) ) { |
|
564 return $size_check; |
|
565 } |
|
566 |
|
567 $overrides = array( |
|
568 'test_form' => false, |
|
569 ); |
|
570 |
|
571 $sideloaded = wp_handle_sideload( $file_data, $overrides ); |
|
572 |
|
573 if ( isset( $sideloaded['error'] ) ) { |
|
574 @unlink( $tmpfname ); |
|
575 |
|
576 return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) ); |
|
577 } |
|
578 |
|
579 return $sideloaded; |
|
580 } |
|
581 |
|
582 /** |
|
583 * Parses filename from a Content-Disposition header value. |
|
584 * |
|
585 * As per RFC6266: |
|
586 * |
|
587 * content-disposition = "Content-Disposition" ":" |
|
588 * disposition-type *( ";" disposition-parm ) |
|
589 * |
|
590 * disposition-type = "inline" | "attachment" | disp-ext-type |
|
591 * ; case-insensitive |
|
592 * disp-ext-type = token |
|
593 * |
|
594 * disposition-parm = filename-parm | disp-ext-parm |
|
595 * |
|
596 * filename-parm = "filename" "=" value |
|
597 * | "filename*" "=" ext-value |
|
598 * |
|
599 * disp-ext-parm = token "=" value |
|
600 * | ext-token "=" ext-value |
|
601 * ext-token = <the characters in token, followed by "*"> |
|
602 * |
|
603 * @since 4.7.0 |
|
604 * |
|
605 * @link http://tools.ietf.org/html/rfc2388 |
|
606 * @link http://tools.ietf.org/html/rfc6266 |
|
607 * |
|
608 * @param string[] $disposition_header List of Content-Disposition header values. |
|
609 * @return string|null Filename if available, or null if not found. |
|
610 */ |
|
611 public static function get_filename_from_disposition( $disposition_header ) { |
|
612 // Get the filename. |
|
613 $filename = null; |
|
614 |
|
615 foreach ( $disposition_header as $value ) { |
|
616 $value = trim( $value ); |
|
617 |
|
618 if ( strpos( $value, ';' ) === false ) { |
|
619 continue; |
|
620 } |
|
621 |
|
622 list( $type, $attr_parts ) = explode( ';', $value, 2 ); |
|
623 |
|
624 $attr_parts = explode( ';', $attr_parts ); |
|
625 $attributes = array(); |
|
626 |
|
627 foreach ( $attr_parts as $part ) { |
|
628 if ( strpos( $part, '=' ) === false ) { |
|
629 continue; |
|
630 } |
|
631 |
|
632 list( $key, $value ) = explode( '=', $part, 2 ); |
|
633 |
|
634 $attributes[ trim( $key ) ] = trim( $value ); |
|
635 } |
|
636 |
|
637 if ( empty( $attributes['filename'] ) ) { |
|
638 continue; |
|
639 } |
|
640 |
|
641 $filename = trim( $attributes['filename'] ); |
|
642 |
|
643 // Unquote quoted filename, but after trimming. |
|
644 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) { |
|
645 $filename = substr( $filename, 1, -1 ); |
|
646 } |
|
647 } |
|
648 |
|
649 return $filename; |
|
650 } |
|
651 |
|
652 /** |
|
653 * Retrieves the query params for collections of attachments. |
|
654 * |
|
655 * @since 4.7.0 |
|
656 * |
|
657 * @return array Query parameters for the attachment collection as an array. |
|
658 */ |
|
659 public function get_collection_params() { |
|
660 $params = parent::get_collection_params(); |
|
661 $params['status']['default'] = 'inherit'; |
|
662 $params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' ); |
|
663 $media_types = $this->get_media_types(); |
|
664 |
|
665 $params['media_type'] = array( |
|
666 'default' => null, |
|
667 'description' => __( 'Limit result set to attachments of a particular media type.' ), |
|
668 'type' => 'string', |
|
669 'enum' => array_keys( $media_types ), |
|
670 ); |
|
671 |
|
672 $params['mime_type'] = array( |
|
673 'default' => null, |
|
674 'description' => __( 'Limit result set to attachments of a particular MIME type.' ), |
|
675 'type' => 'string', |
|
676 ); |
|
677 |
|
678 return $params; |
|
679 } |
|
680 |
|
681 /** |
|
682 * Validates whether the user can query private statuses. |
|
683 * |
|
684 * @since 4.7.0 |
|
685 * |
|
686 * @param mixed $value Status value. |
|
687 * @param WP_REST_Request $request Request object. |
|
688 * @param string $parameter Additional parameter to pass for validation. |
|
689 * @return WP_Error|bool True if the user may query, WP_Error if not. |
|
690 */ |
|
691 public function validate_user_can_query_private_statuses( $value, $request, $parameter ) { |
|
692 if ( 'inherit' === $value ) { |
|
693 return true; |
|
694 } |
|
695 |
|
696 return parent::validate_user_can_query_private_statuses( $value, $request, $parameter ); |
|
697 } |
|
698 |
|
699 /** |
|
700 * Handles an upload via multipart/form-data ($_FILES). |
|
701 * |
|
702 * @since 4.7.0 |
|
703 * |
|
704 * @param array $files Data from the `$_FILES` superglobal. |
|
705 * @param array $headers HTTP headers from the request. |
|
706 * @return array|WP_Error Data from wp_handle_upload(). |
|
707 */ |
|
708 protected function upload_from_file( $files, $headers ) { |
|
709 if ( empty( $files ) ) { |
|
710 return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) ); |
|
711 } |
|
712 |
|
713 // Verify hash, if given. |
|
714 if ( ! empty( $headers['content_md5'] ) ) { |
|
715 $content_md5 = array_shift( $headers['content_md5'] ); |
|
716 $expected = trim( $content_md5 ); |
|
717 $actual = md5_file( $files['file']['tmp_name'] ); |
|
718 |
|
719 if ( $expected !== $actual ) { |
|
720 return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) ); |
|
721 } |
|
722 } |
|
723 |
|
724 // Pass off to WP to handle the actual upload. |
|
725 $overrides = array( |
|
726 'test_form' => false, |
|
727 ); |
|
728 |
|
729 // Bypasses is_uploaded_file() when running unit tests. |
|
730 if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { |
|
731 $overrides['action'] = 'wp_handle_mock_upload'; |
|
732 } |
|
733 |
|
734 $size_check = self::check_upload_size( $files['file'] ); |
|
735 if ( is_wp_error( $size_check ) ) { |
|
736 return $size_check; |
|
737 } |
|
738 |
|
739 /** Include admin functions to get access to wp_handle_upload() */ |
|
740 require_once ABSPATH . 'wp-admin/includes/admin.php'; |
|
741 |
|
742 $file = wp_handle_upload( $files['file'], $overrides ); |
|
743 |
|
744 if ( isset( $file['error'] ) ) { |
|
745 return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) ); |
|
746 } |
|
747 |
|
748 return $file; |
|
749 } |
|
750 |
|
751 /** |
|
752 * Retrieves the supported media types. |
|
753 * |
|
754 * Media types are considered the MIME type category. |
|
755 * |
|
756 * @since 4.7.0 |
|
757 * |
|
758 * @return array Array of supported media types. |
|
759 */ |
|
760 protected function get_media_types() { |
|
761 $media_types = array(); |
|
762 |
|
763 foreach ( get_allowed_mime_types() as $mime_type ) { |
|
764 $parts = explode( '/', $mime_type ); |
|
765 |
|
766 if ( ! isset( $media_types[ $parts[0] ] ) ) { |
|
767 $media_types[ $parts[0] ] = array(); |
|
768 } |
|
769 |
|
770 $media_types[ $parts[0] ][] = $mime_type; |
|
771 } |
|
772 |
|
773 return $media_types; |
|
774 } |
|
775 |
|
776 /** |
|
777 * Determine if uploaded file exceeds space quota on multisite. |
|
778 * |
|
779 * Replicates check_upload_size(). |
|
780 * |
|
781 * @since 4.9.8 |
|
782 * |
|
783 * @param array $file $_FILES array for a given file. |
|
784 * @return true|WP_Error True if can upload, error for errors. |
|
785 */ |
|
786 protected function check_upload_size( $file ) { |
|
787 if ( ! is_multisite() ) { |
|
788 return true; |
|
789 } |
|
790 |
|
791 if ( get_site_option( 'upload_space_check_disabled' ) ) { |
|
792 return true; |
|
793 } |
|
794 |
|
795 $space_left = get_upload_space_available(); |
|
796 |
|
797 $file_size = filesize( $file['tmp_name'] ); |
|
798 if ( $space_left < $file_size ) { |
|
799 /* translators: %s: required disk space in kilobytes */ |
|
800 return new WP_Error( 'rest_upload_limited_space', sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), array( 'status' => 400 ) ); |
|
801 } |
|
802 |
|
803 if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) { |
|
804 /* translators: %s: maximum allowed file size in kilobytes */ |
|
805 return new WP_Error( 'rest_upload_file_too_big', sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), array( 'status' => 400 ) ); |
|
806 } |
|
807 |
|
808 if ( upload_is_user_over_quota( false ) ) { |
|
809 return new WP_Error( 'rest_upload_user_quota_exceeded', __( 'You have used your space quota. Please delete files before uploading.' ), array( 'status' => 400 ) ); |
|
810 } |
|
811 return true; |
|
812 } |
|
813 |
|
814 } |