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