wp/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 18 be944660c56a
--- a/wp/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php	Tue Oct 22 16:11:46 2019 +0200
+++ b/wp/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php	Tue Dec 15 13:49:49 2020 +0100
@@ -17,6 +17,47 @@
 class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
 
 	/**
+	 * Registers the routes for attachments.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @see register_rest_route()
+	 */
+	public function register_routes() {
+		parent::register_routes();
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<id>[\d]+)/post-process',
+			array(
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => array( $this, 'post_process_item' ),
+				'permission_callback' => array( $this, 'post_process_item_permissions_check' ),
+				'args'                => array(
+					'id'     => array(
+						'description' => __( 'Unique identifier for the object.' ),
+						'type'        => 'integer',
+					),
+					'action' => array(
+						'type'     => 'string',
+						'enum'     => array( 'create-image-subsizes' ),
+						'required' => true,
+					),
+				),
+			)
+		);
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<id>[\d]+)/edit',
+			array(
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => array( $this, 'edit_media_item' ),
+				'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
+				'args'                => $this->get_edit_media_item_args(),
+			)
+		);
+	}
+
+	/**
 	 * Determines the allowed query_vars for a get_items() response and
 	 * prepares for WP_Query.
 	 *
@@ -60,7 +101,7 @@
 	 * @since 4.7.0
 	 *
 	 * @param WP_REST_Request $request Full details about the request.
-	 * @return WP_Error|true Boolean true if the attachment may be created, or a WP_Error if not.
+	 * @return true|WP_Error Boolean true if the attachment may be created, or a WP_Error if not.
 	 */
 	public function create_item_permissions_check( $request ) {
 		$ret = parent::create_item_permissions_check( $request );
@@ -70,17 +111,20 @@
 		}
 
 		if ( ! current_user_can( 'upload_files' ) ) {
-			return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to upload media on this site.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_cannot_create',
+				__( 'Sorry, you are not allowed to upload media on this site.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		// Attaching media to a post requires ability to edit said post.
-		if ( ! empty( $request['post'] ) ) {
-			$parent           = get_post( (int) $request['post'] );
-			$post_parent_type = get_post_type_object( $parent->post_type );
-
-			if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
-				return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to upload media to this post.' ), array( 'status' => rest_authorization_required_code() ) );
-			}
+		if ( ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) {
+			return new WP_Error(
+				'rest_cannot_edit',
+				__( 'Sorry, you are not allowed to upload media to this post.' ),
+				array( 'status' => rest_authorization_required_code() )
+			);
 		}
 
 		return true;
@@ -92,14 +136,92 @@
 	 * @since 4.7.0
 	 *
 	 * @param WP_REST_Request $request Full details about the request.
-	 * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
+	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
 	 */
 	public function create_item( $request ) {
+		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
+			return new WP_Error(
+				'rest_invalid_param',
+				__( 'Invalid parent type.' ),
+				array( 'status' => 400 )
+			);
+		}
 
-		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
-			return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
+		$insert = $this->insert_attachment( $request );
+
+		if ( is_wp_error( $insert ) ) {
+			return $insert;
+		}
+
+		$schema = $this->get_item_schema();
+
+		// Extract by name.
+		$attachment_id = $insert['attachment_id'];
+		$file          = $insert['file'];
+
+		if ( isset( $request['alt_text'] ) ) {
+			update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
+		}
+
+		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
+			$meta_update = $this->meta->update_value( $request['meta'], $attachment_id );
+
+			if ( is_wp_error( $meta_update ) ) {
+				return $meta_update;
+			}
+		}
+
+		$attachment    = get_post( $attachment_id );
+		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
+
+		if ( is_wp_error( $fields_update ) ) {
+			return $fields_update;
 		}
 
+		$request->set_param( 'context', 'edit' );
+
+		/**
+		 * Fires after a single attachment is completely created or updated via the REST API.
+		 *
+		 * @since 5.0.0
+		 *
+		 * @param WP_Post         $attachment Inserted or updated attachment object.
+		 * @param WP_REST_Request $request    Request object.
+		 * @param bool            $creating   True when creating an attachment, false when updating.
+		 */
+		do_action( 'rest_after_insert_attachment', $attachment, $request, true );
+
+		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
+			// Set a custom header with the attachment_id.
+			// Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
+			header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
+		}
+
+		// Include media and image functions to get access to wp_generate_attachment_metadata().
+		require_once ABSPATH . 'wp-admin/includes/media.php';
+		require_once ABSPATH . 'wp-admin/includes/image.php';
+
+		// Post-process the upload (create image sub-sizes, make PDF thumbnails, etc.) and insert attachment meta.
+		// At this point the server may run out of resources and post-processing of uploaded images may fail.
+		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
+
+		$response = $this->prepare_item_for_response( $attachment, $request );
+		$response = rest_ensure_response( $response );
+		$response->set_status( 201 );
+		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );
+
+		return $response;
+	}
+
+	/**
+	 * Inserts the attachment post in the database. Does not update the attachment meta.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @param WP_REST_Request $request
+	 * @return array|WP_Error
+	 */
+	protected function insert_attachment( $request ) {
 		// Get the file via $_FILES or raw data.
 		$files   = $request->get_file_params();
 		$headers = $request->get_headers();
@@ -125,7 +247,7 @@
 		// Include image functions to get access to wp_read_image_metadata().
 		require_once ABSPATH . 'wp-admin/includes/image.php';
 
-		// use image exif/iptc data for title and caption defaults if possible
+		// Use image exif/iptc data for title and caption defaults if possible.
 		$image_meta = wp_read_image_metadata( $file );
 
 		if ( ! empty( $image_meta ) ) {
@@ -138,7 +260,8 @@
 			}
 		}
 
-		$attachment                 = $this->prepare_item_for_database( $request );
+		$attachment = $this->prepare_item_for_database( $request );
+
 		$attachment->post_mime_type = $type;
 		$attachment->guid           = $url;
 
@@ -155,6 +278,7 @@
 			} else {
 				$id->add_data( array( 'status' => 400 ) );
 			}
+
 			return $id;
 		}
 
@@ -172,40 +296,10 @@
 		 */
 		do_action( 'rest_insert_attachment', $attachment, $request, true );
 
-		// Include admin function to get access to wp_generate_attachment_metadata().
-		require_once ABSPATH . 'wp-admin/includes/media.php';
-
-		wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
-
-		if ( isset( $request['alt_text'] ) ) {
-			update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
-		}
-
-		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
-
-		if ( is_wp_error( $fields_update ) ) {
-			return $fields_update;
-		}
-
-		$request->set_param( 'context', 'edit' );
-
-		/**
-		 * Fires after a single attachment is completely created or updated via the REST API.
-		 *
-		 * @since 5.0.0
-		 *
-		 * @param WP_Post         $attachment Inserted or updated attachment object.
-		 * @param WP_REST_Request $request    Request object.
-		 * @param bool            $creating   True when creating an attachment, false when updating.
-		 */
-		do_action( 'rest_after_insert_attachment', $attachment, $request, true );
-
-		$response = $this->prepare_item_for_response( $attachment, $request );
-		$response = rest_ensure_response( $response );
-		$response->set_status( 201 );
-		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );
-
-		return $response;
+		return array(
+			'attachment_id' => $id,
+			'file'          => $file,
+		);
 	}
 
 	/**
@@ -214,11 +308,15 @@
 	 * @since 4.7.0
 	 *
 	 * @param WP_REST_Request $request Full details about the request.
-	 * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
+	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
 	 */
 	public function update_item( $request ) {
 		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
-			return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_invalid_param',
+				__( 'Invalid parent type.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		$response = parent::update_item( $request );
@@ -254,17 +352,298 @@
 	}
 
 	/**
+	 * Performs post processing on an attachment.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
+	 */
+	public function post_process_item( $request ) {
+		switch ( $request['action'] ) {
+			case 'create-image-subsizes':
+				require_once ABSPATH . 'wp-admin/includes/image.php';
+				wp_update_image_subsizes( $request['id'] );
+				break;
+		}
+
+		$request['context'] = 'edit';
+
+		return $this->prepare_item_for_response( get_post( $request['id'] ), $request );
+	}
+
+	/**
+	 * Checks if a given request can perform post processing on an attachment.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
+	 */
+	public function post_process_item_permissions_check( $request ) {
+		return $this->update_item_permissions_check( $request );
+	}
+
+	/**
+	 * Checks if a given request has access to editing media.
+	 *
+	 * @since 5.5.0
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+	 */
+	public function edit_media_item_permissions_check( $request ) {
+		if ( ! current_user_can( 'upload_files' ) ) {
+			return new WP_Error(
+				'rest_cannot_edit_image',
+				__( 'Sorry, you are not allowed to upload media on this site.' ),
+				array( 'status' => rest_authorization_required_code() )
+			);
+		}
+
+		return $this->update_item_permissions_check( $request );
+	}
+
+	/**
+	 * Applies edits to a media item and creates a new attachment record.
+	 *
+	 * @since 5.5.0
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
+	 */
+	public function edit_media_item( $request ) {
+		require_once ABSPATH . 'wp-admin/includes/image.php';
+
+		$attachment_id = $request['id'];
+
+		// This also confirms the attachment is an image.
+		$image_file = wp_get_original_image_path( $attachment_id );
+		$image_meta = wp_get_attachment_metadata( $attachment_id );
+
+		if (
+			! $image_meta ||
+			! $image_file ||
+			! wp_image_file_matches_image_meta( $request['src'], $image_meta, $attachment_id )
+		) {
+			return new WP_Error(
+				'rest_unknown_attachment',
+				__( 'Unable to get meta information for file.' ),
+				array( 'status' => 404 )
+			);
+		}
+
+		$supported_types = array( 'image/jpeg', 'image/png', 'image/gif' );
+		$mime_type       = get_post_mime_type( $attachment_id );
+		if ( ! in_array( $mime_type, $supported_types, true ) ) {
+			return new WP_Error(
+				'rest_cannot_edit_file_type',
+				__( 'This type of file cannot be edited.' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		// Check if we need to do anything.
+		$rotate = 0;
+		$crop   = false;
+
+		if ( ! empty( $request['rotation'] ) ) {
+			// Rotation direction: clockwise vs. counter clockwise.
+			$rotate = 0 - (int) $request['rotation'];
+		}
+
+		if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) {
+			$crop = true;
+		}
+
+		if ( ! $rotate && ! $crop ) {
+			return new WP_Error(
+				'rest_image_not_edited',
+				__( 'The image was not edited. Edit the image before applying the changes.' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		/*
+		 * If the file doesn't exist, attempt a URL fopen on the src link.
+		 * This can occur with certain file replication plugins.
+		 * Keep the original file path to get a modified name later.
+		 */
+		$image_file_to_edit = $image_file;
+		if ( ! file_exists( $image_file_to_edit ) ) {
+			$image_file_to_edit = _load_image_to_edit_path( $attachment_id );
+		}
+
+		$image_editor = wp_get_image_editor( $image_file_to_edit );
+
+		if ( is_wp_error( $image_editor ) ) {
+			return new WP_Error(
+				'rest_unknown_image_file_type',
+				__( 'Unable to edit this image.' ),
+				array( 'status' => 500 )
+			);
+		}
+
+		if ( 0 !== $rotate ) {
+			$result = $image_editor->rotate( $rotate );
+
+			if ( is_wp_error( $result ) ) {
+				return new WP_Error(
+					'rest_image_rotation_failed',
+					__( 'Unable to rotate this image.' ),
+					array( 'status' => 500 )
+				);
+			}
+		}
+
+		if ( $crop ) {
+			$size = $image_editor->get_size();
+
+			$crop_x = round( ( $size['width'] * floatval( $request['x'] ) ) / 100.0 );
+			$crop_y = round( ( $size['height'] * floatval( $request['y'] ) ) / 100.0 );
+			$width  = round( ( $size['width'] * floatval( $request['width'] ) ) / 100.0 );
+			$height = round( ( $size['height'] * floatval( $request['height'] ) ) / 100.0 );
+
+			$result = $image_editor->crop( $crop_x, $crop_y, $width, $height );
+
+			if ( is_wp_error( $result ) ) {
+				return new WP_Error(
+					'rest_image_crop_failed',
+					__( 'Unable to crop this image.' ),
+					array( 'status' => 500 )
+				);
+			}
+		}
+
+		// Calculate the file name.
+		$image_ext  = pathinfo( $image_file, PATHINFO_EXTENSION );
+		$image_name = wp_basename( $image_file, ".{$image_ext}" );
+
+		// Do not append multiple `-edited` to the file name.
+		// The user may be editing a previously edited image.
+		if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) {
+			// Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number.
+			$image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name );
+		} else {
+			// Append `-edited` before the extension.
+			$image_name .= '-edited';
+		}
+
+		$filename = "{$image_name}.{$image_ext}";
+
+		// Create the uploads sub-directory if needed.
+		$uploads = wp_upload_dir();
+
+		// Make the file name unique in the (new) upload directory.
+		$filename = wp_unique_filename( $uploads['path'], $filename );
+
+		// Save to disk.
+		$saved = $image_editor->save( $uploads['path'] . "/$filename" );
+
+		if ( is_wp_error( $saved ) ) {
+			return $saved;
+		}
+
+		// Create new attachment post.
+		$new_attachment_post = array(
+			'post_mime_type' => $saved['mime-type'],
+			'guid'           => $uploads['url'] . "/$filename",
+			'post_title'     => $image_name,
+			'post_content'   => '',
+		);
+
+		// Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
+		$attachment_post = get_post( $attachment_id );
+
+		if ( $attachment_post ) {
+			$new_attachment_post['post_content'] = $attachment_post->post_content;
+			$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
+			$new_attachment_post['post_title']   = $attachment_post->post_title;
+		}
+
+		$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );
+
+		if ( is_wp_error( $new_attachment_id ) ) {
+			if ( 'db_update_error' === $new_attachment_id->get_error_code() ) {
+				$new_attachment_id->add_data( array( 'status' => 500 ) );
+			} else {
+				$new_attachment_id->add_data( array( 'status' => 400 ) );
+			}
+
+			return $new_attachment_id;
+		}
+
+		// Copy the image alt text from the edited image.
+		$image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
+
+		if ( ! empty( $image_alt ) ) {
+			// update_post_meta() expects slashed.
+			update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
+		}
+
+		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
+			// Set a custom header with the attachment_id.
+			// Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
+			header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id );
+		}
+
+		// Generate image sub-sizes and meta.
+		$new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] );
+
+		// Copy the EXIF metadata from the original attachment if not generated for the edited image.
+		if ( isset( $image_meta['image_meta'] ) && isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) {
+			// Merge but skip empty values.
+			foreach ( (array) $image_meta['image_meta'] as $key => $value ) {
+				if ( empty( $new_image_meta['image_meta'][ $key ] ) && ! empty( $value ) ) {
+					$new_image_meta['image_meta'][ $key ] = $value;
+				}
+			}
+		}
+
+		// Reset orientation. At this point the image is edited and orientation is correct.
+		if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) {
+			$new_image_meta['image_meta']['orientation'] = 1;
+		}
+
+		// The attachment_id may change if the site is exported and imported.
+		$new_image_meta['parent_image'] = array(
+			'attachment_id' => $attachment_id,
+			// Path to the originally uploaded image file relative to the uploads directory.
+			'file'          => _wp_relative_upload_path( $image_file ),
+		);
+
+		/**
+		 * Filters the meta data for the new image created by editing an existing image.
+		 *
+		 * @since 5.5.0
+		 *
+		 * @param array $new_image_meta    Meta data for the new image.
+		 * @param int   $new_attachment_id Attachment post ID for the new image.
+		 * @param int   $attachment_id     Attachment post ID for the edited (parent) image.
+		 */
+		$new_image_meta = apply_filters( 'wp_edited_image_metadata', $new_image_meta, $new_attachment_id, $attachment_id );
+
+		wp_update_attachment_metadata( $new_attachment_id, $new_image_meta );
+
+		$response = $this->prepare_item_for_response( get_post( $new_attachment_id ), $request );
+		$response->set_status( 201 );
+		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $new_attachment_id ) ) );
+
+		return $response;
+	}
+
+	/**
 	 * Prepares a single attachment for create or update.
 	 *
 	 * @since 4.7.0
 	 *
 	 * @param WP_REST_Request $request Request object.
-	 * @return WP_Error|stdClass $prepared_attachment Post object.
+	 * @return stdClass|WP_Error Post object.
 	 */
 	protected function prepare_item_for_database( $request ) {
 		$prepared_attachment = parent::prepare_item_for_database( $request );
 
-		// Attachment caption (post_excerpt internally)
+		// Attachment caption (post_excerpt internally).
 		if ( isset( $request['caption'] ) ) {
 			if ( is_string( $request['caption'] ) ) {
 				$prepared_attachment->post_excerpt = $request['caption'];
@@ -273,7 +652,7 @@
 			}
 		}
 
-		// Attachment description (post_content internally)
+		// Attachment description (post_content internally).
 		if ( isset( $request['description'] ) ) {
 			if ( is_string( $request['description'] ) ) {
 				$prepared_attachment->post_content = $request['description'];
@@ -313,7 +692,11 @@
 
 		if ( in_array( 'caption', $fields, true ) ) {
 			/** This filter is documented in wp-includes/post-template.php */
-			$caption         = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
+			$caption = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
+
+			/** This filter is documented in wp-includes/post-template.php */
+			$caption = apply_filters( 'the_excerpt', $caption );
+
 			$data['caption'] = array(
 				'raw'      => $post->post_excerpt,
 				'rendered' => $caption,
@@ -380,6 +763,11 @@
 			$data['source_url'] = wp_get_attachment_url( $post->ID );
 		}
 
+		if ( in_array( 'missing_image_sizes', $fields, true ) ) {
+			require_once ABSPATH . 'wp-admin/includes/image.php';
+			$data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) );
+		}
+
 		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
 
 		$data = $this->filter_response_by_context( $data, $context );
@@ -417,6 +805,9 @@
 	 * @return array Item schema as an array.
 	 */
 	public function get_item_schema() {
+		if ( $this->schema ) {
+			return $this->add_additional_fields_schema( $this->schema );
+		}
 
 		$schema = parent::get_item_schema();
 
@@ -434,8 +825,8 @@
 			'type'        => 'object',
 			'context'     => array( 'view', 'edit', 'embed' ),
 			'arg_options' => array(
-				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
-				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
+				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
+				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
 			),
 			'properties'  => array(
 				'raw'      => array(
@@ -457,8 +848,8 @@
 			'type'        => 'object',
 			'context'     => array( 'view', 'edit' ),
 			'arg_options' => array(
-				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
-				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database()
+				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
+				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
 			),
 			'properties'  => array(
 				'raw'      => array(
@@ -511,9 +902,19 @@
 			'readonly'    => true,
 		);
 
+		$schema['properties']['missing_image_sizes'] = array(
+			'description' => __( 'List of the missing image sizes of the attachment.' ),
+			'type'        => 'array',
+			'items'       => array( 'type' => 'string' ),
+			'context'     => array( 'edit' ),
+			'readonly'    => true,
+		);
+
 		unset( $schema['properties']['password'] );
 
-		return $schema;
+		$this->schema = $schema;
+
+		return $this->add_additional_fields_schema( $this->schema );
 	}
 
 	/**
@@ -527,21 +928,37 @@
 	 */
 	protected function upload_from_data( $data, $headers ) {
 		if ( empty( $data ) ) {
-			return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_no_data',
+				__( 'No data supplied.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		if ( empty( $headers['content_type'] ) ) {
-			return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_no_content_type',
+				__( 'No Content-Type supplied.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		if ( empty( $headers['content_disposition'] ) ) {
-			return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_no_content_disposition',
+				__( 'No Content-Disposition supplied.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		$filename = self::get_filename_from_disposition( $headers['content_disposition'] );
 
 		if ( empty( $filename ) ) {
-			return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_invalid_disposition',
+				__( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		if ( ! empty( $headers['content_md5'] ) ) {
@@ -550,14 +967,18 @@
 			$actual      = md5( $data );
 
 			if ( $expected !== $actual ) {
-				return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
+				return new WP_Error(
+					'rest_upload_hash_mismatch',
+					__( 'Content hash did not match expected.' ),
+					array( 'status' => 412 )
+				);
 			}
 		}
 
 		// Get the content-type.
 		$type = array_shift( $headers['content_type'] );
 
-		/** Include admin functions to get access to wp_tempnam() and wp_handle_sideload(). */
+		// Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload().
 		require_once ABSPATH . 'wp-admin/includes/file.php';
 
 		// Save the file.
@@ -566,7 +987,11 @@
 		$fp = fopen( $tmpfname, 'w+' );
 
 		if ( ! $fp ) {
-			return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) );
+			return new WP_Error(
+				'rest_upload_file_error',
+				__( 'Could not open file handle.' ),
+				array( 'status' => 500 )
+			);
 		}
 
 		fwrite( $fp, $data );
@@ -594,7 +1019,11 @@
 		if ( isset( $sideloaded['error'] ) ) {
 			@unlink( $tmpfname );
 
-			return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) );
+			return new WP_Error(
+				'rest_upload_sideload_error',
+				$sideloaded['error'],
+				array( 'status' => 500 )
+			);
 		}
 
 		return $sideloaded;
@@ -623,8 +1052,8 @@
 	 *
 	 * @since 4.7.0
 	 *
-	 * @link http://tools.ietf.org/html/rfc2388
-	 * @link http://tools.ietf.org/html/rfc6266
+	 * @link https://tools.ietf.org/html/rfc2388
+	 * @link https://tools.ietf.org/html/rfc6266
 	 *
 	 * @param string[] $disposition_header List of Content-Disposition header values.
 	 * @return string|null Filename if available, or null if not found.
@@ -710,7 +1139,11 @@
 	 */
 	protected function upload_from_file( $files, $headers ) {
 		if ( empty( $files ) ) {
-			return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_no_data',
+				__( 'No data supplied.' ),
+				array( 'status' => 400 )
+			);
 		}
 
 		// Verify hash, if given.
@@ -720,7 +1153,11 @@
 			$actual      = md5_file( $files['file']['tmp_name'] );
 
 			if ( $expected !== $actual ) {
-				return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
+				return new WP_Error(
+					'rest_upload_hash_mismatch',
+					__( 'Content hash did not match expected.' ),
+					array( 'status' => 412 )
+				);
 			}
 		}
 
@@ -739,13 +1176,17 @@
 			return $size_check;
 		}
 
-		/** Include admin function to get access to wp_handle_upload(). */
+		// Include filesystem functions to get access to wp_handle_upload().
 		require_once ABSPATH . 'wp-admin/includes/file.php';
 
 		$file = wp_handle_upload( $files['file'], $overrides );
 
 		if ( isset( $file['error'] ) ) {
-			return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
+			return new WP_Error(
+				'rest_upload_unknown_error',
+				$file['error'],
+				array( 'status' => 500 )
+			);
 		}
 
 		return $file;
@@ -798,23 +1239,87 @@
 		$space_left = get_upload_space_available();
 
 		$file_size = filesize( $file['tmp_name'] );
+
 		if ( $space_left < $file_size ) {
-			/* translators: %s: required disk space in kilobytes */
-			return new WP_Error( 'rest_upload_limited_space', sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_limited_space',
+				/* translators: %s: Required disk space in kilobytes. */
+				sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ),
+				array( 'status' => 400 )
+			);
 		}
 
 		if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
-			/* translators: %s: maximum allowed file size in kilobytes */
-			return new WP_Error( 'rest_upload_file_too_big', sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_file_too_big',
+				/* translators: %s: Maximum allowed file size in kilobytes. */
+				sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ),
+				array( 'status' => 400 )
+			);
 		}
 
-		// Include admin function to get access to upload_is_user_over_quota().
+		// Include multisite admin functions to get access to upload_is_user_over_quota().
 		require_once ABSPATH . 'wp-admin/includes/ms.php';
 
 		if ( upload_is_user_over_quota( false ) ) {
-			return new WP_Error( 'rest_upload_user_quota_exceeded', __( 'You have used your space quota. Please delete files before uploading.' ), array( 'status' => 400 ) );
+			return new WP_Error(
+				'rest_upload_user_quota_exceeded',
+				__( 'You have used your space quota. Please delete files before uploading.' ),
+				array( 'status' => 400 )
+			);
 		}
+
 		return true;
 	}
 
+	/**
+	 * Gets the request args for the edit item route.
+	 *
+	 * @since 5.5.0
+	 *
+	 * @return array
+	 */
+	protected function get_edit_media_item_args() {
+		return array(
+			'rotation' => array(
+				'description'      => __( 'The amount to rotate the image clockwise in degrees.' ),
+				'type'             => 'integer',
+				'minimum'          => 0,
+				'exclusiveMinimum' => true,
+				'maximum'          => 360,
+				'exclusiveMaximum' => true,
+			),
+			'x'        => array(
+				'description' => __( 'As a percentage of the image, the x position to start the crop from.' ),
+				'type'        => 'number',
+				'minimum'     => 0,
+				'maximum'     => 100,
+			),
+			'y'        => array(
+				'description' => __( 'As a percentage of the image, the y position to start the crop from.' ),
+				'type'        => 'number',
+				'minimum'     => 0,
+				'maximum'     => 100,
+			),
+			'width'    => array(
+				'description' => __( 'As a percentage of the image, the width to crop the image to.' ),
+				'type'        => 'number',
+				'minimum'     => 0,
+				'maximum'     => 100,
+			),
+			'height'   => array(
+				'description' => __( 'As a percentage of the image, the height to crop the image to.' ),
+				'type'        => 'number',
+				'minimum'     => 0,
+				'maximum'     => 100,
+			),
+			'src'      => array(
+				'description' => __( 'URL to the edited image file.' ),
+				'type'        => 'string',
+				'format'      => 'uri',
+				'required'    => true,
+			),
+		);
+	}
+
 }