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