WordPress 4.7.2
[autoinstalls/wordpress.git] / wp-includes / rest-api / endpoints / class-wp-rest-revisions-controller.php
1 <?php
2 /**
3  * REST API: WP_REST_Revisions_Controller class
4  *
5  * @package WordPress
6  * @subpackage REST_API
7  * @since 4.7.0
8  */
9
10 /**
11  * Core class used to access revisions via the REST API.
12  *
13  * @since 4.7.0
14  *0
15  * @see WP_REST_Controller
16  */
17 class WP_REST_Revisions_Controller extends WP_REST_Controller {
18
19         /**
20          * Parent post type.
21          *
22          * @since 4.7.0
23          * @access private
24          * @var string
25          */
26         private $parent_post_type;
27
28         /**
29          * Parent controller.
30          *
31          * @since 4.7.0
32          * @access private
33          * @var WP_REST_Controller
34          */
35         private $parent_controller;
36
37         /**
38          * The base of the parent controller's route.
39          *
40          * @since 4.7.0
41          * @access private
42          * @var string
43          */
44         private $parent_base;
45
46         /**
47          * Constructor.
48          *
49          * @since 4.7.0
50          * @access public
51          *
52          * @param string $parent_post_type Post type of the parent.
53          */
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;
61         }
62
63         /**
64          * Registers routes for revisions based on post types supporting revisions.
65          *
66          * @since 4.7.0
67          * @access public
68          *
69          * @see register_rest_route()
70          */
71         public function register_routes() {
72
73                 register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base, array(
74                         'args' => array(
75                                 'parent' => array(
76                                         'description' => __( 'The ID for the parent of the object.' ),
77                                         'type'        => 'integer',
78                                 ),
79                         ),
80                         array(
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(),
85                         ),
86                         'schema' => array( $this, 'get_public_item_schema' ),
87                 ) );
88
89                 register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
90                         'args' => array(
91                                 'parent' => array(
92                                         'description' => __( 'The ID for the parent of the object.' ),
93                                         'type'        => 'integer',
94                                 ),
95                                 'id' => array(
96                                         'description' => __( 'Unique identifier for the object.' ),
97                                         'type'        => 'integer',
98                                 ),
99                         ),
100                         array(
101                                 'methods'             => WP_REST_Server::READABLE,
102                                 'callback'            => array( $this, 'get_item' ),
103                                 'permission_callback' => array( $this, 'get_item_permissions_check' ),
104                                 'args'                => array(
105                                         'context' => $this->get_context_param( array( 'default' => 'view' ) ),
106                                 ),
107                         ),
108                         array(
109                                 'methods'             => WP_REST_Server::DELETABLE,
110                                 'callback'            => array( $this, 'delete_item' ),
111                                 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
112                                 'args'                => array(
113                                         'force' => array(
114                                                 'type'        => 'boolean',
115                                                 'default'     => false,
116                                                 'description' => __( 'Required to be true, as revisions do not support trashing.' ),
117                                         ),
118                                 ),
119                         ),
120                         'schema' => array( $this, 'get_public_item_schema' ),
121                 ));
122
123         }
124
125         /**
126          * Get the parent post, if the ID is valid.
127          *
128          * @since 4.7.2
129          *
130          * @param int $id Supplied ID.
131          * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
132          */
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 ) {
136                         return $error;
137                 }
138
139                 $parent = get_post( (int) $parent );
140                 if ( empty( $parent ) || empty( $parent->ID ) || $this->parent_post_type !== $parent->post_type ) {
141                         return $error;
142                 }
143
144                 return $parent;
145         }
146
147         /**
148          * Checks if a given request has access to get revisions.
149          *
150          * @since 4.7.0
151          * @access public
152          *
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.
155          */
156         public function get_items_permissions_check( $request ) {
157                 $parent = $this->get_parent( $request['parent'] );
158                 if ( is_wp_error( $parent ) ) {
159                         return $parent;
160                 }
161
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() ) );
165                 }
166
167                 return true;
168         }
169
170         /**
171          * Get the revision, if the ID is valid.
172          *
173          * @since 4.7.2
174          *
175          * @param int $id Supplied ID.
176          * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
177          */
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 ) {
181                         return $error;
182                 }
183
184                 $revision = get_post( (int) $id );
185                 if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) {
186                         return $error;
187                 }
188
189                 return $revision;
190         }
191
192         /**
193          * Gets a collection of revisions.
194          *
195          * @since 4.7.0
196          * @access public
197          *
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.
200          */
201         public function get_items( $request ) {
202                 $parent = $this->get_parent( $request['parent'] );
203                 if ( is_wp_error( $parent ) ) {
204                         return $parent;
205                 }
206
207                 $revisions = wp_get_post_revisions( $request['parent'] );
208
209                 $response = array();
210                 foreach ( $revisions as $revision ) {
211                         $data = $this->prepare_item_for_response( $revision, $request );
212                         $response[] = $this->prepare_response_for_collection( $data );
213                 }
214                 return rest_ensure_response( $response );
215         }
216
217         /**
218          * Checks if a given request has access to get a specific revision.
219          *
220          * @since 4.7.0
221          * @access public
222          *
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.
225          */
226         public function get_item_permissions_check( $request ) {
227                 return $this->get_items_permissions_check( $request );
228         }
229
230         /**
231          * Retrieves one revision from the collection.
232          *
233          * @since 4.7.0
234          * @access public
235          *
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.
238          */
239         public function get_item( $request ) {
240                 $parent = $this->get_parent( $request['parent'] );
241                 if ( is_wp_error( $parent ) ) {
242                         return $parent;
243                 }
244
245                 $revision = $this->get_revision( $request['id'] );
246                 if ( is_wp_error( $revision ) ) {
247                         return $revision;
248                 }
249
250                 $response = $this->prepare_item_for_response( $revision, $request );
251                 return rest_ensure_response( $response );
252         }
253
254         /**
255          * Checks if a given request has access to delete a revision.
256          *
257          * @since 4.7.0
258          * @access public
259          *
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.
262          */
263         public function delete_item_permissions_check( $request ) {
264                 $parent = $this->get_parent( $request['parent'] );
265                 if ( is_wp_error( $parent ) ) {
266                         return $parent;
267                 }
268
269                 $revision = $this->get_revision( $request['id'] );
270                 if ( is_wp_error( $revision ) ) {
271                         return $revision;
272                 }
273
274                 $response = $this->get_items_permissions_check( $request );
275                 if ( ! $response || is_wp_error( $response ) ) {
276                         return $response;
277                 }
278
279                 $post_type = get_post_type_object( 'revision' );
280                 return current_user_can( $post_type->cap->delete_post, $revision->ID );
281         }
282
283         /**
284          * Deletes a single revision.
285          *
286          * @since 4.7.0
287          * @access public
288          *
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.
291          */
292         public function delete_item( $request ) {
293                 $revision = $this->get_revision( $request['id'] );
294                 if ( is_wp_error( $revision ) ) {
295                         return $revision;
296                 }
297
298                 $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
299
300                 // We don't support trashing for revisions.
301                 if ( ! $force ) {
302                         return new WP_Error( 'rest_trash_not_supported', __( 'Revisions do not support trashing. Set force=true to delete.' ), array( 'status' => 501 ) );
303                 }
304
305                 $previous = $this->prepare_item_for_response( $revision, $request );
306
307                 $result = wp_delete_post( $request['id'], true );
308
309                 /**
310                  * Fires after a revision is deleted via the REST API.
311                  *
312                  * @since 4.7.0
313                  *
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.
318                  */
319                 do_action( 'rest_delete_revision', $result, $request );
320
321                 if ( ! $result ) {
322                         return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
323                 }
324
325                 $response = new WP_REST_Response();
326                 $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
327                 return $response;
328         }
329
330         /**
331          * Prepares the revision for the REST response.
332          *
333          * @since 4.7.0
334          * @access public
335          *
336          * @param WP_Post         $post    Post revision object.
337          * @param WP_REST_Request $request Request object.
338          * @return WP_REST_Response Response object.
339          */
340         public function prepare_item_for_response( $post, $request ) {
341
342                 $schema = $this->get_item_schema();
343
344                 $data = array();
345
346                 if ( ! empty( $schema['properties']['author'] ) ) {
347                         $data['author'] = $post->post_author;
348                 }
349
350                 if ( ! empty( $schema['properties']['date'] ) ) {
351                         $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
352                 }
353
354                 if ( ! empty( $schema['properties']['date_gmt'] ) ) {
355                         $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
356                 }
357
358                 if ( ! empty( $schema['properties']['id'] ) ) {
359                         $data['id'] = $post->ID;
360                 }
361
362                 if ( ! empty( $schema['properties']['modified'] ) ) {
363                         $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
364                 }
365
366                 if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
367                         $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
368                 }
369
370                 if ( ! empty( $schema['properties']['parent'] ) ) {
371                         $data['parent'] = (int) $post->post_parent;
372                 }
373
374                 if ( ! empty( $schema['properties']['slug'] ) ) {
375                         $data['slug'] = $post->post_name;
376                 }
377
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,
383                         );
384                 }
385
386                 if ( ! empty( $schema['properties']['title'] ) ) {
387                         $data['title'] = array(
388                                 'raw'      => $post->post_title,
389                                 'rendered' => get_the_title( $post->ID ),
390                         );
391                 }
392
393                 if ( ! empty( $schema['properties']['content'] ) ) {
394
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 ),
399                         );
400                 }
401
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 ),
406                         );
407                 }
408
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 );
413
414                 if ( ! empty( $data['parent'] ) ) {
415                         $response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
416                 }
417
418                 /**
419                  * Filters a revision returned from the API.
420                  *
421                  * Allows modification of the revision right before it is returned.
422                  *
423                  * @since 4.7.0
424                  *
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.
428                  */
429                 return apply_filters( 'rest_prepare_revision', $response, $post, $request );
430         }
431
432         /**
433          * Checks the post_date_gmt or modified_gmt and prepare any post or
434          * modified date for single post output.
435          *
436          * @since 4.7.0
437          * @access protected
438          *
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.
442          */
443         protected function prepare_date_response( $date_gmt, $date = null ) {
444                 if ( '0000-00-00 00:00:00' === $date_gmt ) {
445                         return null;
446                 }
447
448                 if ( isset( $date ) ) {
449                         return mysql_to_rfc3339( $date );
450                 }
451
452                 return mysql_to_rfc3339( $date_gmt );
453         }
454
455         /**
456          * Retrieves the revision's schema, conforming to JSON Schema.
457          *
458          * @since 4.7.0
459          * @access public
460          *
461          * @return array Item schema data.
462          */
463         public function get_item_schema() {
464                 $schema = array(
465                         '$schema'    => 'http://json-schema.org/schema#',
466                         'title'      => "{$this->parent_post_type}-revision",
467                         'type'       => 'object',
468                         // Base properties for every Revision.
469                         'properties' => array(
470                                 'author'          => array(
471                                         'description' => __( 'The ID for the author of the object.' ),
472                                         'type'        => 'integer',
473                                         'context'     => array( 'view', 'edit', 'embed' ),
474                                 ),
475                                 'date'            => array(
476                                         'description' => __( "The date the object was published, in the site's timezone." ),
477                                         'type'        => 'string',
478                                         'format'      => 'date-time',
479                                         'context'     => array( 'view', 'edit', 'embed' ),
480                                 ),
481                                 'date_gmt'        => array(
482                                         'description' => __( 'The date the object was published, as GMT.' ),
483                                         'type'        => 'string',
484                                         'format'      => 'date-time',
485                                         'context'     => array( 'view', 'edit' ),
486                                 ),
487                                 'guid'            => array(
488                                         'description' => __( 'GUID for the object, as it exists in the database.' ),
489                                         'type'        => 'string',
490                                         'context'     => array( 'view', 'edit' ),
491                                 ),
492                                 'id'              => array(
493                                         'description' => __( 'Unique identifier for the object.' ),
494                                         'type'        => 'integer',
495                                         'context'     => array( 'view', 'edit', 'embed' ),
496                                 ),
497                                 'modified'        => array(
498                                         'description' => __( "The date the object was last modified, in the site's timezone." ),
499                                         'type'        => 'string',
500                                         'format'      => 'date-time',
501                                         'context'     => array( 'view', 'edit' ),
502                                 ),
503                                 'modified_gmt'    => array(
504                                         'description' => __( 'The date the object was last modified, as GMT.' ),
505                                         'type'        => 'string',
506                                         'format'      => 'date-time',
507                                         'context'     => array( 'view', 'edit' ),
508                                 ),
509                                 'parent'          => array(
510                                         'description' => __( 'The ID for the parent of the object.' ),
511                                         'type'        => 'integer',
512                                         'context'     => array( 'view', 'edit', 'embed' ),
513                                         ),
514                                 'slug'            => array(
515                                         'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
516                                         'type'        => 'string',
517                                         'context'     => array( 'view', 'edit', 'embed' ),
518                                 ),
519                         ),
520                 );
521
522                 $parent_schema = $this->parent_controller->get_item_schema();
523
524                 if ( ! empty( $parent_schema['properties']['title'] ) ) {
525                         $schema['properties']['title'] = $parent_schema['properties']['title'];
526                 }
527
528                 if ( ! empty( $parent_schema['properties']['content'] ) ) {
529                         $schema['properties']['content'] = $parent_schema['properties']['content'];
530                 }
531
532                 if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
533                         $schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
534                 }
535
536                 if ( ! empty( $parent_schema['properties']['guid'] ) ) {
537                         $schema['properties']['guid'] = $parent_schema['properties']['guid'];
538                 }
539
540                 return $this->add_additional_fields_schema( $schema );
541         }
542
543         /**
544          * Retrieves the query params for collections.
545          *
546          * @since 4.7.0
547          * @access public
548          *
549          * @return array Collection parameters.
550          */
551         public function get_collection_params() {
552                 return array(
553                         'context' => $this->get_context_param( array( 'default' => 'view' ) ),
554                 );
555         }
556
557         /**
558          * Checks the post excerpt and prepare it for single post output.
559          *
560          * @since 4.7.0
561          * @access protected
562          *
563          * @param string  $excerpt The post excerpt.
564          * @param WP_Post $post    Post revision object.
565          * @return string Prepared excerpt or empty string.
566          */
567         protected function prepare_excerpt_response( $excerpt, $post ) {
568
569                 /** This filter is documented in wp-includes/post-template.php */
570                 $excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
571
572                 if ( empty( $excerpt ) ) {
573                         return '';
574                 }
575
576                 return $excerpt;
577         }
578 }