PluginProbe
Gutenberg / 4.6.1
Gutenberg v4.6.1
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / class-wp-rest-autosaves-controller.php

class-wp-rest-autosaves-controller.php in Gutenberg 4.6.1, at lib/class-wp-rest-autosaves-controller.php

427 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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.', 'gutenberg' ),
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.', 'gutenberg' ),
113 'type' => 'integer',
114 ),
115 'id' => array(
116 'description' => __( 'The ID for the object.', 'gutenberg' ),
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 revisions of this post.', 'gutenberg' ), 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.', 'gutenberg' ), 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.', 'gutenberg' ), 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.', 'gutenberg' ), 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.', 'gutenberg' ),
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.', 'gutenberg' ), 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 $schema = $this->get_item_schema();
383
384 if ( ! empty( $schema['properties']['preview_link'] ) ) {
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->filter_response_by_context( $response->data, $context );
399
400 /**
401 * Filters a revision returned from the API.
402 *
403 * Allows modification of the revision right before it is returned.
404 *
405 * @since 5.0.0
406 *
407 * @param WP_REST_Response $response The response object.
408 * @param WP_Post $post The original revision object.
409 * @param WP_REST_Request $request Request used to generate the response.
410 */
411 return apply_filters( 'rest_prepare_autosave', $response, $post, $request );
412 }
413
414 /**
415 * Retrieves the query params for the autosaves collection.
416 *
417 * @since 5.0.0
418 *
419 * @return array Collection parameters.
420 */
421 public function get_collection_params() {
422 return array(
423 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
424 );
425 }
426 }
427