3 * REST API: WP_REST_Revisions_Controller class
11 * Core class used to access revisions via the REST API.
15 * @see WP_REST_Controller
17 class WP_REST_Revisions_Controller extends WP_REST_Controller {
26 private $parent_post_type;
33 * @var WP_REST_Controller
35 private $parent_controller;
38 * The base of the parent controller's route.
52 * @param string $parent_post_type Post type of the parent.
54 public function __construct( $parent_post_type ) {
55 $this->parent_post_type = $parent_post_type;
56 $this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
57 $this->namespace = 'wp/v2';
58 $this->rest_base = 'revisions';
59 $post_type_object = get_post_type_object( $parent_post_type );
60 $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
64 * Registers routes for revisions based on post types supporting revisions.
69 * @see register_rest_route()
71 public function register_routes() {
73 register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base, array(
76 'description' => __( 'The ID for the parent of the object.' ),
81 'methods' => WP_REST_Server::READABLE,
82 'callback' => array( $this, 'get_items' ),
83 'permission_callback' => array( $this, 'get_items_permissions_check' ),
84 'args' => $this->get_collection_params(),
86 'schema' => array( $this, 'get_public_item_schema' ),
89 register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
92 'description' => __( 'The ID for the parent of the object.' ),
96 'description' => __( 'Unique identifier for the object.' ),
101 'methods' => WP_REST_Server::READABLE,
102 'callback' => array( $this, 'get_item' ),
103 'permission_callback' => array( $this, 'get_item_permissions_check' ),
105 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
109 'methods' => WP_REST_Server::DELETABLE,
110 'callback' => array( $this, 'delete_item' ),
111 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
116 'description' => __( 'Required to be true, as revisions do not support trashing.' ),
120 'schema' => array( $this, 'get_public_item_schema' ),
126 * Get the parent post, if the ID is valid.
130 * @param int $id Supplied ID.
131 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
133 protected function get_parent( $parent ) {
134 $error = new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent ID.' ), array( 'status' => 404 ) );
135 if ( (int) $parent <= 0 ) {
139 $parent = get_post( (int) $parent );
140 if ( empty( $parent ) || empty( $parent->ID ) || $this->parent_post_type !== $parent->post_type ) {
148 * Checks if a given request has access to get revisions.
153 * @param WP_REST_Request $request Full data about the request.
154 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
156 public function get_items_permissions_check( $request ) {
157 $parent = $this->get_parent( $request['parent'] );
158 if ( is_wp_error( $parent ) ) {
162 $parent_post_type_obj = get_post_type_object( $parent->post_type );
163 if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
164 return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
171 * Get the revision, if the ID is valid.
175 * @param int $id Supplied ID.
176 * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
178 protected function get_revision( $id ) {
179 $error = new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision ID.' ), array( 'status' => 404 ) );
180 if ( (int) $id <= 0 ) {
184 $revision = get_post( (int) $id );
185 if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) {
193 * Gets a collection of revisions.
198 * @param WP_REST_Request $request Full data about the request.
199 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
201 public function get_items( $request ) {
202 $parent = $this->get_parent( $request['parent'] );
203 if ( is_wp_error( $parent ) ) {
207 $revisions = wp_get_post_revisions( $request['parent'] );
210 foreach ( $revisions as $revision ) {
211 $data = $this->prepare_item_for_response( $revision, $request );
212 $response[] = $this->prepare_response_for_collection( $data );
214 return rest_ensure_response( $response );
218 * Checks if a given request has access to get a specific revision.
223 * @param WP_REST_Request $request Full data about the request.
224 * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
226 public function get_item_permissions_check( $request ) {
227 return $this->get_items_permissions_check( $request );
231 * Retrieves one revision from the collection.
236 * @param WP_REST_Request $request Full data about the request.
237 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
239 public function get_item( $request ) {
240 $parent = $this->get_parent( $request['parent'] );
241 if ( is_wp_error( $parent ) ) {
245 $revision = $this->get_revision( $request['id'] );
246 if ( is_wp_error( $revision ) ) {
250 $response = $this->prepare_item_for_response( $revision, $request );
251 return rest_ensure_response( $response );
255 * Checks if a given request has access to delete a revision.
260 * @param WP_REST_Request $request Full details about the request.
261 * @return bool|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
263 public function delete_item_permissions_check( $request ) {
264 $parent = $this->get_parent( $request['parent'] );
265 if ( is_wp_error( $parent ) ) {
269 $revision = $this->get_revision( $request['id'] );
270 if ( is_wp_error( $revision ) ) {
274 $response = $this->get_items_permissions_check( $request );
275 if ( ! $response || is_wp_error( $response ) ) {
279 $post_type = get_post_type_object( 'revision' );
280 return current_user_can( $post_type->cap->delete_post, $revision->ID );
284 * Deletes a single revision.
289 * @param WP_REST_Request $request Full details about the request.
290 * @return true|WP_Error True on success, or WP_Error object on failure.
292 public function delete_item( $request ) {
293 $revision = $this->get_revision( $request['id'] );
294 if ( is_wp_error( $revision ) ) {
298 $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
300 // We don't support trashing for revisions.
302 return new WP_Error( 'rest_trash_not_supported', __( 'Revisions do not support trashing. Set force=true to delete.' ), array( 'status' => 501 ) );
305 $previous = $this->prepare_item_for_response( $revision, $request );
307 $result = wp_delete_post( $request['id'], true );
310 * Fires after a revision is deleted via the REST API.
314 * @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
315 * or false (failure). If the revision was moved to to the trash, $result represents
316 * its new state; if it was deleted, $result represents its state before deletion.
317 * @param WP_REST_Request $request The request sent to the API.
319 do_action( 'rest_delete_revision', $result, $request );
322 return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
325 $response = new WP_REST_Response();
326 $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
331 * Prepares the revision for the REST response.
336 * @param WP_Post $post Post revision object.
337 * @param WP_REST_Request $request Request object.
338 * @return WP_REST_Response Response object.
340 public function prepare_item_for_response( $post, $request ) {
342 $schema = $this->get_item_schema();
346 if ( ! empty( $schema['properties']['author'] ) ) {
347 $data['author'] = $post->post_author;
350 if ( ! empty( $schema['properties']['date'] ) ) {
351 $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
354 if ( ! empty( $schema['properties']['date_gmt'] ) ) {
355 $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
358 if ( ! empty( $schema['properties']['id'] ) ) {
359 $data['id'] = $post->ID;
362 if ( ! empty( $schema['properties']['modified'] ) ) {
363 $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
366 if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
367 $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
370 if ( ! empty( $schema['properties']['parent'] ) ) {
371 $data['parent'] = (int) $post->post_parent;
374 if ( ! empty( $schema['properties']['slug'] ) ) {
375 $data['slug'] = $post->post_name;
378 if ( ! empty( $schema['properties']['guid'] ) ) {
379 $data['guid'] = array(
380 /** This filter is documented in wp-includes/post-template.php */
381 'rendered' => apply_filters( 'get_the_guid', $post->guid ),
382 'raw' => $post->guid,
386 if ( ! empty( $schema['properties']['title'] ) ) {
387 $data['title'] = array(
388 'raw' => $post->post_title,
389 'rendered' => get_the_title( $post->ID ),
393 if ( ! empty( $schema['properties']['content'] ) ) {
395 $data['content'] = array(
396 'raw' => $post->post_content,
397 /** This filter is documented in wp-includes/post-template.php */
398 'rendered' => apply_filters( 'the_content', $post->post_content ),
402 if ( ! empty( $schema['properties']['excerpt'] ) ) {
403 $data['excerpt'] = array(
404 'raw' => $post->post_excerpt,
405 'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
409 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
410 $data = $this->add_additional_fields_to_object( $data, $request );
411 $data = $this->filter_response_by_context( $data, $context );
412 $response = rest_ensure_response( $data );
414 if ( ! empty( $data['parent'] ) ) {
415 $response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
419 * Filters a revision returned from the API.
421 * Allows modification of the revision right before it is returned.
425 * @param WP_REST_Response $response The response object.
426 * @param WP_Post $post The original revision object.
427 * @param WP_REST_Request $request Request used to generate the response.
429 return apply_filters( 'rest_prepare_revision', $response, $post, $request );
433 * Checks the post_date_gmt or modified_gmt and prepare any post or
434 * modified date for single post output.
439 * @param string $date_gmt GMT publication time.
440 * @param string|null $date Optional. Local publication time. Default null.
441 * @return string|null ISO8601/RFC3339 formatted datetime, otherwise null.
443 protected function prepare_date_response( $date_gmt, $date = null ) {
444 if ( '0000-00-00 00:00:00' === $date_gmt ) {
448 if ( isset( $date ) ) {
449 return mysql_to_rfc3339( $date );
452 return mysql_to_rfc3339( $date_gmt );
456 * Retrieves the revision's schema, conforming to JSON Schema.
461 * @return array Item schema data.
463 public function get_item_schema() {
465 '$schema' => 'http://json-schema.org/schema#',
466 'title' => "{$this->parent_post_type}-revision",
468 // Base properties for every Revision.
469 'properties' => array(
471 'description' => __( 'The ID for the author of the object.' ),
473 'context' => array( 'view', 'edit', 'embed' ),
476 'description' => __( "The date the object was published, in the site's timezone." ),
478 'format' => 'date-time',
479 'context' => array( 'view', 'edit', 'embed' ),
482 'description' => __( 'The date the object was published, as GMT.' ),
484 'format' => 'date-time',
485 'context' => array( 'view', 'edit' ),
488 'description' => __( 'GUID for the object, as it exists in the database.' ),
490 'context' => array( 'view', 'edit' ),
493 'description' => __( 'Unique identifier for the object.' ),
495 'context' => array( 'view', 'edit', 'embed' ),
498 'description' => __( "The date the object was last modified, in the site's timezone." ),
500 'format' => 'date-time',
501 'context' => array( 'view', 'edit' ),
503 'modified_gmt' => array(
504 'description' => __( 'The date the object was last modified, as GMT.' ),
506 'format' => 'date-time',
507 'context' => array( 'view', 'edit' ),
510 'description' => __( 'The ID for the parent of the object.' ),
512 'context' => array( 'view', 'edit', 'embed' ),
515 'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
517 'context' => array( 'view', 'edit', 'embed' ),
522 $parent_schema = $this->parent_controller->get_item_schema();
524 if ( ! empty( $parent_schema['properties']['title'] ) ) {
525 $schema['properties']['title'] = $parent_schema['properties']['title'];
528 if ( ! empty( $parent_schema['properties']['content'] ) ) {
529 $schema['properties']['content'] = $parent_schema['properties']['content'];
532 if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
533 $schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
536 if ( ! empty( $parent_schema['properties']['guid'] ) ) {
537 $schema['properties']['guid'] = $parent_schema['properties']['guid'];
540 return $this->add_additional_fields_schema( $schema );
544 * Retrieves the query params for collections.
549 * @return array Collection parameters.
551 public function get_collection_params() {
553 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
558 * Checks the post excerpt and prepare it for single post output.
563 * @param string $excerpt The post excerpt.
564 * @param WP_Post $post Post revision object.
565 * @return string Prepared excerpt or empty string.
567 protected function prepare_excerpt_response( $excerpt, $post ) {
569 /** This filter is documented in wp-includes/post-template.php */
570 $excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
572 if ( empty( $excerpt ) ) {