wp/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php
changeset 9 177826044cd9
child 16 a86126ab1dd4
equal deleted inserted replaced
8:c7c34916027a 9:177826044cd9
       
     1 <?php
       
     2 /**
       
     3  * REST API: WP_REST_Autosaves_Controller class.
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage REST_API
       
     7  * @since 5.0.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Core class used to access autosaves via the REST API.
       
    12  *
       
    13  * @since 5.0.0
       
    14  *
       
    15  * @see WP_REST_Controller
       
    16  */
       
    17 class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller {
       
    18 
       
    19 	/**
       
    20 	 * Parent post type.
       
    21 	 *
       
    22 	 * @since 5.0.0
       
    23 	 * @var string
       
    24 	 */
       
    25 	private $parent_post_type;
       
    26 
       
    27 	/**
       
    28 	 * Parent post controller.
       
    29 	 *
       
    30 	 * @since 5.0.0
       
    31 	 * @var WP_REST_Controller
       
    32 	 */
       
    33 	private $parent_controller;
       
    34 
       
    35 	/**
       
    36 	 * Revision controller.
       
    37 	 *
       
    38 	 * @since 5.0.0
       
    39 	 * @var WP_REST_Controller
       
    40 	 */
       
    41 	private $revisions_controller;
       
    42 
       
    43 	/**
       
    44 	 * The base of the parent controller's route.
       
    45 	 *
       
    46 	 * @since 5.0.0
       
    47 	 * @var string
       
    48 	 */
       
    49 	private $parent_base;
       
    50 
       
    51 	/**
       
    52 	 * Constructor.
       
    53 	 *
       
    54 	 * @since 5.0.0
       
    55 	 *
       
    56 	 * @param string $parent_post_type Post type of the parent.
       
    57 	 */
       
    58 	public function __construct( $parent_post_type ) {
       
    59 		$this->parent_post_type = $parent_post_type;
       
    60 		$post_type_object       = get_post_type_object( $parent_post_type );
       
    61 
       
    62 		// Ensure that post type-specific controller logic is available.
       
    63 		$parent_controller_class = ! empty( $post_type_object->rest_controller_class ) ? $post_type_object->rest_controller_class : 'WP_REST_Posts_Controller';
       
    64 
       
    65 		$this->parent_controller    = new $parent_controller_class( $post_type_object->name );
       
    66 		$this->revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
       
    67 		$this->rest_namespace       = 'wp/v2';
       
    68 		$this->rest_base            = 'autosaves';
       
    69 		$this->parent_base          = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
       
    70 	}
       
    71 
       
    72 	/**
       
    73 	 * Registers routes for autosaves.
       
    74 	 *
       
    75 	 * @since 5.0.0
       
    76 	 *
       
    77 	 * @see register_rest_route()
       
    78 	 */
       
    79 	public function register_routes() {
       
    80 		register_rest_route(
       
    81 			$this->rest_namespace,
       
    82 			'/' . $this->parent_base . '/(?P<id>[\d]+)/' . $this->rest_base,
       
    83 			array(
       
    84 				'args'   => array(
       
    85 					'parent' => array(
       
    86 						'description' => __( 'The ID for the parent of the object.' ),
       
    87 						'type'        => 'integer',
       
    88 					),
       
    89 				),
       
    90 				array(
       
    91 					'methods'             => WP_REST_Server::READABLE,
       
    92 					'callback'            => array( $this, 'get_items' ),
       
    93 					'permission_callback' => array( $this, 'get_items_permissions_check' ),
       
    94 					'args'                => $this->get_collection_params(),
       
    95 				),
       
    96 				array(
       
    97 					'methods'             => WP_REST_Server::CREATABLE,
       
    98 					'callback'            => array( $this, 'create_item' ),
       
    99 					'permission_callback' => array( $this, 'create_item_permissions_check' ),
       
   100 					'args'                => $this->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
       
   101 				),
       
   102 				'schema' => array( $this, 'get_public_item_schema' ),
       
   103 			)
       
   104 		);
       
   105 
       
   106 		register_rest_route(
       
   107 			$this->rest_namespace,
       
   108 			'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
       
   109 			array(
       
   110 				'args'   => array(
       
   111 					'parent' => array(
       
   112 						'description' => __( 'The ID for the parent of the object.' ),
       
   113 						'type'        => 'integer',
       
   114 					),
       
   115 					'id'     => array(
       
   116 						'description' => __( 'The ID for the object.' ),
       
   117 						'type'        => 'integer',
       
   118 					),
       
   119 				),
       
   120 				array(
       
   121 					'methods'             => WP_REST_Server::READABLE,
       
   122 					'callback'            => array( $this, 'get_item' ),
       
   123 					'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ),
       
   124 					'args'                => array(
       
   125 						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
       
   126 					),
       
   127 				),
       
   128 				'schema' => array( $this, 'get_public_item_schema' ),
       
   129 			)
       
   130 		);
       
   131 
       
   132 	}
       
   133 
       
   134 	/**
       
   135 	 * Get the parent post.
       
   136 	 *
       
   137 	 * @since 5.0.0
       
   138 	 *
       
   139 	 * @param int $parent_id Supplied ID.
       
   140 	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
       
   141 	 */
       
   142 	protected function get_parent( $parent_id ) {
       
   143 		return $this->revisions_controller->get_parent( $parent_id );
       
   144 	}
       
   145 
       
   146 	/**
       
   147 	 * Checks if a given request has access to get autosaves.
       
   148 	 *
       
   149 	 * @since 5.0.0
       
   150 	 *
       
   151 	 * @param WP_REST_Request $request Full data about the request.
       
   152 	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
       
   153 	 */
       
   154 	public function get_items_permissions_check( $request ) {
       
   155 		$parent = $this->get_parent( $request['id'] );
       
   156 		if ( is_wp_error( $parent ) ) {
       
   157 			return $parent;
       
   158 		}
       
   159 
       
   160 		$parent_post_type_obj = get_post_type_object( $parent->post_type );
       
   161 		if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
       
   162 			return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to view autosaves of this post.' ), array( 'status' => rest_authorization_required_code() ) );
       
   163 		}
       
   164 
       
   165 		return true;
       
   166 	}
       
   167 
       
   168 	/**
       
   169 	 * Checks if a given request has access to create an autosave revision.
       
   170 	 *
       
   171 	 * Autosave revisions inherit permissions from the parent post,
       
   172 	 * check if the current user has permission to edit the post.
       
   173 	 *
       
   174 	 * @since 5.0.0
       
   175 	 *
       
   176 	 * @param WP_REST_Request $request Full details about the request.
       
   177 	 * @return true|WP_Error True if the request has access to create the item, WP_Error object otherwise.
       
   178 	 */
       
   179 	public function create_item_permissions_check( $request ) {
       
   180 		$id = $request->get_param( 'id' );
       
   181 		if ( empty( $id ) ) {
       
   182 			return new WP_Error( 'rest_post_invalid_id', __( 'Invalid item ID.' ), array( 'status' => 404 ) );
       
   183 		}
       
   184 
       
   185 		return $this->parent_controller->update_item_permissions_check( $request );
       
   186 	}
       
   187 
       
   188 	/**
       
   189 	 * Creates, updates or deletes an autosave revision.
       
   190 	 *
       
   191 	 * @since 5.0.0
       
   192 	 *
       
   193 	 * @param WP_REST_Request $request Full details about the request.
       
   194 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   195 	 */
       
   196 	public function create_item( $request ) {
       
   197 
       
   198 		if ( ! defined( 'DOING_AUTOSAVE' ) ) {
       
   199 			define( 'DOING_AUTOSAVE', true );
       
   200 		}
       
   201 
       
   202 		$post = get_post( $request['id'] );
       
   203 
       
   204 		if ( is_wp_error( $post ) ) {
       
   205 			return $post;
       
   206 		}
       
   207 
       
   208 		$prepared_post     = $this->parent_controller->prepare_item_for_database( $request );
       
   209 		$prepared_post->ID = $post->ID;
       
   210 		$user_id           = get_current_user_id();
       
   211 
       
   212 		if ( ( 'draft' === $post->post_status || 'auto-draft' === $post->post_status ) && $post->post_author == $user_id ) {
       
   213 			// Draft posts for the same author: autosaving updates the post and does not create a revision.
       
   214 			// Convert the post object to an array and add slashes, wp_update_post expects escaped array.
       
   215 			$autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
       
   216 		} else {
       
   217 			// Non-draft posts: create or update the post autosave.
       
   218 			$autosave_id = $this->create_post_autosave( (array) $prepared_post );
       
   219 		}
       
   220 
       
   221 		if ( is_wp_error( $autosave_id ) ) {
       
   222 			return $autosave_id;
       
   223 		}
       
   224 
       
   225 		$autosave = get_post( $autosave_id );
       
   226 		$request->set_param( 'context', 'edit' );
       
   227 
       
   228 		$response = $this->prepare_item_for_response( $autosave, $request );
       
   229 		$response = rest_ensure_response( $response );
       
   230 
       
   231 		return $response;
       
   232 	}
       
   233 
       
   234 	/**
       
   235 	 * Get the autosave, if the ID is valid.
       
   236 	 *
       
   237 	 * @since 5.0.0
       
   238 	 *
       
   239 	 * @param WP_REST_Request $request Full data about the request.
       
   240 	 * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
       
   241 	 */
       
   242 	public function get_item( $request ) {
       
   243 		$parent_id = (int) $request->get_param( 'parent' );
       
   244 
       
   245 		if ( $parent_id <= 0 ) {
       
   246 			return new WP_Error( 'rest_post_invalid_id', __( 'Invalid parent post ID.' ), array( 'status' => 404 ) );
       
   247 		}
       
   248 
       
   249 		$autosave = wp_get_post_autosave( $parent_id );
       
   250 
       
   251 		if ( ! $autosave ) {
       
   252 			return new WP_Error( 'rest_post_no_autosave', __( 'There is no autosave revision for this post.' ), array( 'status' => 404 ) );
       
   253 		}
       
   254 
       
   255 		$response = $this->prepare_item_for_response( $autosave, $request );
       
   256 		return $response;
       
   257 	}
       
   258 
       
   259 	/**
       
   260 	 * Gets a collection of autosaves using wp_get_post_autosave.
       
   261 	 *
       
   262 	 * Contains the user's autosave, for empty if it doesn't exist.
       
   263 	 *
       
   264 	 * @since 5.0.0
       
   265 	 *
       
   266 	 * @param WP_REST_Request $request Full data about the request.
       
   267 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   268 	 */
       
   269 	public function get_items( $request ) {
       
   270 		$parent = $this->get_parent( $request['id'] );
       
   271 		if ( is_wp_error( $parent ) ) {
       
   272 			return $parent;
       
   273 		}
       
   274 
       
   275 		$response  = array();
       
   276 		$parent_id = $parent->ID;
       
   277 		$revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) );
       
   278 
       
   279 		foreach ( $revisions as $revision ) {
       
   280 			if ( false !== strpos( $revision->post_name, "{$parent_id}-autosave" ) ) {
       
   281 				$data       = $this->prepare_item_for_response( $revision, $request );
       
   282 				$response[] = $this->prepare_response_for_collection( $data );
       
   283 			}
       
   284 		}
       
   285 
       
   286 		return rest_ensure_response( $response );
       
   287 	}
       
   288 
       
   289 
       
   290 	/**
       
   291 	 * Retrieves the autosave's schema, conforming to JSON Schema.
       
   292 	 *
       
   293 	 * @since 5.0.0
       
   294 	 *
       
   295 	 * @return array Item schema data.
       
   296 	 */
       
   297 	public function get_item_schema() {
       
   298 		$schema = $this->revisions_controller->get_item_schema();
       
   299 
       
   300 		$schema['properties']['preview_link'] = array(
       
   301 			'description' => __( 'Preview link for the post.' ),
       
   302 			'type'        => 'string',
       
   303 			'format'      => 'uri',
       
   304 			'context'     => array( 'edit' ),
       
   305 			'readonly'    => true,
       
   306 		);
       
   307 
       
   308 		return $schema;
       
   309 	}
       
   310 
       
   311 	/**
       
   312 	 * Creates autosave for the specified post.
       
   313 	 *
       
   314 	 * From wp-admin/post.php.
       
   315 	 *
       
   316 	 * @since 5.0.0
       
   317 	 *
       
   318 	 * @param mixed $post_data Associative array containing the post data.
       
   319 	 * @return mixed The autosave revision ID or WP_Error.
       
   320 	 */
       
   321 	public function create_post_autosave( $post_data ) {
       
   322 
       
   323 		$post_id = (int) $post_data['ID'];
       
   324 		$post    = get_post( $post_id );
       
   325 
       
   326 		if ( is_wp_error( $post ) ) {
       
   327 			return $post;
       
   328 		}
       
   329 
       
   330 		$user_id = get_current_user_id();
       
   331 
       
   332 		// Store one autosave per author. If there is already an autosave, overwrite it.
       
   333 		$old_autosave = wp_get_post_autosave( $post_id, $user_id );
       
   334 
       
   335 		if ( $old_autosave ) {
       
   336 			$new_autosave                = _wp_post_revision_data( $post_data, true );
       
   337 			$new_autosave['ID']          = $old_autosave->ID;
       
   338 			$new_autosave['post_author'] = $user_id;
       
   339 
       
   340 			// If the new autosave has the same content as the post, delete the autosave.
       
   341 			$autosave_is_different = false;
       
   342 
       
   343 			foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) {
       
   344 				if ( normalize_whitespace( $new_autosave[ $field ] ) != normalize_whitespace( $post->$field ) ) {
       
   345 					$autosave_is_different = true;
       
   346 					break;
       
   347 				}
       
   348 			}
       
   349 
       
   350 			if ( ! $autosave_is_different ) {
       
   351 				wp_delete_post_revision( $old_autosave->ID );
       
   352 				return new WP_Error( 'rest_autosave_no_changes', __( 'There is nothing to save. The autosave and the post content are the same.' ), array( 'status' => 400 ) );
       
   353 			}
       
   354 
       
   355 			/**
       
   356 			 * This filter is documented in wp-admin/post.php.
       
   357 			 */
       
   358 			do_action( 'wp_creating_autosave', $new_autosave );
       
   359 
       
   360 			// wp_update_post expects escaped array.
       
   361 			return wp_update_post( wp_slash( $new_autosave ) );
       
   362 		}
       
   363 
       
   364 		// Create the new autosave as a special post revision.
       
   365 		return _wp_put_post_revision( $post_data, true );
       
   366 	}
       
   367 
       
   368 	/**
       
   369 	 * Prepares the revision for the REST response.
       
   370 	 *
       
   371 	 * @since 5.0.0
       
   372 	 *
       
   373 	 * @param WP_Post         $post    Post revision object.
       
   374 	 * @param WP_REST_Request $request Request object.
       
   375 	 *
       
   376 	 * @return WP_REST_Response Response object.
       
   377 	 */
       
   378 	public function prepare_item_for_response( $post, $request ) {
       
   379 
       
   380 		$response = $this->revisions_controller->prepare_item_for_response( $post, $request );
       
   381 
       
   382 		$fields = $this->get_fields_for_response( $request );
       
   383 
       
   384 		if ( in_array( 'preview_link', $fields, true ) ) {
       
   385 			$parent_id          = wp_is_post_autosave( $post );
       
   386 			$preview_post_id    = false === $parent_id ? $post->ID : $parent_id;
       
   387 			$preview_query_args = array();
       
   388 
       
   389 			if ( false !== $parent_id ) {
       
   390 				$preview_query_args['preview_id']    = $parent_id;
       
   391 				$preview_query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $parent_id );
       
   392 			}
       
   393 
       
   394 			$response->data['preview_link'] = get_preview_post_link( $preview_post_id, $preview_query_args );
       
   395 		}
       
   396 
       
   397 		$context        = ! empty( $request['context'] ) ? $request['context'] : 'view';
       
   398 		$response->data = $this->add_additional_fields_to_object( $response->data, $request );
       
   399 		$response->data = $this->filter_response_by_context( $response->data, $context );
       
   400 
       
   401 		/**
       
   402 		 * Filters a revision returned from the API.
       
   403 		 *
       
   404 		 * Allows modification of the revision right before it is returned.
       
   405 		 *
       
   406 		 * @since 5.0.0
       
   407 		 *
       
   408 		 * @param WP_REST_Response $response The response object.
       
   409 		 * @param WP_Post          $post     The original revision object.
       
   410 		 * @param WP_REST_Request  $request  Request used to generate the response.
       
   411 		 */
       
   412 		return apply_filters( 'rest_prepare_autosave', $response, $post, $request );
       
   413 	}
       
   414 
       
   415 	/**
       
   416 	 * Retrieves the query params for the autosaves collection.
       
   417 	 *
       
   418 	 * @since 5.0.0
       
   419 	 *
       
   420 	 * @return array Collection parameters.
       
   421 	 */
       
   422 	public function get_collection_params() {
       
   423 		return array(
       
   424 			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
       
   425 		);
       
   426 	}
       
   427 }