[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-autosaves-controller.php (source)

   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  }


Generated : Sun Jun 14 08:20:09 2026 Cross-referenced by PHPXref