[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-attachments-controller.php (source)

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


Generated: Tue May 26 01:00:04 2020 Cross-referenced by PHPXref 0.7.1