WordPress 4.7
[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                         array(
75                                 'methods'             => WP_REST_Server::READABLE,
76                                 'callback'            => array( $this, 'get_items' ),
77                                 'permission_callback' => array( $this, 'get_items_permissions_check' ),
78                                 'args'                => $this->get_collection_params(),
79                         ),
80                         'schema' => array( $this, 'get_public_item_schema' ),
81                 ) );
82
83                 register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
84                         array(
85                                 'methods'             => WP_REST_Server::READABLE,
86                                 'callback'            => array( $this, 'get_item' ),
87                                 'permission_callback' => array( $this, 'get_item_permissions_check' ),
88                                 'args'                => array(
89                                         'context' => $this->get_context_param( array( 'default' => 'view' ) ),
90                                 ),
91                         ),
92                         array(
93                                 'methods'             => WP_REST_Server::DELETABLE,
94                                 'callback'            => array( $this, 'delete_item' ),
95                                 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
96                                 'args'                => array(
97                                         'force' => array(
98                                                 'type'        => 'boolean',
99                                                 'default'     => false,
100                                                 'description' => __( 'Required to be true, as revisions do not support trashing.' ),
101                                         ),
102                                 ),
103                         ),
104                         'schema' => array( $this, 'get_public_item_schema' ),
105                 ));
106
107         }
108
109         /**
110          * Checks if a given request has access to get revisions.
111          *
112          * @since 4.7.0
113          * @access public
114          *
115          * @param WP_REST_Request $request Full data about the request.
116          * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
117          */
118         public function get_items_permissions_check( $request ) {
119
120                 $parent = get_post( $request['parent'] );
121                 if ( ! $parent ) {
122                         return true;
123                 }
124                 $parent_post_type_obj = get_post_type_object( $parent->post_type );
125                 if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
126                         return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
127                 }
128
129                 return true;
130         }
131
132         /**
133          * Gets a collection of revisions.
134          *
135          * @since 4.7.0
136          * @access public
137          *
138          * @param WP_REST_Request $request Full data about the request.
139          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
140          */
141         public function get_items( $request ) {
142                 $parent = get_post( $request['parent'] );
143                 if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
144                         return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent ID.' ), array( 'status' => 404 ) );
145                 }
146
147                 $revisions = wp_get_post_revisions( $request['parent'] );
148
149                 $response = array();
150                 foreach ( $revisions as $revision ) {
151                         $data = $this->prepare_item_for_response( $revision, $request );
152                         $response[] = $this->prepare_response_for_collection( $data );
153                 }
154                 return rest_ensure_response( $response );
155         }
156
157         /**
158          * Checks if a given request has access to get a specific revision.
159          *
160          * @since 4.7.0
161          * @access public
162          *
163          * @param WP_REST_Request $request Full data about the request.
164          * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
165          */
166         public function get_item_permissions_check( $request ) {
167                 return $this->get_items_permissions_check( $request );
168         }
169
170         /**
171          * Retrieves one revision from the collection.
172          *
173          * @since 4.7.0
174          * @access public
175          *
176          * @param WP_REST_Request $request Full data about the request.
177          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
178          */
179         public function get_item( $request ) {
180                 $parent = get_post( $request['parent'] );
181                 if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
182                         return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent ID.' ), array( 'status' => 404 ) );
183                 }
184
185                 $revision = get_post( $request['id'] );
186                 if ( ! $revision || 'revision' !== $revision->post_type ) {
187                         return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision ID.' ), array( 'status' => 404 ) );
188                 }
189
190                 $response = $this->prepare_item_for_response( $revision, $request );
191                 return rest_ensure_response( $response );
192         }
193
194         /**
195          * Checks if a given request has access to delete a revision.
196          *
197          * @since 4.7.0
198          * @access public
199          *
200          * @param  WP_REST_Request $request Full details about the request.
201          * @return bool|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
202          */
203         public function delete_item_permissions_check( $request ) {
204
205                 $response = $this->get_items_permissions_check( $request );
206                 if ( ! $response || is_wp_error( $response ) ) {
207                         return $response;
208                 }
209
210                 $post = get_post( $request['id'] );
211                 if ( ! $post ) {
212                         return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision ID.' ), array( 'status' => 404 ) );
213                 }
214                 $post_type = get_post_type_object( 'revision' );
215                 return current_user_can( $post_type->cap->delete_post, $post->ID );
216         }
217
218         /**
219          * Deletes a single revision.
220          *
221          * @since 4.7.0
222          * @access public
223          *
224          * @param WP_REST_Request $request Full details about the request.
225          * @return true|WP_Error True on success, or WP_Error object on failure.
226          */
227         public function delete_item( $request ) {
228                 $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
229
230                 // We don't support trashing for revisions.
231                 if ( ! $force ) {
232                         return new WP_Error( 'rest_trash_not_supported', __( 'Revisions do not support trashing. Set force=true to delete.' ), array( 'status' => 501 ) );
233                 }
234
235                 $revision = get_post( $request['id'] );
236                 $previous = $this->prepare_item_for_response( $revision, $request );
237
238                 $result = wp_delete_post( $request['id'], true );
239
240                 /**
241                  * Fires after a revision is deleted via the REST API.
242                  *
243                  * @since 4.7.0
244                  *
245                  * @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
246                  *                        or false (failure). If the revision was moved to to the trash, $result represents
247                  *                        its new state; if it was deleted, $result represents its state before deletion.
248                  * @param WP_REST_Request $request The request sent to the API.
249                  */
250                 do_action( 'rest_delete_revision', $result, $request );
251
252                 if ( ! $result ) {
253                         return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
254                 }
255
256                 $response = new WP_REST_Response();
257                 $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
258                 return $response;
259         }
260
261         /**
262          * Prepares the revision for the REST response.
263          *
264          * @since 4.7.0
265          * @access public
266          *
267          * @param WP_Post         $post    Post revision object.
268          * @param WP_REST_Request $request Request object.
269          * @return WP_REST_Response Response object.
270          */
271         public function prepare_item_for_response( $post, $request ) {
272
273                 $schema = $this->get_item_schema();
274
275                 $data = array();
276
277                 if ( ! empty( $schema['properties']['author'] ) ) {
278                         $data['author'] = $post->post_author;
279                 }
280
281                 if ( ! empty( $schema['properties']['date'] ) ) {
282                         $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
283                 }
284
285                 if ( ! empty( $schema['properties']['date_gmt'] ) ) {
286                         $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
287                 }
288
289                 if ( ! empty( $schema['properties']['id'] ) ) {
290                         $data['id'] = $post->ID;
291                 }
292
293                 if ( ! empty( $schema['properties']['modified'] ) ) {
294                         $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
295                 }
296
297                 if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
298                         $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
299                 }
300
301                 if ( ! empty( $schema['properties']['parent'] ) ) {
302                         $data['parent'] = (int) $post->post_parent;
303                 }
304
305                 if ( ! empty( $schema['properties']['slug'] ) ) {
306                         $data['slug'] = $post->post_name;
307                 }
308
309                 if ( ! empty( $schema['properties']['guid'] ) ) {
310                         $data['guid'] = array(
311                                 /** This filter is documented in wp-includes/post-template.php */
312                                 'rendered' => apply_filters( 'get_the_guid', $post->guid ),
313                                 'raw'      => $post->guid,
314                         );
315                 }
316
317                 if ( ! empty( $schema['properties']['title'] ) ) {
318                         $data['title'] = array(
319                                 'raw'      => $post->post_title,
320                                 'rendered' => get_the_title( $post->ID ),
321                         );
322                 }
323
324                 if ( ! empty( $schema['properties']['content'] ) ) {
325
326                         $data['content'] = array(
327                                 'raw'      => $post->post_content,
328                                 /** This filter is documented in wp-includes/post-template.php */
329                                 'rendered' => apply_filters( 'the_content', $post->post_content ),
330                         );
331                 }
332
333                 if ( ! empty( $schema['properties']['excerpt'] ) ) {
334                         $data['excerpt'] = array(
335                                 'raw'      => $post->post_excerpt,
336                                 'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
337                         );
338                 }
339
340                 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
341                 $data = $this->add_additional_fields_to_object( $data, $request );
342                 $data = $this->filter_response_by_context( $data, $context );
343                 $response = rest_ensure_response( $data );
344
345                 if ( ! empty( $data['parent'] ) ) {
346                         $response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
347                 }
348
349                 /**
350                  * Filters a revision returned from the API.
351                  *
352                  * Allows modification of the revision right before it is returned.
353                  *
354                  * @since 4.7.0
355                  *
356                  * @param WP_REST_Response $response The response object.
357                  * @param WP_Post          $post     The original revision object.
358                  * @param WP_REST_Request  $request  Request used to generate the response.
359                  */
360                 return apply_filters( 'rest_prepare_revision', $response, $post, $request );
361         }
362
363         /**
364          * Checks the post_date_gmt or modified_gmt and prepare any post or
365          * modified date for single post output.
366          *
367          * @since 4.7.0
368          * @access protected
369          *
370          * @param string      $date_gmt GMT publication time.
371          * @param string|null $date     Optional. Local publication time. Default null.
372          * @return string|null ISO8601/RFC3339 formatted datetime, otherwise null.
373          */
374         protected function prepare_date_response( $date_gmt, $date = null ) {
375                 if ( '0000-00-00 00:00:00' === $date_gmt ) {
376                         return null;
377                 }
378
379                 if ( isset( $date ) ) {
380                         return mysql_to_rfc3339( $date );
381                 }
382
383                 return mysql_to_rfc3339( $date_gmt );
384         }
385
386         /**
387          * Retrieves the revision's schema, conforming to JSON Schema.
388          *
389          * @since 4.7.0
390          * @access public
391          *
392          * @return array Item schema data.
393          */
394         public function get_item_schema() {
395                 $schema = array(
396                         '$schema'    => 'http://json-schema.org/schema#',
397                         'title'      => "{$this->parent_post_type}-revision",
398                         'type'       => 'object',
399                         // Base properties for every Revision.
400                         'properties' => array(
401                                 'author'          => array(
402                                         'description' => __( 'The ID for the author of the object.' ),
403                                         'type'        => 'integer',
404                                         'context'     => array( 'view', 'edit', 'embed' ),
405                                 ),
406                                 'date'            => array(
407                                         'description' => __( "The date the object was published, in the site's timezone." ),
408                                         'type'        => 'string',
409                                         'format'      => 'date-time',
410                                         'context'     => array( 'view', 'edit', 'embed' ),
411                                 ),
412                                 'date_gmt'        => array(
413                                         'description' => __( 'The date the object was published, as GMT.' ),
414                                         'type'        => 'string',
415                                         'format'      => 'date-time',
416                                         'context'     => array( 'view', 'edit' ),
417                                 ),
418                                 'guid'            => array(
419                                         'description' => __( 'GUID for the object, as it exists in the database.' ),
420                                         'type'        => 'string',
421                                         'context'     => array( 'view', 'edit' ),
422                                 ),
423                                 'id'              => array(
424                                         'description' => __( 'Unique identifier for the object.' ),
425                                         'type'        => 'integer',
426                                         'context'     => array( 'view', 'edit', 'embed' ),
427                                 ),
428                                 'modified'        => array(
429                                         'description' => __( "The date the object was last modified, in the site's timezone." ),
430                                         'type'        => 'string',
431                                         'format'      => 'date-time',
432                                         'context'     => array( 'view', 'edit' ),
433                                 ),
434                                 'modified_gmt'    => array(
435                                         'description' => __( 'The date the object was last modified, as GMT.' ),
436                                         'type'        => 'string',
437                                         'format'      => 'date-time',
438                                         'context'     => array( 'view', 'edit' ),
439                                 ),
440                                 'parent'          => array(
441                                         'description' => __( 'The ID for the parent of the object.' ),
442                                         'type'        => 'integer',
443                                         'context'     => array( 'view', 'edit', 'embed' ),
444                                         ),
445                                 'slug'            => array(
446                                         'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
447                                         'type'        => 'string',
448                                         'context'     => array( 'view', 'edit', 'embed' ),
449                                 ),
450                         ),
451                 );
452
453                 $parent_schema = $this->parent_controller->get_item_schema();
454
455                 if ( ! empty( $parent_schema['properties']['title'] ) ) {
456                         $schema['properties']['title'] = $parent_schema['properties']['title'];
457                 }
458
459                 if ( ! empty( $parent_schema['properties']['content'] ) ) {
460                         $schema['properties']['content'] = $parent_schema['properties']['content'];
461                 }
462
463                 if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
464                         $schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
465                 }
466
467                 if ( ! empty( $parent_schema['properties']['guid'] ) ) {
468                         $schema['properties']['guid'] = $parent_schema['properties']['guid'];
469                 }
470
471                 return $this->add_additional_fields_schema( $schema );
472         }
473
474         /**
475          * Retrieves the query params for collections.
476          *
477          * @since 4.7.0
478          * @access public
479          *
480          * @return array Collection parameters.
481          */
482         public function get_collection_params() {
483                 return array(
484                         'context' => $this->get_context_param( array( 'default' => 'view' ) ),
485                 );
486         }
487
488         /**
489          * Checks the post excerpt and prepare it for single post output.
490          *
491          * @since 4.7.0
492          * @access protected
493          *
494          * @param string  $excerpt The post excerpt.
495          * @param WP_Post $post    Post revision object.
496          * @return string Prepared excerpt or empty string.
497          */
498         protected function prepare_excerpt_response( $excerpt, $post ) {
499
500                 /** This filter is documented in wp-includes/post-template.php */
501                 $excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
502
503                 if ( empty( $excerpt ) ) {
504                         return '';
505                 }
506
507                 return $excerpt;
508         }
509 }