angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-rest-attachments-controller.php (43823B)


      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 attachment.' ),
     38 						'type'        => 'integer',
     39 					),
     40 					'action' => array(
     41 						'type'     => 'string',
     42 						'enum'     => array( 'create-image-subsizes' ),
     43 						'required' => true,
     44 					),
     45 				),
     46 			)
     47 		);
     48 		register_rest_route(
     49 			$this->namespace,
     50 			'/' . $this->rest_base . '/(?P<id>[\d]+)/edit',
     51 			array(
     52 				'methods'             => WP_REST_Server::CREATABLE,
     53 				'callback'            => array( $this, 'edit_media_item' ),
     54 				'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
     55 				'args'                => $this->get_edit_media_item_args(),
     56 			)
     57 		);
     58 	}
     59 
     60 	/**
     61 	 * Determines the allowed query_vars for a get_items() response and
     62 	 * prepares for WP_Query.
     63 	 *
     64 	 * @since 4.7.0
     65 	 *
     66 	 * @param array           $prepared_args Optional. Array of prepared arguments. Default empty array.
     67 	 * @param WP_REST_Request $request       Optional. Request to prepare items for.
     68 	 * @return array Array of query arguments.
     69 	 */
     70 	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
     71 		$query_args = parent::prepare_items_query( $prepared_args, $request );
     72 
     73 		if ( empty( $query_args['post_status'] ) ) {
     74 			$query_args['post_status'] = 'inherit';
     75 		}
     76 
     77 		$media_types = $this->get_media_types();
     78 
     79 		if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
     80 			$query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
     81 		}
     82 
     83 		if ( ! empty( $request['mime_type'] ) ) {
     84 			$parts = explode( '/', $request['mime_type'] );
     85 			if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
     86 				$query_args['post_mime_type'] = $request['mime_type'];
     87 			}
     88 		}
     89 
     90 		// Filter query clauses to include filenames.
     91 		if ( isset( $query_args['s'] ) ) {
     92 			add_filter( 'posts_clauses', '_filter_query_attachment_filenames' );
     93 		}
     94 
     95 		return $query_args;
     96 	}
     97 
     98 	/**
     99 	 * Checks if a given request has access to create an attachment.
    100 	 *
    101 	 * @since 4.7.0
    102 	 *
    103 	 * @param WP_REST_Request $request Full details about the request.
    104 	 * @return true|WP_Error Boolean true if the attachment may be created, or a WP_Error if not.
    105 	 */
    106 	public function create_item_permissions_check( $request ) {
    107 		$ret = parent::create_item_permissions_check( $request );
    108 
    109 		if ( ! $ret || is_wp_error( $ret ) ) {
    110 			return $ret;
    111 		}
    112 
    113 		if ( ! current_user_can( 'upload_files' ) ) {
    114 			return new WP_Error(
    115 				'rest_cannot_create',
    116 				__( 'Sorry, you are not allowed to upload media on this site.' ),
    117 				array( 'status' => 400 )
    118 			);
    119 		}
    120 
    121 		// Attaching media to a post requires ability to edit said post.
    122 		if ( ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) {
    123 			return new WP_Error(
    124 				'rest_cannot_edit',
    125 				__( 'Sorry, you are not allowed to upload media to this post.' ),
    126 				array( 'status' => rest_authorization_required_code() )
    127 			);
    128 		}
    129 
    130 		return true;
    131 	}
    132 
    133 	/**
    134 	 * Creates a single attachment.
    135 	 *
    136 	 * @since 4.7.0
    137 	 *
    138 	 * @param WP_REST_Request $request Full details about the request.
    139 	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
    140 	 */
    141 	public function create_item( $request ) {
    142 		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
    143 			return new WP_Error(
    144 				'rest_invalid_param',
    145 				__( 'Invalid parent type.' ),
    146 				array( 'status' => 400 )
    147 			);
    148 		}
    149 
    150 		$insert = $this->insert_attachment( $request );
    151 
    152 		if ( is_wp_error( $insert ) ) {
    153 			return $insert;
    154 		}
    155 
    156 		$schema = $this->get_item_schema();
    157 
    158 		// Extract by name.
    159 		$attachment_id = $insert['attachment_id'];
    160 		$file          = $insert['file'];
    161 
    162 		if ( isset( $request['alt_text'] ) ) {
    163 			update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
    164 		}
    165 
    166 		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
    167 			$meta_update = $this->meta->update_value( $request['meta'], $attachment_id );
    168 
    169 			if ( is_wp_error( $meta_update ) ) {
    170 				return $meta_update;
    171 			}
    172 		}
    173 
    174 		$attachment    = get_post( $attachment_id );
    175 		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
    176 
    177 		if ( is_wp_error( $fields_update ) ) {
    178 			return $fields_update;
    179 		}
    180 
    181 		$request->set_param( 'context', 'edit' );
    182 
    183 		/**
    184 		 * Fires after a single attachment is completely created or updated via the REST API.
    185 		 *
    186 		 * @since 5.0.0
    187 		 *
    188 		 * @param WP_Post         $attachment Inserted or updated attachment object.
    189 		 * @param WP_REST_Request $request    Request object.
    190 		 * @param bool            $creating   True when creating an attachment, false when updating.
    191 		 */
    192 		do_action( 'rest_after_insert_attachment', $attachment, $request, true );
    193 
    194 		wp_after_insert_post( $attachment, false, null );
    195 
    196 		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
    197 			// Set a custom header with the attachment_id.
    198 			// Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
    199 			header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
    200 		}
    201 
    202 		// Include media and image functions to get access to wp_generate_attachment_metadata().
    203 		require_once ABSPATH . 'wp-admin/includes/media.php';
    204 		require_once ABSPATH . 'wp-admin/includes/image.php';
    205 
    206 		// Post-process the upload (create image sub-sizes, make PDF thumbnails, etc.) and insert attachment meta.
    207 		// At this point the server may run out of resources and post-processing of uploaded images may fail.
    208 		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
    209 
    210 		$response = $this->prepare_item_for_response( $attachment, $request );
    211 		$response = rest_ensure_response( $response );
    212 		$response->set_status( 201 );
    213 		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );
    214 
    215 		return $response;
    216 	}
    217 
    218 	/**
    219 	 * Inserts the attachment post in the database. Does not update the attachment meta.
    220 	 *
    221 	 * @since 5.3.0
    222 	 *
    223 	 * @param WP_REST_Request $request
    224 	 * @return array|WP_Error
    225 	 */
    226 	protected function insert_attachment( $request ) {
    227 		// Get the file via $_FILES or raw data.
    228 		$files   = $request->get_file_params();
    229 		$headers = $request->get_headers();
    230 
    231 		if ( ! empty( $files ) ) {
    232 			$file = $this->upload_from_file( $files, $headers );
    233 		} else {
    234 			$file = $this->upload_from_data( $request->get_body(), $headers );
    235 		}
    236 
    237 		if ( is_wp_error( $file ) ) {
    238 			return $file;
    239 		}
    240 
    241 		$name       = wp_basename( $file['file'] );
    242 		$name_parts = pathinfo( $name );
    243 		$name       = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) );
    244 
    245 		$url  = $file['url'];
    246 		$type = $file['type'];
    247 		$file = $file['file'];
    248 
    249 		// Include image functions to get access to wp_read_image_metadata().
    250 		require_once ABSPATH . 'wp-admin/includes/image.php';
    251 
    252 		// Use image exif/iptc data for title and caption defaults if possible.
    253 		$image_meta = wp_read_image_metadata( $file );
    254 
    255 		if ( ! empty( $image_meta ) ) {
    256 			if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
    257 				$request['title'] = $image_meta['title'];
    258 			}
    259 
    260 			if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
    261 				$request['caption'] = $image_meta['caption'];
    262 			}
    263 		}
    264 
    265 		$attachment = $this->prepare_item_for_database( $request );
    266 
    267 		$attachment->post_mime_type = $type;
    268 		$attachment->guid           = $url;
    269 
    270 		if ( empty( $attachment->post_title ) ) {
    271 			$attachment->post_title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
    272 		}
    273 
    274 		// $post_parent is inherited from $attachment['post_parent'].
    275 		$id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false );
    276 
    277 		if ( is_wp_error( $id ) ) {
    278 			if ( 'db_update_error' === $id->get_error_code() ) {
    279 				$id->add_data( array( 'status' => 500 ) );
    280 			} else {
    281 				$id->add_data( array( 'status' => 400 ) );
    282 			}
    283 
    284 			return $id;
    285 		}
    286 
    287 		$attachment = get_post( $id );
    288 
    289 		/**
    290 		 * Fires after a single attachment is created or updated via the REST API.
    291 		 *
    292 		 * @since 4.7.0
    293 		 *
    294 		 * @param WP_Post         $attachment Inserted or updated attachment
    295 		 *                                    object.
    296 		 * @param WP_REST_Request $request    The request sent to the API.
    297 		 * @param bool            $creating   True when creating an attachment, false when updating.
    298 		 */
    299 		do_action( 'rest_insert_attachment', $attachment, $request, true );
    300 
    301 		return array(
    302 			'attachment_id' => $id,
    303 			'file'          => $file,
    304 		);
    305 	}
    306 
    307 	/**
    308 	 * Updates a single attachment.
    309 	 *
    310 	 * @since 4.7.0
    311 	 *
    312 	 * @param WP_REST_Request $request Full details about the request.
    313 	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
    314 	 */
    315 	public function update_item( $request ) {
    316 		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
    317 			return new WP_Error(
    318 				'rest_invalid_param',
    319 				__( 'Invalid parent type.' ),
    320 				array( 'status' => 400 )
    321 			);
    322 		}
    323 
    324 		$attachment_before = get_post( $request['id'] );
    325 		$response          = parent::update_item( $request );
    326 
    327 		if ( is_wp_error( $response ) ) {
    328 			return $response;
    329 		}
    330 
    331 		$response = rest_ensure_response( $response );
    332 		$data     = $response->get_data();
    333 
    334 		if ( isset( $request['alt_text'] ) ) {
    335 			update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
    336 		}
    337 
    338 		$attachment = get_post( $request['id'] );
    339 
    340 		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
    341 
    342 		if ( is_wp_error( $fields_update ) ) {
    343 			return $fields_update;
    344 		}
    345 
    346 		$request->set_param( 'context', 'edit' );
    347 
    348 		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
    349 		do_action( 'rest_after_insert_attachment', $attachment, $request, false );
    350 
    351 		wp_after_insert_post( $attachment, true, $attachment_before );
    352 
    353 		$response = $this->prepare_item_for_response( $attachment, $request );
    354 		$response = rest_ensure_response( $response );
    355 
    356 		return $response;
    357 	}
    358 
    359 	/**
    360 	 * Performs post processing on an attachment.
    361 	 *
    362 	 * @since 5.3.0
    363 	 *
    364 	 * @param WP_REST_Request $request Full details about the request.
    365 	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
    366 	 */
    367 	public function post_process_item( $request ) {
    368 		switch ( $request['action'] ) {
    369 			case 'create-image-subsizes':
    370 				require_once ABSPATH . 'wp-admin/includes/image.php';
    371 				wp_update_image_subsizes( $request['id'] );
    372 				break;
    373 		}
    374 
    375 		$request['context'] = 'edit';
    376 
    377 		return $this->prepare_item_for_response( get_post( $request['id'] ), $request );
    378 	}
    379 
    380 	/**
    381 	 * Checks if a given request can perform post processing on an attachment.
    382 	 *
    383 	 * @since 5.3.0
    384 	 *
    385 	 * @param WP_REST_Request $request Full details about the request.
    386 	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
    387 	 */
    388 	public function post_process_item_permissions_check( $request ) {
    389 		return $this->update_item_permissions_check( $request );
    390 	}
    391 
    392 	/**
    393 	 * Checks if a given request has access to editing media.
    394 	 *
    395 	 * @since 5.5.0
    396 	 *
    397 	 * @param WP_REST_Request $request Full details about the request.
    398 	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
    399 	 */
    400 	public function edit_media_item_permissions_check( $request ) {
    401 		if ( ! current_user_can( 'upload_files' ) ) {
    402 			return new WP_Error(
    403 				'rest_cannot_edit_image',
    404 				__( 'Sorry, you are not allowed to upload media on this site.' ),
    405 				array( 'status' => rest_authorization_required_code() )
    406 			);
    407 		}
    408 
    409 		return $this->update_item_permissions_check( $request );
    410 	}
    411 
    412 	/**
    413 	 * Applies edits to a media item and creates a new attachment record.
    414 	 *
    415 	 * @since 5.5.0
    416 	 *
    417 	 * @param WP_REST_Request $request Full details about the request.
    418 	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
    419 	 */
    420 	public function edit_media_item( $request ) {
    421 		require_once ABSPATH . 'wp-admin/includes/image.php';
    422 
    423 		$attachment_id = $request['id'];
    424 
    425 		// This also confirms the attachment is an image.
    426 		$image_file = wp_get_original_image_path( $attachment_id );
    427 		$image_meta = wp_get_attachment_metadata( $attachment_id );
    428 
    429 		if (
    430 			! $image_meta ||
    431 			! $image_file ||
    432 			! wp_image_file_matches_image_meta( $request['src'], $image_meta, $attachment_id )
    433 		) {
    434 			return new WP_Error(
    435 				'rest_unknown_attachment',
    436 				__( 'Unable to get meta information for file.' ),
    437 				array( 'status' => 404 )
    438 			);
    439 		}
    440 
    441 		$supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
    442 		$mime_type       = get_post_mime_type( $attachment_id );
    443 		if ( ! in_array( $mime_type, $supported_types, true ) ) {
    444 			return new WP_Error(
    445 				'rest_cannot_edit_file_type',
    446 				__( 'This type of file cannot be edited.' ),
    447 				array( 'status' => 400 )
    448 			);
    449 		}
    450 
    451 		// The `modifiers` param takes precedence over the older format.
    452 		if ( isset( $request['modifiers'] ) ) {
    453 			$modifiers = $request['modifiers'];
    454 		} else {
    455 			$modifiers = array();
    456 
    457 			if ( ! empty( $request['rotation'] ) ) {
    458 				$modifiers[] = array(
    459 					'type' => 'rotate',
    460 					'args' => array(
    461 						'angle' => $request['rotation'],
    462 					),
    463 				);
    464 			}
    465 
    466 			if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) {
    467 				$modifiers[] = array(
    468 					'type' => 'crop',
    469 					'args' => array(
    470 						'left'   => $request['x'],
    471 						'top'    => $request['y'],
    472 						'width'  => $request['width'],
    473 						'height' => $request['height'],
    474 					),
    475 				);
    476 			}
    477 
    478 			if ( 0 === count( $modifiers ) ) {
    479 				return new WP_Error(
    480 					'rest_image_not_edited',
    481 					__( 'The image was not edited. Edit the image before applying the changes.' ),
    482 					array( 'status' => 400 )
    483 				);
    484 			}
    485 		}
    486 
    487 		/*
    488 		 * If the file doesn't exist, attempt a URL fopen on the src link.
    489 		 * This can occur with certain file replication plugins.
    490 		 * Keep the original file path to get a modified name later.
    491 		 */
    492 		$image_file_to_edit = $image_file;
    493 		if ( ! file_exists( $image_file_to_edit ) ) {
    494 			$image_file_to_edit = _load_image_to_edit_path( $attachment_id );
    495 		}
    496 
    497 		$image_editor = wp_get_image_editor( $image_file_to_edit );
    498 
    499 		if ( is_wp_error( $image_editor ) ) {
    500 			return new WP_Error(
    501 				'rest_unknown_image_file_type',
    502 				__( 'Unable to edit this image.' ),
    503 				array( 'status' => 500 )
    504 			);
    505 		}
    506 
    507 		foreach ( $modifiers as $modifier ) {
    508 			$args = $modifier['args'];
    509 			switch ( $modifier['type'] ) {
    510 				case 'rotate':
    511 					// Rotation direction: clockwise vs. counter clockwise.
    512 					$rotate = 0 - $args['angle'];
    513 
    514 					if ( 0 !== $rotate ) {
    515 						$result = $image_editor->rotate( $rotate );
    516 
    517 						if ( is_wp_error( $result ) ) {
    518 							return new WP_Error(
    519 								'rest_image_rotation_failed',
    520 								__( 'Unable to rotate this image.' ),
    521 								array( 'status' => 500 )
    522 							);
    523 						}
    524 					}
    525 
    526 					break;
    527 
    528 				case 'crop':
    529 					$size = $image_editor->get_size();
    530 
    531 					$crop_x = round( ( $size['width'] * $args['left'] ) / 100.0 );
    532 					$crop_y = round( ( $size['height'] * $args['top'] ) / 100.0 );
    533 					$width  = round( ( $size['width'] * $args['width'] ) / 100.0 );
    534 					$height = round( ( $size['height'] * $args['height'] ) / 100.0 );
    535 
    536 					if ( $size['width'] !== $width && $size['height'] !== $height ) {
    537 						$result = $image_editor->crop( $crop_x, $crop_y, $width, $height );
    538 
    539 						if ( is_wp_error( $result ) ) {
    540 							return new WP_Error(
    541 								'rest_image_crop_failed',
    542 								__( 'Unable to crop this image.' ),
    543 								array( 'status' => 500 )
    544 							);
    545 						}
    546 					}
    547 
    548 					break;
    549 
    550 			}
    551 		}
    552 
    553 		// Calculate the file name.
    554 		$image_ext  = pathinfo( $image_file, PATHINFO_EXTENSION );
    555 		$image_name = wp_basename( $image_file, ".{$image_ext}" );
    556 
    557 		// Do not append multiple `-edited` to the file name.
    558 		// The user may be editing a previously edited image.
    559 		if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) {
    560 			// Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number.
    561 			$image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name );
    562 		} else {
    563 			// Append `-edited` before the extension.
    564 			$image_name .= '-edited';
    565 		}
    566 
    567 		$filename = "{$image_name}.{$image_ext}";
    568 
    569 		// Create the uploads sub-directory if needed.
    570 		$uploads = wp_upload_dir();
    571 
    572 		// Make the file name unique in the (new) upload directory.
    573 		$filename = wp_unique_filename( $uploads['path'], $filename );
    574 
    575 		// Save to disk.
    576 		$saved = $image_editor->save( $uploads['path'] . "/$filename" );
    577 
    578 		if ( is_wp_error( $saved ) ) {
    579 			return $saved;
    580 		}
    581 
    582 		// Create new attachment post.
    583 		$new_attachment_post = array(
    584 			'post_mime_type' => $saved['mime-type'],
    585 			'guid'           => $uploads['url'] . "/$filename",
    586 			'post_title'     => $image_name,
    587 			'post_content'   => '',
    588 		);
    589 
    590 		// Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
    591 		$attachment_post = get_post( $attachment_id );
    592 
    593 		if ( $attachment_post ) {
    594 			$new_attachment_post['post_content'] = $attachment_post->post_content;
    595 			$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
    596 			$new_attachment_post['post_title']   = $attachment_post->post_title;
    597 		}
    598 
    599 		$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );
    600 
    601 		if ( is_wp_error( $new_attachment_id ) ) {
    602 			if ( 'db_update_error' === $new_attachment_id->get_error_code() ) {
    603 				$new_attachment_id->add_data( array( 'status' => 500 ) );
    604 			} else {
    605 				$new_attachment_id->add_data( array( 'status' => 400 ) );
    606 			}
    607 
    608 			return $new_attachment_id;
    609 		}
    610 
    611 		// Copy the image alt text from the edited image.
    612 		$image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
    613 
    614 		if ( ! empty( $image_alt ) ) {
    615 			// update_post_meta() expects slashed.
    616 			update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
    617 		}
    618 
    619 		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
    620 			// Set a custom header with the attachment_id.
    621 			// Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
    622 			header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id );
    623 		}
    624 
    625 		// Generate image sub-sizes and meta.
    626 		$new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] );
    627 
    628 		// Copy the EXIF metadata from the original attachment if not generated for the edited image.
    629 		if ( isset( $image_meta['image_meta'] ) && isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) {
    630 			// Merge but skip empty values.
    631 			foreach ( (array) $image_meta['image_meta'] as $key => $value ) {
    632 				if ( empty( $new_image_meta['image_meta'][ $key ] ) && ! empty( $value ) ) {
    633 					$new_image_meta['image_meta'][ $key ] = $value;
    634 				}
    635 			}
    636 		}
    637 
    638 		// Reset orientation. At this point the image is edited and orientation is correct.
    639 		if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) {
    640 			$new_image_meta['image_meta']['orientation'] = 1;
    641 		}
    642 
    643 		// The attachment_id may change if the site is exported and imported.
    644 		$new_image_meta['parent_image'] = array(
    645 			'attachment_id' => $attachment_id,
    646 			// Path to the originally uploaded image file relative to the uploads directory.
    647 			'file'          => _wp_relative_upload_path( $image_file ),
    648 		);
    649 
    650 		/**
    651 		 * Filters the meta data for the new image created by editing an existing image.
    652 		 *
    653 		 * @since 5.5.0
    654 		 *
    655 		 * @param array $new_image_meta    Meta data for the new image.
    656 		 * @param int   $new_attachment_id Attachment post ID for the new image.
    657 		 * @param int   $attachment_id     Attachment post ID for the edited (parent) image.
    658 		 */
    659 		$new_image_meta = apply_filters( 'wp_edited_image_metadata', $new_image_meta, $new_attachment_id, $attachment_id );
    660 
    661 		wp_update_attachment_metadata( $new_attachment_id, $new_image_meta );
    662 
    663 		$response = $this->prepare_item_for_response( get_post( $new_attachment_id ), $request );
    664 		$response->set_status( 201 );
    665 		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $new_attachment_id ) ) );
    666 
    667 		return $response;
    668 	}
    669 
    670 	/**
    671 	 * Prepares a single attachment for create or update.
    672 	 *
    673 	 * @since 4.7.0
    674 	 *
    675 	 * @param WP_REST_Request $request Request object.
    676 	 * @return stdClass|WP_Error Post object.
    677 	 */
    678 	protected function prepare_item_for_database( $request ) {
    679 		$prepared_attachment = parent::prepare_item_for_database( $request );
    680 
    681 		// Attachment caption (post_excerpt internally).
    682 		if ( isset( $request['caption'] ) ) {
    683 			if ( is_string( $request['caption'] ) ) {
    684 				$prepared_attachment->post_excerpt = $request['caption'];
    685 			} elseif ( isset( $request['caption']['raw'] ) ) {
    686 				$prepared_attachment->post_excerpt = $request['caption']['raw'];
    687 			}
    688 		}
    689 
    690 		// Attachment description (post_content internally).
    691 		if ( isset( $request['description'] ) ) {
    692 			if ( is_string( $request['description'] ) ) {
    693 				$prepared_attachment->post_content = $request['description'];
    694 			} elseif ( isset( $request['description']['raw'] ) ) {
    695 				$prepared_attachment->post_content = $request['description']['raw'];
    696 			}
    697 		}
    698 
    699 		if ( isset( $request['post'] ) ) {
    700 			$prepared_attachment->post_parent = (int) $request['post'];
    701 		}
    702 
    703 		return $prepared_attachment;
    704 	}
    705 
    706 	/**
    707 	 * Prepares a single attachment output for response.
    708 	 *
    709 	 * @since 4.7.0
    710 	 *
    711 	 * @param WP_Post         $post    Attachment object.
    712 	 * @param WP_REST_Request $request Request object.
    713 	 * @return WP_REST_Response Response object.
    714 	 */
    715 	public function prepare_item_for_response( $post, $request ) {
    716 		$response = parent::prepare_item_for_response( $post, $request );
    717 		$fields   = $this->get_fields_for_response( $request );
    718 		$data     = $response->get_data();
    719 
    720 		if ( in_array( 'description', $fields, true ) ) {
    721 			$data['description'] = array(
    722 				'raw'      => $post->post_content,
    723 				/** This filter is documented in wp-includes/post-template.php */
    724 				'rendered' => apply_filters( 'the_content', $post->post_content ),
    725 			);
    726 		}
    727 
    728 		if ( in_array( 'caption', $fields, true ) ) {
    729 			/** This filter is documented in wp-includes/post-template.php */
    730 			$caption = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
    731 
    732 			/** This filter is documented in wp-includes/post-template.php */
    733 			$caption = apply_filters( 'the_excerpt', $caption );
    734 
    735 			$data['caption'] = array(
    736 				'raw'      => $post->post_excerpt,
    737 				'rendered' => $caption,
    738 			);
    739 		}
    740 
    741 		if ( in_array( 'alt_text', $fields, true ) ) {
    742 			$data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
    743 		}
    744 
    745 		if ( in_array( 'media_type', $fields, true ) ) {
    746 			$data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
    747 		}
    748 
    749 		if ( in_array( 'mime_type', $fields, true ) ) {
    750 			$data['mime_type'] = $post->post_mime_type;
    751 		}
    752 
    753 		if ( in_array( 'media_details', $fields, true ) ) {
    754 			$data['media_details'] = wp_get_attachment_metadata( $post->ID );
    755 
    756 			// Ensure empty details is an empty object.
    757 			if ( empty( $data['media_details'] ) ) {
    758 				$data['media_details'] = new stdClass;
    759 			} elseif ( ! empty( $data['media_details']['sizes'] ) ) {
    760 
    761 				foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
    762 
    763 					if ( isset( $size_data['mime-type'] ) ) {
    764 						$size_data['mime_type'] = $size_data['mime-type'];
    765 						unset( $size_data['mime-type'] );
    766 					}
    767 
    768 					// Use the same method image_downsize() does.
    769 					$image_src = wp_get_attachment_image_src( $post->ID, $size );
    770 					if ( ! $image_src ) {
    771 						continue;
    772 					}
    773 
    774 					$size_data['source_url'] = $image_src[0];
    775 				}
    776 
    777 				$full_src = wp_get_attachment_image_src( $post->ID, 'full' );
    778 
    779 				if ( ! empty( $full_src ) ) {
    780 					$data['media_details']['sizes']['full'] = array(
    781 						'file'       => wp_basename( $full_src[0] ),
    782 						'width'      => $full_src[1],
    783 						'height'     => $full_src[2],
    784 						'mime_type'  => $post->post_mime_type,
    785 						'source_url' => $full_src[0],
    786 					);
    787 				}
    788 			} else {
    789 				$data['media_details']['sizes'] = new stdClass;
    790 			}
    791 		}
    792 
    793 		if ( in_array( 'post', $fields, true ) ) {
    794 			$data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
    795 		}
    796 
    797 		if ( in_array( 'source_url', $fields, true ) ) {
    798 			$data['source_url'] = wp_get_attachment_url( $post->ID );
    799 		}
    800 
    801 		if ( in_array( 'missing_image_sizes', $fields, true ) ) {
    802 			require_once ABSPATH . 'wp-admin/includes/image.php';
    803 			$data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) );
    804 		}
    805 
    806 		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
    807 
    808 		$data = $this->filter_response_by_context( $data, $context );
    809 
    810 		$links = $response->get_links();
    811 
    812 		// Wrap the data in a response object.
    813 		$response = rest_ensure_response( $data );
    814 
    815 		foreach ( $links as $rel => $rel_links ) {
    816 			foreach ( $rel_links as $link ) {
    817 				$response->add_link( $rel, $link['href'], $link['attributes'] );
    818 			}
    819 		}
    820 
    821 		/**
    822 		 * Filters an attachment returned from the REST API.
    823 		 *
    824 		 * Allows modification of the attachment right before it is returned.
    825 		 *
    826 		 * @since 4.7.0
    827 		 *
    828 		 * @param WP_REST_Response $response The response object.
    829 		 * @param WP_Post          $post     The original attachment post.
    830 		 * @param WP_REST_Request  $request  Request used to generate the response.
    831 		 */
    832 		return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
    833 	}
    834 
    835 	/**
    836 	 * Retrieves the attachment's schema, conforming to JSON Schema.
    837 	 *
    838 	 * @since 4.7.0
    839 	 *
    840 	 * @return array Item schema as an array.
    841 	 */
    842 	public function get_item_schema() {
    843 		if ( $this->schema ) {
    844 			return $this->add_additional_fields_schema( $this->schema );
    845 		}
    846 
    847 		$schema = parent::get_item_schema();
    848 
    849 		$schema['properties']['alt_text'] = array(
    850 			'description' => __( 'Alternative text to display when attachment is not displayed.' ),
    851 			'type'        => 'string',
    852 			'context'     => array( 'view', 'edit', 'embed' ),
    853 			'arg_options' => array(
    854 				'sanitize_callback' => 'sanitize_text_field',
    855 			),
    856 		);
    857 
    858 		$schema['properties']['caption'] = array(
    859 			'description' => __( 'The attachment caption.' ),
    860 			'type'        => 'object',
    861 			'context'     => array( 'view', 'edit', 'embed' ),
    862 			'arg_options' => array(
    863 				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
    864 				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
    865 			),
    866 			'properties'  => array(
    867 				'raw'      => array(
    868 					'description' => __( 'Caption for the attachment, as it exists in the database.' ),
    869 					'type'        => 'string',
    870 					'context'     => array( 'edit' ),
    871 				),
    872 				'rendered' => array(
    873 					'description' => __( 'HTML caption for the attachment, transformed for display.' ),
    874 					'type'        => 'string',
    875 					'context'     => array( 'view', 'edit', 'embed' ),
    876 					'readonly'    => true,
    877 				),
    878 			),
    879 		);
    880 
    881 		$schema['properties']['description'] = array(
    882 			'description' => __( 'The attachment description.' ),
    883 			'type'        => 'object',
    884 			'context'     => array( 'view', 'edit' ),
    885 			'arg_options' => array(
    886 				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
    887 				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
    888 			),
    889 			'properties'  => array(
    890 				'raw'      => array(
    891 					'description' => __( 'Description for the attachment, as it exists in the database.' ),
    892 					'type'        => 'string',
    893 					'context'     => array( 'edit' ),
    894 				),
    895 				'rendered' => array(
    896 					'description' => __( 'HTML description for the attachment, transformed for display.' ),
    897 					'type'        => 'string',
    898 					'context'     => array( 'view', 'edit' ),
    899 					'readonly'    => true,
    900 				),
    901 			),
    902 		);
    903 
    904 		$schema['properties']['media_type'] = array(
    905 			'description' => __( 'Attachment type.' ),
    906 			'type'        => 'string',
    907 			'enum'        => array( 'image', 'file' ),
    908 			'context'     => array( 'view', 'edit', 'embed' ),
    909 			'readonly'    => true,
    910 		);
    911 
    912 		$schema['properties']['mime_type'] = array(
    913 			'description' => __( 'The attachment MIME type.' ),
    914 			'type'        => 'string',
    915 			'context'     => array( 'view', 'edit', 'embed' ),
    916 			'readonly'    => true,
    917 		);
    918 
    919 		$schema['properties']['media_details'] = array(
    920 			'description' => __( 'Details about the media file, specific to its type.' ),
    921 			'type'        => 'object',
    922 			'context'     => array( 'view', 'edit', 'embed' ),
    923 			'readonly'    => true,
    924 		);
    925 
    926 		$schema['properties']['post'] = array(
    927 			'description' => __( 'The ID for the associated post of the attachment.' ),
    928 			'type'        => 'integer',
    929 			'context'     => array( 'view', 'edit' ),
    930 		);
    931 
    932 		$schema['properties']['source_url'] = array(
    933 			'description' => __( 'URL to the original attachment file.' ),
    934 			'type'        => 'string',
    935 			'format'      => 'uri',
    936 			'context'     => array( 'view', 'edit', 'embed' ),
    937 			'readonly'    => true,
    938 		);
    939 
    940 		$schema['properties']['missing_image_sizes'] = array(
    941 			'description' => __( 'List of the missing image sizes of the attachment.' ),
    942 			'type'        => 'array',
    943 			'items'       => array( 'type' => 'string' ),
    944 			'context'     => array( 'edit' ),
    945 			'readonly'    => true,
    946 		);
    947 
    948 		unset( $schema['properties']['password'] );
    949 
    950 		$this->schema = $schema;
    951 
    952 		return $this->add_additional_fields_schema( $this->schema );
    953 	}
    954 
    955 	/**
    956 	 * Handles an upload via raw POST data.
    957 	 *
    958 	 * @since 4.7.0
    959 	 *
    960 	 * @param array $data    Supplied file data.
    961 	 * @param array $headers HTTP headers from the request.
    962 	 * @return array|WP_Error Data from wp_handle_sideload().
    963 	 */
    964 	protected function upload_from_data( $data, $headers ) {
    965 		if ( empty( $data ) ) {
    966 			return new WP_Error(
    967 				'rest_upload_no_data',
    968 				__( 'No data supplied.' ),
    969 				array( 'status' => 400 )
    970 			);
    971 		}
    972 
    973 		if ( empty( $headers['content_type'] ) ) {
    974 			return new WP_Error(
    975 				'rest_upload_no_content_type',
    976 				__( 'No Content-Type supplied.' ),
    977 				array( 'status' => 400 )
    978 			);
    979 		}
    980 
    981 		if ( empty( $headers['content_disposition'] ) ) {
    982 			return new WP_Error(
    983 				'rest_upload_no_content_disposition',
    984 				__( 'No Content-Disposition supplied.' ),
    985 				array( 'status' => 400 )
    986 			);
    987 		}
    988 
    989 		$filename = self::get_filename_from_disposition( $headers['content_disposition'] );
    990 
    991 		if ( empty( $filename ) ) {
    992 			return new WP_Error(
    993 				'rest_upload_invalid_disposition',
    994 				__( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ),
    995 				array( 'status' => 400 )
    996 			);
    997 		}
    998 
    999 		if ( ! empty( $headers['content_md5'] ) ) {
   1000 			$content_md5 = array_shift( $headers['content_md5'] );
   1001 			$expected    = trim( $content_md5 );
   1002 			$actual      = md5( $data );
   1003 
   1004 			if ( $expected !== $actual ) {
   1005 				return new WP_Error(
   1006 					'rest_upload_hash_mismatch',
   1007 					__( 'Content hash did not match expected.' ),
   1008 					array( 'status' => 412 )
   1009 				);
   1010 			}
   1011 		}
   1012 
   1013 		// Get the content-type.
   1014 		$type = array_shift( $headers['content_type'] );
   1015 
   1016 		// Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload().
   1017 		require_once ABSPATH . 'wp-admin/includes/file.php';
   1018 
   1019 		// Save the file.
   1020 		$tmpfname = wp_tempnam( $filename );
   1021 
   1022 		$fp = fopen( $tmpfname, 'w+' );
   1023 
   1024 		if ( ! $fp ) {
   1025 			return new WP_Error(
   1026 				'rest_upload_file_error',
   1027 				__( 'Could not open file handle.' ),
   1028 				array( 'status' => 500 )
   1029 			);
   1030 		}
   1031 
   1032 		fwrite( $fp, $data );
   1033 		fclose( $fp );
   1034 
   1035 		// Now, sideload it in.
   1036 		$file_data = array(
   1037 			'error'    => null,
   1038 			'tmp_name' => $tmpfname,
   1039 			'name'     => $filename,
   1040 			'type'     => $type,
   1041 		);
   1042 
   1043 		$size_check = self::check_upload_size( $file_data );
   1044 		if ( is_wp_error( $size_check ) ) {
   1045 			return $size_check;
   1046 		}
   1047 
   1048 		$overrides = array(
   1049 			'test_form' => false,
   1050 		);
   1051 
   1052 		$sideloaded = wp_handle_sideload( $file_data, $overrides );
   1053 
   1054 		if ( isset( $sideloaded['error'] ) ) {
   1055 			@unlink( $tmpfname );
   1056 
   1057 			return new WP_Error(
   1058 				'rest_upload_sideload_error',
   1059 				$sideloaded['error'],
   1060 				array( 'status' => 500 )
   1061 			);
   1062 		}
   1063 
   1064 		return $sideloaded;
   1065 	}
   1066 
   1067 	/**
   1068 	 * Parses filename from a Content-Disposition header value.
   1069 	 *
   1070 	 * As per RFC6266:
   1071 	 *
   1072 	 *     content-disposition = "Content-Disposition" ":"
   1073 	 *                            disposition-type *( ";" disposition-parm )
   1074 	 *
   1075 	 *     disposition-type    = "inline" | "attachment" | disp-ext-type
   1076 	 *                         ; case-insensitive
   1077 	 *     disp-ext-type       = token
   1078 	 *
   1079 	 *     disposition-parm    = filename-parm | disp-ext-parm
   1080 	 *
   1081 	 *     filename-parm       = "filename" "=" value
   1082 	 *                         | "filename*" "=" ext-value
   1083 	 *
   1084 	 *     disp-ext-parm       = token "=" value
   1085 	 *                         | ext-token "=" ext-value
   1086 	 *     ext-token           = <the characters in token, followed by "*">
   1087 	 *
   1088 	 * @since 4.7.0
   1089 	 *
   1090 	 * @link https://tools.ietf.org/html/rfc2388
   1091 	 * @link https://tools.ietf.org/html/rfc6266
   1092 	 *
   1093 	 * @param string[] $disposition_header List of Content-Disposition header values.
   1094 	 * @return string|null Filename if available, or null if not found.
   1095 	 */
   1096 	public static function get_filename_from_disposition( $disposition_header ) {
   1097 		// Get the filename.
   1098 		$filename = null;
   1099 
   1100 		foreach ( $disposition_header as $value ) {
   1101 			$value = trim( $value );
   1102 
   1103 			if ( strpos( $value, ';' ) === false ) {
   1104 				continue;
   1105 			}
   1106 
   1107 			list( $type, $attr_parts ) = explode( ';', $value, 2 );
   1108 
   1109 			$attr_parts = explode( ';', $attr_parts );
   1110 			$attributes = array();
   1111 
   1112 			foreach ( $attr_parts as $part ) {
   1113 				if ( strpos( $part, '=' ) === false ) {
   1114 					continue;
   1115 				}
   1116 
   1117 				list( $key, $value ) = explode( '=', $part, 2 );
   1118 
   1119 				$attributes[ trim( $key ) ] = trim( $value );
   1120 			}
   1121 
   1122 			if ( empty( $attributes['filename'] ) ) {
   1123 				continue;
   1124 			}
   1125 
   1126 			$filename = trim( $attributes['filename'] );
   1127 
   1128 			// Unquote quoted filename, but after trimming.
   1129 			if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
   1130 				$filename = substr( $filename, 1, -1 );
   1131 			}
   1132 		}
   1133 
   1134 		return $filename;
   1135 	}
   1136 
   1137 	/**
   1138 	 * Retrieves the query params for collections of attachments.
   1139 	 *
   1140 	 * @since 4.7.0
   1141 	 *
   1142 	 * @return array Query parameters for the attachment collection as an array.
   1143 	 */
   1144 	public function get_collection_params() {
   1145 		$params                            = parent::get_collection_params();
   1146 		$params['status']['default']       = 'inherit';
   1147 		$params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' );
   1148 		$media_types                       = $this->get_media_types();
   1149 
   1150 		$params['media_type'] = array(
   1151 			'default'     => null,
   1152 			'description' => __( 'Limit result set to attachments of a particular media type.' ),
   1153 			'type'        => 'string',
   1154 			'enum'        => array_keys( $media_types ),
   1155 		);
   1156 
   1157 		$params['mime_type'] = array(
   1158 			'default'     => null,
   1159 			'description' => __( 'Limit result set to attachments of a particular MIME type.' ),
   1160 			'type'        => 'string',
   1161 		);
   1162 
   1163 		return $params;
   1164 	}
   1165 
   1166 	/**
   1167 	 * Handles an upload via multipart/form-data ($_FILES).
   1168 	 *
   1169 	 * @since 4.7.0
   1170 	 *
   1171 	 * @param array $files   Data from the `$_FILES` superglobal.
   1172 	 * @param array $headers HTTP headers from the request.
   1173 	 * @return array|WP_Error Data from wp_handle_upload().
   1174 	 */
   1175 	protected function upload_from_file( $files, $headers ) {
   1176 		if ( empty( $files ) ) {
   1177 			return new WP_Error(
   1178 				'rest_upload_no_data',
   1179 				__( 'No data supplied.' ),
   1180 				array( 'status' => 400 )
   1181 			);
   1182 		}
   1183 
   1184 		// Verify hash, if given.
   1185 		if ( ! empty( $headers['content_md5'] ) ) {
   1186 			$content_md5 = array_shift( $headers['content_md5'] );
   1187 			$expected    = trim( $content_md5 );
   1188 			$actual      = md5_file( $files['file']['tmp_name'] );
   1189 
   1190 			if ( $expected !== $actual ) {
   1191 				return new WP_Error(
   1192 					'rest_upload_hash_mismatch',
   1193 					__( 'Content hash did not match expected.' ),
   1194 					array( 'status' => 412 )
   1195 				);
   1196 			}
   1197 		}
   1198 
   1199 		// Pass off to WP to handle the actual upload.
   1200 		$overrides = array(
   1201 			'test_form' => false,
   1202 		);
   1203 
   1204 		// Bypasses is_uploaded_file() when running unit tests.
   1205 		if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
   1206 			$overrides['action'] = 'wp_handle_mock_upload';
   1207 		}
   1208 
   1209 		$size_check = self::check_upload_size( $files['file'] );
   1210 		if ( is_wp_error( $size_check ) ) {
   1211 			return $size_check;
   1212 		}
   1213 
   1214 		// Include filesystem functions to get access to wp_handle_upload().
   1215 		require_once ABSPATH . 'wp-admin/includes/file.php';
   1216 
   1217 		$file = wp_handle_upload( $files['file'], $overrides );
   1218 
   1219 		if ( isset( $file['error'] ) ) {
   1220 			return new WP_Error(
   1221 				'rest_upload_unknown_error',
   1222 				$file['error'],
   1223 				array( 'status' => 500 )
   1224 			);
   1225 		}
   1226 
   1227 		return $file;
   1228 	}
   1229 
   1230 	/**
   1231 	 * Retrieves the supported media types.
   1232 	 *
   1233 	 * Media types are considered the MIME type category.
   1234 	 *
   1235 	 * @since 4.7.0
   1236 	 *
   1237 	 * @return array Array of supported media types.
   1238 	 */
   1239 	protected function get_media_types() {
   1240 		$media_types = array();
   1241 
   1242 		foreach ( get_allowed_mime_types() as $mime_type ) {
   1243 			$parts = explode( '/', $mime_type );
   1244 
   1245 			if ( ! isset( $media_types[ $parts[0] ] ) ) {
   1246 				$media_types[ $parts[0] ] = array();
   1247 			}
   1248 
   1249 			$media_types[ $parts[0] ][] = $mime_type;
   1250 		}
   1251 
   1252 		return $media_types;
   1253 	}
   1254 
   1255 	/**
   1256 	 * Determine if uploaded file exceeds space quota on multisite.
   1257 	 *
   1258 	 * Replicates check_upload_size().
   1259 	 *
   1260 	 * @since 4.9.8
   1261 	 *
   1262 	 * @param array $file $_FILES array for a given file.
   1263 	 * @return true|WP_Error True if can upload, error for errors.
   1264 	 */
   1265 	protected function check_upload_size( $file ) {
   1266 		if ( ! is_multisite() ) {
   1267 			return true;
   1268 		}
   1269 
   1270 		if ( get_site_option( 'upload_space_check_disabled' ) ) {
   1271 			return true;
   1272 		}
   1273 
   1274 		$space_left = get_upload_space_available();
   1275 
   1276 		$file_size = filesize( $file['tmp_name'] );
   1277 
   1278 		if ( $space_left < $file_size ) {
   1279 			return new WP_Error(
   1280 				'rest_upload_limited_space',
   1281 				/* translators: %s: Required disk space in kilobytes. */
   1282 				sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ),
   1283 				array( 'status' => 400 )
   1284 			);
   1285 		}
   1286 
   1287 		if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
   1288 			return new WP_Error(
   1289 				'rest_upload_file_too_big',
   1290 				/* translators: %s: Maximum allowed file size in kilobytes. */
   1291 				sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ),
   1292 				array( 'status' => 400 )
   1293 			);
   1294 		}
   1295 
   1296 		// Include multisite admin functions to get access to upload_is_user_over_quota().
   1297 		require_once ABSPATH . 'wp-admin/includes/ms.php';
   1298 
   1299 		if ( upload_is_user_over_quota( false ) ) {
   1300 			return new WP_Error(
   1301 				'rest_upload_user_quota_exceeded',
   1302 				__( 'You have used your space quota. Please delete files before uploading.' ),
   1303 				array( 'status' => 400 )
   1304 			);
   1305 		}
   1306 
   1307 		return true;
   1308 	}
   1309 
   1310 	/**
   1311 	 * Gets the request args for the edit item route.
   1312 	 *
   1313 	 * @since 5.5.0
   1314 	 *
   1315 	 * @return array
   1316 	 */
   1317 	protected function get_edit_media_item_args() {
   1318 		return array(
   1319 			'src'       => array(
   1320 				'description' => __( 'URL to the edited image file.' ),
   1321 				'type'        => 'string',
   1322 				'format'      => 'uri',
   1323 				'required'    => true,
   1324 			),
   1325 			'modifiers' => array(
   1326 				'description' => __( 'Array of image edits.' ),
   1327 				'type'        => 'array',
   1328 				'minItems'    => 1,
   1329 				'items'       => array(
   1330 					'description' => __( 'Image edit.' ),
   1331 					'type'        => 'object',
   1332 					'required'    => array(
   1333 						'type',
   1334 						'args',
   1335 					),
   1336 					'oneOf'       => array(
   1337 						array(
   1338 							'title'      => __( 'Rotation' ),
   1339 							'properties' => array(
   1340 								'type' => array(
   1341 									'description' => __( 'Rotation type.' ),
   1342 									'type'        => 'string',
   1343 									'enum'        => array( 'rotate' ),
   1344 								),
   1345 								'args' => array(
   1346 									'description' => __( 'Rotation arguments.' ),
   1347 									'type'        => 'object',
   1348 									'required'    => array(
   1349 										'angle',
   1350 									),
   1351 									'properties'  => array(
   1352 										'angle' => array(
   1353 											'description' => __( 'Angle to rotate clockwise in degrees.' ),
   1354 											'type'        => 'number',
   1355 										),
   1356 									),
   1357 								),
   1358 							),
   1359 						),
   1360 						array(
   1361 							'title'      => __( 'Crop' ),
   1362 							'properties' => array(
   1363 								'type' => array(
   1364 									'description' => __( 'Crop type.' ),
   1365 									'type'        => 'string',
   1366 									'enum'        => array( 'crop' ),
   1367 								),
   1368 								'args' => array(
   1369 									'description' => __( 'Crop arguments.' ),
   1370 									'type'        => 'object',
   1371 									'required'    => array(
   1372 										'left',
   1373 										'top',
   1374 										'width',
   1375 										'height',
   1376 									),
   1377 									'properties'  => array(
   1378 										'left'   => array(
   1379 											'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ),
   1380 											'type'        => 'number',
   1381 										),
   1382 										'top'    => array(
   1383 											'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ),
   1384 											'type'        => 'number',
   1385 										),
   1386 										'width'  => array(
   1387 											'description' => __( 'Width of the crop as a percentage of the image width.' ),
   1388 											'type'        => 'number',
   1389 										),
   1390 										'height' => array(
   1391 											'description' => __( 'Height of the crop as a percentage of the image height.' ),
   1392 											'type'        => 'number',
   1393 										),
   1394 									),
   1395 								),
   1396 							),
   1397 						),
   1398 					),
   1399 				),
   1400 			),
   1401 			'rotation'  => array(
   1402 				'description'      => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ),
   1403 				'type'             => 'integer',
   1404 				'minimum'          => 0,
   1405 				'exclusiveMinimum' => true,
   1406 				'maximum'          => 360,
   1407 				'exclusiveMaximum' => true,
   1408 			),
   1409 			'x'         => array(
   1410 				'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
   1411 				'type'        => 'number',
   1412 				'minimum'     => 0,
   1413 				'maximum'     => 100,
   1414 			),
   1415 			'y'         => array(
   1416 				'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
   1417 				'type'        => 'number',
   1418 				'minimum'     => 0,
   1419 				'maximum'     => 100,
   1420 			),
   1421 			'width'     => array(
   1422 				'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
   1423 				'type'        => 'number',
   1424 				'minimum'     => 0,
   1425 				'maximum'     => 100,
   1426 			),
   1427 			'height'    => array(
   1428 				'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
   1429 				'type'        => 'number',
   1430 				'minimum'     => 0,
   1431 				'maximum'     => 100,
   1432 			),
   1433 		);
   1434 	}
   1435 
   1436 }