Make WordPress Core

source: tags/6.5/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php

Last change on this file was 56819, checked in by spacedmonkey, 9 months ago

REST API: Fix issue with Template and Template Part Revision/Autosave REST API controllers.

The Template and Template Part REST API controllers have unique characteristics compared to other post type REST API controllers. They do not rely on integer IDs to reference objects; instead, they use a combination of the theme name and slug of the template, like 'twentytwentyfourhome.' Consequently, when the post types template and template part were introduced in [52062], it led to the registration of REST API endpoints for autosaves and revisions with invalid URL structures.

In this commit, we introduce new functionality to enable custom autosave and revisions endpoints to be registered at the post type level. Similar to the 'rest_controller_class' parameter, developers can now define 'revisions_rest_controller' and 'autosave_rest_controller.' This empowers developers to create custom controllers for these functionalities. Additionally, we introduce a 'late_route_registration' parameter, which proves helpful when dealing with custom URL patterns and regex pattern matching issues.
This commit registers new classes for template and template part autosave and revisions controllers, differentiating them from standard controllers in the following ways:

  • The response shape now matches that of the template controller.
  • Permission checks align with the template controller.
  • A custom URL pattern is introduced to support slug-based identification of templates.

Furthermore, we've updated the utility function '_build_block_template_result_from_post' to support passing revision post objects. This enhancement ensures compatibility with the custom revisions controller.

Props spacedmonkey, revgeorge, andraganescu, hellofromTonya, antonvlasenko, kadamwhite, ironprogrammer, costdev, mukesh27, timothyblynjacobs, adamsilverstein.
Fixes 56922.

  • Property svn:eol-style set to native
File size: 14.4 KB
Line 
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_Revisions_Controller
16 * @see WP_REST_Controller
17 */
18class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller {
19
20        /**
21         * Parent post type.
22         *
23         * @since 5.0.0
24         * @var string
25         */
26        private $parent_post_type;
27
28        /**
29         * Parent post controller.
30         *
31         * @since 5.0.0
32         * @var WP_REST_Controller
33         */
34        private $parent_controller;
35
36        /**
37         * Revision controller.
38         *
39         * @since 5.0.0
40         * @var WP_REST_Revisions_Controller
41         */
42        private $revisions_controller;
43
44        /**
45         * The base of the parent controller's route.
46         *
47         * @since 5.0.0
48         * @var string
49         */
50        private $parent_base;
51
52        /**
53         * Constructor.
54         *
55         * @since 5.0.0
56         *
57         * @param string $parent_post_type Post type of the parent.
58         */
59        public function __construct( $parent_post_type ) {
60                $this->parent_post_type = $parent_post_type;
61                $post_type_object       = get_post_type_object( $parent_post_type );
62                $parent_controller      = $post_type_object->get_rest_controller();
63
64                if ( ! $parent_controller ) {
65                        $parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
66                }
67
68                $this->parent_controller = $parent_controller;
69
70                $revisions_controller = $post_type_object->get_revisions_rest_controller();
71                if ( ! $revisions_controller ) {
72                        $revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
73                }
74                $this->revisions_controller = $revisions_controller;
75                $this->rest_base            = 'autosaves';
76                $this->parent_base          = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
77                $this->namespace            = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
78        }
79
80        /**
81         * Registers the routes for autosaves.
82         *
83         * @since 5.0.0
84         *
85         * @see register_rest_route()
86         */
87        public function register_routes() {
88                register_rest_route(
89                        $this->namespace,
90                        '/' . $this->parent_base . '/(?P<id>[\d]+)/' . $this->rest_base,
91                        array(
92                                'args'   => array(
93                                        'parent' => array(
94                                                'description' => __( 'The ID for the parent of the autosave.' ),
95                                                'type'        => 'integer',
96                                        ),
97                                ),
98                                array(
99                                        'methods'             => WP_REST_Server::READABLE,
100                                        'callback'            => array( $this, 'get_items' ),
101                                        'permission_callback' => array( $this, 'get_items_permissions_check' ),
102                                        'args'                => $this->get_collection_params(),
103                                ),
104                                array(
105                                        'methods'             => WP_REST_Server::CREATABLE,
106                                        'callback'            => array( $this, 'create_item' ),
107                                        'permission_callback' => array( $this, 'create_item_permissions_check' ),
108                                        'args'                => $this->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
109                                ),
110                                'schema' => array( $this, 'get_public_item_schema' ),
111                        )
112                );
113
114                register_rest_route(
115                        $this->namespace,
116                        '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
117                        array(
118                                'args'   => array(
119                                        'parent' => array(
120                                                'description' => __( 'The ID for the parent of the autosave.' ),
121                                                'type'        => 'integer',
122                                        ),
123                                        'id'     => array(
124                                                'description' => __( 'The ID for the autosave.' ),
125                                                'type'        => 'integer',
126                                        ),
127                                ),
128                                array(
129                                        'methods'             => WP_REST_Server::READABLE,
130                                        'callback'            => array( $this, 'get_item' ),
131                                        'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ),
132                                        'args'                => array(
133                                                'context' => $this->get_context_param( array( 'default' => 'view' ) ),
134                                        ),
135                                ),
136                                'schema' => array( $this, 'get_public_item_schema' ),
137                        )
138                );
139        }
140
141        /**
142         * Get the parent post.
143         *
144         * @since 5.0.0
145         *
146         * @param int $parent_id Supplied ID.
147         * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
148         */
149        protected function get_parent( $parent_id ) {
150                return $this->revisions_controller->get_parent( $parent_id );
151        }
152
153        /**
154         * Checks if a given request has access to get autosaves.
155         *
156         * @since 5.0.0
157         *
158         * @param WP_REST_Request $request Full details about the request.
159         * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
160         */
161        public function get_items_permissions_check( $request ) {
162                $parent = $this->get_parent( $request['id'] );
163                if ( is_wp_error( $parent ) ) {
164                        return $parent;
165                }
166
167                if ( ! current_user_can( 'edit_post', $parent->ID ) ) {
168                        return new WP_Error(
169                                'rest_cannot_read',
170                                __( 'Sorry, you are not allowed to view autosaves of this post.' ),
171                                array( 'status' => rest_authorization_required_code() )
172                        );
173                }
174
175                return true;
176        }
177
178        /**
179         * Checks if a given request has access to create an autosave revision.
180         *
181         * Autosave revisions inherit permissions from the parent post,
182         * check if the current user has permission to edit the post.
183         *
184         * @since 5.0.0
185         *
186         * @param WP_REST_Request $request Full details about the request.
187         * @return true|WP_Error True if the request has access to create the item, WP_Error object otherwise.
188         */
189        public function create_item_permissions_check( $request ) {
190                $id = $request->get_param( 'id' );
191
192                if ( empty( $id ) ) {
193                        return new WP_Error(
194                                'rest_post_invalid_id',
195                                __( 'Invalid item ID.' ),
196                                array( 'status' => 404 )
197                        );
198                }
199
200                return $this->parent_controller->update_item_permissions_check( $request );
201        }
202
203        /**
204         * Creates, updates or deletes an autosave revision.
205         *
206         * @since 5.0.0
207         *
208         * @param WP_REST_Request $request Full details about the request.
209         * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
210         */
211        public function create_item( $request ) {
212
213                if ( ! defined( 'WP_RUN_CORE_TESTS' ) && ! defined( 'DOING_AUTOSAVE' ) ) {
214                        define( 'DOING_AUTOSAVE', true );
215                }
216
217                $post = $this->get_parent( $request['id'] );
218
219                if ( is_wp_error( $post ) ) {
220                        return $post;
221                }
222
223                $prepared_post     = $this->parent_controller->prepare_item_for_database( $request );
224                $prepared_post->ID = $post->ID;
225                $user_id           = get_current_user_id();
226
227                // We need to check post lock to ensure the original author didn't leave their browser tab open.
228                if ( ! function_exists( 'wp_check_post_lock' ) ) {
229                        require_once ABSPATH . 'wp-admin/includes/post.php';
230                }
231
232                $post_lock = wp_check_post_lock( $post->ID );
233                $is_draft  = 'draft' === $post->post_status || 'auto-draft' === $post->post_status;
234
235                if ( $is_draft && (int) $post->post_author === $user_id && ! $post_lock ) {
236                        /*
237                         * Draft posts for the same author: autosaving updates the post and does not create a revision.
238                         * Convert the post object to an array and add slashes, wp_update_post() expects escaped array.
239                         */
240                        $autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
241                } else {
242                        // Non-draft posts: create or update the post autosave. Pass the meta data.
243                        $autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) );
244                }
245
246                if ( is_wp_error( $autosave_id ) ) {
247                        return $autosave_id;
248                }
249
250                $autosave = get_post( $autosave_id );
251                $request->set_param( 'context', 'edit' );
252
253                $response = $this->prepare_item_for_response( $autosave, $request );
254                $response = rest_ensure_response( $response );
255
256                return $response;
257        }
258
259        /**
260         * Get the autosave, if the ID is valid.
261         *
262         * @since 5.0.0
263         *
264         * @param WP_REST_Request $request Full details about the request.
265         * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
266         */
267        public function get_item( $request ) {
268                $parent_id = (int) $request->get_param( 'parent' );
269
270                if ( $parent_id <= 0 ) {
271                        return new WP_Error(
272                                'rest_post_invalid_id',
273                                __( 'Invalid post parent ID.' ),
274                                array( 'status' => 404 )
275                        );
276                }
277
278                $autosave = wp_get_post_autosave( $parent_id );
279
280                if ( ! $autosave ) {
281                        return new WP_Error(
282                                'rest_post_no_autosave',
283                                __( 'There is no autosave revision for this post.' ),
284                                array( 'status' => 404 )
285                        );
286                }
287
288                $response = $this->prepare_item_for_response( $autosave, $request );
289                return $response;
290        }
291
292        /**
293         * Gets a collection of autosaves using wp_get_post_autosave.
294         *
295         * Contains the user's autosave, for empty if it doesn't exist.
296         *
297         * @since 5.0.0
298         *
299         * @param WP_REST_Request $request Full details about the request.
300         * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
301         */
302        public function get_items( $request ) {
303                $parent = $this->get_parent( $request['id'] );
304                if ( is_wp_error( $parent ) ) {
305                        return $parent;
306                }
307
308                $response  = array();
309                $parent_id = $parent->ID;
310                $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) );
311
312                foreach ( $revisions as $revision ) {
313                        if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) {
314                                $data       = $this->prepare_item_for_response( $revision, $request );
315                                $response[] = $this->prepare_response_for_collection( $data );
316                        }
317                }
318
319                return rest_ensure_response( $response );
320        }
321
322
323        /**
324         * Retrieves the autosave's schema, conforming to JSON Schema.
325         *
326         * @since 5.0.0
327         *
328         * @return array Item schema data.
329         */
330        public function get_item_schema() {
331                if ( $this->schema ) {
332                        return $this->add_additional_fields_schema( $this->schema );
333                }
334
335                $schema = $this->revisions_controller->get_item_schema();
336
337                $schema['properties']['preview_link'] = array(
338                        'description' => __( 'Preview link for the post.' ),
339                        'type'        => 'string',
340                        'format'      => 'uri',
341                        'context'     => array( 'edit' ),
342                        'readonly'    => true,
343                );
344
345                $this->schema = $schema;
346
347                return $this->add_additional_fields_schema( $this->schema );
348        }
349
350        /**
351         * Creates autosave for the specified post.
352         *
353         * From wp-admin/post.php.
354         *
355         * @since 5.0.0
356         * @since 6.4.0 The `$meta` parameter was added.
357         *
358         * @param array $post_data Associative array containing the post data.
359         * @param array $meta      Associative array containing the post meta data.
360         * @return mixed The autosave revision ID or WP_Error.
361         */
362        public function create_post_autosave( $post_data, array $meta = array() ) {
363
364                $post_id = (int) $post_data['ID'];
365                $post    = get_post( $post_id );
366
367                if ( is_wp_error( $post ) ) {
368                        return $post;
369                }
370
371                // Only create an autosave when it is different from the saved post.
372                $autosave_is_different = false;
373                $new_autosave          = _wp_post_revision_data( $post_data, true );
374
375                foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) {
376                        if ( normalize_whitespace( $new_autosave[ $field ] ) !== normalize_whitespace( $post->$field ) ) {
377                                $autosave_is_different = true;
378                                break;
379                        }
380                }
381
382                // Check if meta values have changed.
383                if ( ! empty( $meta ) ) {
384                        $revisioned_meta_keys = wp_post_revision_meta_keys( $post->post_type );
385                        foreach ( $revisioned_meta_keys as $meta_key ) {
386                                // get_metadata_raw is used to avoid retrieving the default value.
387                                $old_meta = get_metadata_raw( 'post', $post_id, $meta_key, true );
388                                $new_meta = isset( $meta[ $meta_key ] ) ? $meta[ $meta_key ] : '';
389
390                                if ( $new_meta !== $old_meta ) {
391                                        $autosave_is_different = true;
392                                        break;
393                                }
394                        }
395                }
396
397                $user_id = get_current_user_id();
398
399                // Store one autosave per author. If there is already an autosave, overwrite it.
400                $old_autosave = wp_get_post_autosave( $post_id, $user_id );
401
402                if ( ! $autosave_is_different && $old_autosave ) {
403                        // Nothing to save, return the existing autosave.
404                        return $old_autosave->ID;
405                }
406
407                if ( $old_autosave ) {
408                        $new_autosave['ID']          = $old_autosave->ID;
409                        $new_autosave['post_author'] = $user_id;
410
411                        /** This filter is documented in wp-admin/post.php */
412                        do_action( 'wp_creating_autosave', $new_autosave );
413
414                        // wp_update_post() expects escaped array.
415                        $revision_id = wp_update_post( wp_slash( $new_autosave ) );
416                } else {
417                        // Create the new autosave as a special post revision.
418                        $revision_id = _wp_put_post_revision( $post_data, true );
419                }
420
421                if ( is_wp_error( $revision_id ) || 0 === $revision_id ) {
422                        return $revision_id;
423                }
424
425                // Attached any passed meta values that have revisions enabled.
426                if ( ! empty( $meta ) ) {
427                        foreach ( $revisioned_meta_keys as $meta_key ) {
428                                if ( isset( $meta[ $meta_key ] ) ) {
429                                        update_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta[ $meta_key ] ) );
430                                }
431                        }
432                }
433
434                return $revision_id;
435        }
436
437        /**
438         * Prepares the revision for the REST response.
439         *
440         * @since 5.0.0
441         * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
442         *
443         * @param WP_Post         $item    Post revision object.
444         * @param WP_REST_Request $request Request object.
445         * @return WP_REST_Response Response object.
446         */
447        public function prepare_item_for_response( $item, $request ) {
448                // Restores the more descriptive, specific name for use within this method.
449                $post = $item;
450
451                $response = $this->revisions_controller->prepare_item_for_response( $post, $request );
452                $fields   = $this->get_fields_for_response( $request );
453
454                if ( in_array( 'preview_link', $fields, true ) ) {
455                        $parent_id          = wp_is_post_autosave( $post );
456                        $preview_post_id    = false === $parent_id ? $post->ID : $parent_id;
457                        $preview_query_args = array();
458
459                        if ( false !== $parent_id ) {
460                                $preview_query_args['preview_id']    = $parent_id;
461                                $preview_query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $parent_id );
462                        }
463
464                        $response->data['preview_link'] = get_preview_post_link( $preview_post_id, $preview_query_args );
465                }
466
467                $context        = ! empty( $request['context'] ) ? $request['context'] : 'view';
468                $response->data = $this->add_additional_fields_to_object( $response->data, $request );
469                $response->data = $this->filter_response_by_context( $response->data, $context );
470
471                /**
472                 * Filters a revision returned from the REST API.
473                 *
474                 * Allows modification of the revision right before it is returned.
475                 *
476                 * @since 5.0.0
477                 *
478                 * @param WP_REST_Response $response The response object.
479                 * @param WP_Post          $post     The original revision object.
480                 * @param WP_REST_Request  $request  Request used to generate the response.
481                 */
482                return apply_filters( 'rest_prepare_autosave', $response, $post, $request );
483        }
484
485        /**
486         * Retrieves the query params for the autosaves collection.
487         *
488         * @since 5.0.0
489         *
490         * @return array Collection parameters.
491         */
492        public function get_collection_params() {
493                return array(
494                        'context' => $this->get_context_param( array( 'default' => 'view' ) ),
495                );
496        }
497}
Note: See TracBrowser for help on using the repository browser.