| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 */ 18 class 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_is_active = wp_check_post_lock( $post->ID ); 233 $is_draft = 'draft' === $post->post_status || 'auto-draft' === $post->post_status; 234 235 /* 236 * When a post is still in draft form, updates from the author can directly update the post. 237 * Other autosaves must be stored as per-user autosave revisions. 238 */ 239 $can_update_author_draft_post = ( 240 $is_draft && 241 (int) $post->post_author === $user_id 242 ); 243 244 $should_update_parent_draft_post = ( 245 ! $post_lock_is_active && $can_update_author_draft_post 246 ); 247 248 if ( $should_update_parent_draft_post ) { 249 /* 250 * Draft posts for the same author: autosaving updates the post and does not create a revision. 251 * Convert the post object to an array and add slashes, wp_update_post() expects escaped array. 252 */ 253 $autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true ); 254 } else { 255 $autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) ); 256 } 257 258 if ( is_wp_error( $autosave_id ) ) { 259 return $autosave_id; 260 } 261 262 $autosave = get_post( $autosave_id ); 263 $request->set_param( 'context', 'edit' ); 264 265 $response = $this->prepare_item_for_response( $autosave, $request ); 266 $response = rest_ensure_response( $response ); 267 268 return $response; 269 } 270 271 /** 272 * Get the autosave, if the ID is valid. 273 * 274 * @since 5.0.0 275 * 276 * @param WP_REST_Request $request Full details about the request. 277 * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise. 278 */ 279 public function get_item( $request ) { 280 $parent_id = (int) $request->get_param( 'parent' ); 281 282 if ( $parent_id <= 0 ) { 283 return new WP_Error( 284 'rest_post_invalid_id', 285 __( 'Invalid post parent ID.' ), 286 array( 'status' => 404 ) 287 ); 288 } 289 290 $autosave = wp_get_post_autosave( $parent_id ); 291 292 if ( ! $autosave ) { 293 return new WP_Error( 294 'rest_post_no_autosave', 295 __( 'There is no autosave revision for this post.' ), 296 array( 'status' => 404 ) 297 ); 298 } 299 300 $response = $this->prepare_item_for_response( $autosave, $request ); 301 return $response; 302 } 303 304 /** 305 * Gets a collection of autosaves using wp_get_post_autosave. 306 * 307 * Contains the user's autosave, for empty if it doesn't exist. 308 * 309 * @since 5.0.0 310 * 311 * @param WP_REST_Request $request Full details about the request. 312 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 313 */ 314 public function get_items( $request ) { 315 $parent = $this->get_parent( $request['id'] ); 316 if ( is_wp_error( $parent ) ) { 317 return $parent; 318 } 319 320 if ( $request->is_method( 'HEAD' ) ) { 321 // Return early as this handler doesn't add any response headers. 322 return new WP_REST_Response( array() ); 323 } 324 $response = array(); 325 $parent_id = $parent->ID; 326 $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) ); 327 328 foreach ( $revisions as $revision ) { 329 if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) { 330 $data = $this->prepare_item_for_response( $revision, $request ); 331 $response[] = $this->prepare_response_for_collection( $data ); 332 } 333 } 334 335 return rest_ensure_response( $response ); 336 } 337 338 339 /** 340 * Retrieves the autosave's schema, conforming to JSON Schema. 341 * 342 * @since 5.0.0 343 * 344 * @return array Item schema data. 345 */ 346 public function get_item_schema() { 347 if ( $this->schema ) { 348 return $this->add_additional_fields_schema( $this->schema ); 349 } 350 351 $schema = $this->revisions_controller->get_item_schema(); 352 353 $schema['properties']['preview_link'] = array( 354 'description' => __( 'Preview link for the post.' ), 355 'type' => 'string', 356 'format' => 'uri', 357 'context' => array( 'edit' ), 358 'readonly' => true, 359 ); 360 361 $this->schema = $schema; 362 363 return $this->add_additional_fields_schema( $this->schema ); 364 } 365 366 /** 367 * Creates autosave for the specified post. 368 * 369 * From wp-admin/post.php. 370 * 371 * @since 5.0.0 372 * @since 6.4.0 The `$meta` parameter was added. 373 * 374 * @param array $post_data Associative array containing the post data. 375 * @param array $meta Associative array containing the post meta data. 376 * @return mixed The autosave revision ID or WP_Error. 377 */ 378 public function create_post_autosave( $post_data, array $meta = array() ) { 379 380 $post_id = (int) $post_data['ID']; 381 $post = get_post( $post_id ); 382 383 if ( is_wp_error( $post ) ) { 384 return $post; 385 } 386 387 // Only create an autosave when it is different from the saved post. 388 $autosave_is_different = false; 389 $new_autosave = _wp_post_revision_data( $post_data, true ); 390 391 foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) { 392 if ( normalize_whitespace( $new_autosave[ $field ] ) !== normalize_whitespace( $post->$field ) ) { 393 $autosave_is_different = true; 394 break; 395 } 396 } 397 398 // Check if meta values have changed. 399 if ( ! empty( $meta ) ) { 400 $revisioned_meta_keys = wp_post_revision_meta_keys( $post->post_type ); 401 foreach ( $revisioned_meta_keys as $meta_key ) { 402 // get_metadata_raw is used to avoid retrieving the default value. 403 $old_meta = get_metadata_raw( 'post', $post_id, $meta_key, true ); 404 $new_meta = $meta[ $meta_key ] ?? ''; 405 406 if ( $new_meta !== $old_meta ) { 407 $autosave_is_different = true; 408 break; 409 } 410 } 411 } 412 413 $user_id = get_current_user_id(); 414 415 // Store one autosave per author. If there is already an autosave, overwrite it. 416 $old_autosave = wp_get_post_autosave( $post_id, $user_id ); 417 418 if ( ! $autosave_is_different && $old_autosave ) { 419 // Nothing to save, return the existing autosave. 420 return $old_autosave->ID; 421 } 422 423 if ( $old_autosave ) { 424 $new_autosave['ID'] = $old_autosave->ID; 425 $new_autosave['post_author'] = $user_id; 426 427 /** This action is documented in wp-admin/includes/post.php */ 428 do_action( 'wp_creating_autosave', $new_autosave ); 429 430 // wp_update_post() expects escaped array. 431 $revision_id = wp_update_post( wp_slash( $new_autosave ) ); 432 } else { 433 // Create the new autosave as a special post revision. 434 $revision_id = _wp_put_post_revision( $post_data, true ); 435 } 436 437 if ( is_wp_error( $revision_id ) || 0 === $revision_id ) { 438 return $revision_id; 439 } 440 441 // Attached any passed meta values that have revisions enabled. 442 if ( ! empty( $meta ) ) { 443 foreach ( $revisioned_meta_keys as $meta_key ) { 444 if ( isset( $meta[ $meta_key ] ) ) { 445 update_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta[ $meta_key ] ) ); 446 } 447 } 448 } 449 450 return $revision_id; 451 } 452 453 /** 454 * Prepares the revision for the REST response. 455 * 456 * @since 5.0.0 457 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support. 458 * 459 * @param WP_Post $item Post revision object. 460 * @param WP_REST_Request $request Request object. 461 * @return WP_REST_Response Response object. 462 */ 463 public function prepare_item_for_response( $item, $request ) { 464 // Restores the more descriptive, specific name for use within this method. 465 $post = $item; 466 467 // Don't prepare the response body for HEAD requests. 468 if ( $request->is_method( 'HEAD' ) ) { 469 /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php */ 470 return apply_filters( 'rest_prepare_autosave', new WP_REST_Response( array() ), $post, $request ); 471 } 472 $response = $this->revisions_controller->prepare_item_for_response( $post, $request ); 473 $fields = $this->get_fields_for_response( $request ); 474 475 if ( in_array( 'preview_link', $fields, true ) ) { 476 $parent_id = wp_is_post_autosave( $post ); 477 $preview_post_id = false === $parent_id ? $post->ID : $parent_id; 478 $preview_query_args = array(); 479 480 if ( false !== $parent_id ) { 481 $preview_query_args['preview_id'] = $parent_id; 482 $preview_query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $parent_id ); 483 } 484 485 $response->data['preview_link'] = get_preview_post_link( $preview_post_id, $preview_query_args ); 486 } 487 488 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 489 $response->data = $this->add_additional_fields_to_object( $response->data, $request ); 490 $response->data = $this->filter_response_by_context( $response->data, $context ); 491 492 /** 493 * Filters a revision returned from the REST API. 494 * 495 * Allows modification of the revision right before it is returned. 496 * 497 * @since 5.0.0 498 * 499 * @param WP_REST_Response $response The response object. 500 * @param WP_Post $post The original revision object. 501 * @param WP_REST_Request $request Request used to generate the response. 502 */ 503 return apply_filters( 'rest_prepare_autosave', $response, $post, $request ); 504 } 505 506 /** 507 * Retrieves the query params for the autosaves collection. 508 * 509 * @since 5.0.0 510 * 511 * @return array Collection parameters. 512 */ 513 public function get_collection_params() { 514 return array( 515 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 516 ); 517 } 518 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Jun 14 08:20:09 2026 | Cross-referenced by PHPXref |