From d4d16ec0830927009250c34c065b918d82136924 Mon Sep 17 00:00:00 2001 From: desrosj Date: Thu, 13 Dec 2018 22:42:38 +0000 Subject: [PATCH] REST API: Introduce Autosaves controller and endpoint. - Adds `WP_REST_Autosaves_Controller` which extends `WP_REST_Revisions_Controller`. - Autosaves endpoint is registered for all post types except attachment because even post types without revisions enabled are expected to autosave. - Because setting the `DOING_AUTOSAVE` constant pollutes the test suite, autosaves tests are run last. We may want to improve upon this later. Also, use a truly impossibly high number in User Controller tests. The number `100`, (or `7777` in `trunk`), could be valid in certain test run configurations. The `REST_TESTS_IMPOSSIBLY_HIGH_NUMBER` constant is impossibly high for this very reason. Finally, Skip Autosaves controller test for multisite. There's a PHP 5.2 edge case where paths calculated differently, possibly caused by differing version of PHPUnit. Props adamsilverstein, aduth, azaozz, danielbachhuber, rmccue, danielbachhuber. Merges [43767], [43768], [43769] to trunk. See #45132, #45131. Fixes #45128, #43316. Built from https://develop.svn.wordpress.org/trunk@44126 git-svn-id: http://core.svn.wordpress.org/trunk@43956 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/rest-api.php | 5 + .../class-wp-rest-autosaves-controller.php | 392 ++++++++++++++++++ wp-includes/version.php | 2 +- wp-settings.php | 1 + 4 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php diff --git a/wp-includes/rest-api.php b/wp-includes/rest-api.php index 5cd7ef8774..5ab3857cd2 100644 --- a/wp-includes/rest-api.php +++ b/wp-includes/rest-api.php @@ -193,6 +193,11 @@ function create_initial_rest_routes() { $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name ); $revisions_controller->register_routes(); } + + if ( 'attachment' !== $post_type->name ) { + $autosaves_controller = new WP_REST_Autosaves_Controller( $post_type->name ); + $autosaves_controller->register_routes(); + } } // Post types. diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php new file mode 100644 index 0000000000..79529213c3 --- /dev/null +++ b/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -0,0 +1,392 @@ +parent_post_type = $parent_post_type; + $post_type_object = get_post_type_object( $parent_post_type ); + + // Ensure that post type-specific controller logic is available. + $parent_controller_class = ! empty( $post_type_object->rest_controller_class ) ? $post_type_object->rest_controller_class : 'WP_REST_Posts_Controller'; + + $this->parent_controller = new $parent_controller_class( $post_type_object->name ); + $this->revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type ); + $this->rest_namespace = 'wp/v2'; + $this->rest_base = 'autosaves'; + $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; + } + + /** + * Registers routes for autosaves. + * + * @since 5.0.0 + * + * @see register_rest_route() + */ + public function register_routes() { + register_rest_route( + $this->rest_namespace, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The ID for the parent of the object.' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this->revisions_controller, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->rest_namespace, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The ID for the parent of the object.' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'The ID for the object.' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + } + + /** + * Get the parent post. + * + * @since 5.0.0 + * + * @param int $parent_id Supplied ID. + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent( $parent_id ) { + return $this->revisions_controller->get_parent( $parent_id ); + } + + /** + * Checks if a given request has access to create an autosave revision. + * + * Autosave revisions inherit permissions from the parent post, + * check if the current user has permission to edit the post. + * + * @since 5.0.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has access to create the item, WP_Error object otherwise. + */ + public function create_item_permissions_check( $request ) { + $id = $request->get_param( 'id' ); + if ( empty( $id ) ) { + return new WP_Error( 'rest_post_invalid_id', __( 'Invalid item ID.' ), array( 'status' => 404 ) ); + } + + return $this->parent_controller->update_item_permissions_check( $request ); + } + + /** + * Creates, updates or deletes an autosave revision. + * + * @since 5.0.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + + if ( ! defined( 'DOING_AUTOSAVE' ) ) { + define( 'DOING_AUTOSAVE', true ); + } + + $post = get_post( $request->get_param( 'id' ) ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + $prepared_post = $this->parent_controller->prepare_item_for_database( $request ); + $prepared_post->ID = $post->ID; + $user_id = get_current_user_id(); + + if ( ( 'draft' === $post->post_status || 'auto-draft' === $post->post_status ) && $post->post_author == $user_id ) { + // Draft posts for the same author: autosaving updates the post and does not create a revision. + // Convert the post object to an array and add slashes, wp_update_post expects escaped array. + $autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true ); + } else { + // Non-draft posts: create or update the post autosave. + $autosave_id = $this->create_post_autosave( (array) $prepared_post ); + } + + if ( is_wp_error( $autosave_id ) ) { + return $autosave_id; + } + + $autosave = get_post( $autosave_id ); + $request->set_param( 'context', 'edit' ); + + $response = $this->prepare_item_for_response( $autosave, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + + /** + * Get the autosave, if the ID is valid. + * + * @since 5.0.0 + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise. + */ + public function get_item( $request ) { + $parent_id = (int) $request->get_param( 'parent' ); + + if ( $parent_id <= 0 ) { + return new WP_Error( 'rest_post_invalid_id', __( 'Invalid parent post ID.' ), array( 'status' => 404 ) ); + } + + $autosave = wp_get_post_autosave( $parent_id ); + + if ( ! $autosave ) { + return new WP_Error( 'rest_post_no_autosave', __( 'There is no autosave revision for this post.' ), array( 'status' => 404 ) ); + } + + $response = $this->prepare_item_for_response( $autosave, $request ); + return $response; + } + + /** + * Gets a collection of autosaves using wp_get_post_autosave. + * + * Contains the user's autosave, for empty if it doesn't exist. + * + * @since 5.0.0 + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $parent = $this->get_parent( $request->get_param( 'parent' ) ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + $response = array(); + $parent_id = $parent->ID; + $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) ); + + foreach ( $revisions as $revision ) { + if ( false !== strpos( $revision->post_name, "{$parent_id}-autosave" ) ) { + $data = $this->prepare_item_for_response( $revision, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } + } + + return rest_ensure_response( $response ); + } + + + /** + * Retrieves the autosave's schema, conforming to JSON Schema. + * + * @since 5.0.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = $this->revisions_controller->get_item_schema(); + + $schema['properties']['preview_link'] = array( + 'description' => __( 'Preview link for the post.' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + + return $schema; + } + + /** + * Creates autosave for the specified post. + * + * From wp-admin/post.php. + * + * @since 5.0.0 + * + * @param mixed $post_data Associative array containing the post data. + * @return mixed The autosave revision ID or WP_Error. + */ + public function create_post_autosave( $post_data ) { + + $post_id = (int) $post_data['ID']; + $post = get_post( $post_id ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + $user_id = get_current_user_id(); + + // Store one autosave per author. If there is already an autosave, overwrite it. + $old_autosave = wp_get_post_autosave( $post_id, $user_id ); + + if ( $old_autosave ) { + $new_autosave = _wp_post_revision_data( $post_data, true ); + $new_autosave['ID'] = $old_autosave->ID; + $new_autosave['post_author'] = $user_id; + + // If the new autosave has the same content as the post, delete the autosave. + $autosave_is_different = false; + + foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) { + if ( normalize_whitespace( $new_autosave[ $field ] ) != normalize_whitespace( $post->$field ) ) { + $autosave_is_different = true; + break; + } + } + + if ( ! $autosave_is_different ) { + wp_delete_post_revision( $old_autosave->ID ); + return new WP_Error( 'rest_autosave_no_changes', __( 'There is nothing to save. The autosave and the post content are the same.' ), array( 'status' => 400 ) ); + } + + /** + * This filter is documented in wp-admin/post.php. + */ + do_action( 'wp_creating_autosave', $new_autosave ); + + // wp_update_post expects escaped array. + return wp_update_post( wp_slash( $new_autosave ) ); + } + + // Create the new autosave as a special post revision. + return _wp_put_post_revision( $post_data, true ); + } + + /** + * Prepares the revision for the REST response. + * + * @since 5.0.0 + * + * @param WP_Post $post Post revision object. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $post, $request ) { + + $response = $this->revisions_controller->prepare_item_for_response( $post, $request ); + + $fields = $this->get_fields_for_response( $request ); + + if ( in_array( 'preview_link', $fields, true ) ) { + $parent_id = wp_is_post_autosave( $post ); + $preview_post_id = false === $parent_id ? $post->ID : $parent_id; + $preview_query_args = array(); + + if ( false !== $parent_id ) { + $preview_query_args['preview_id'] = $parent_id; + $preview_query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $parent_id ); + } + + $response->data['preview_link'] = get_preview_post_link( $preview_post_id, $preview_query_args ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $response->data = $this->add_additional_fields_to_object( $response->data, $request ); + $response->data = $this->filter_response_by_context( $response->data, $context ); + + /** + * Filters a revision returned from the API. + * + * Allows modification of the revision right before it is returned. + * + * @since 5.0.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post The original revision object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'rest_prepare_autosave', $response, $post, $request ); + } +} diff --git a/wp-includes/version.php b/wp-includes/version.php index cc17b085b3..089e75457d 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.1-alpha-44125'; +$wp_version = '5.1-alpha-44126'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp-settings.php b/wp-settings.php index 87ca2d9e94..9b1d9f5445 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -230,6 +230,7 @@ require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-attachments-contro require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-types-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-statuses-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-revisions-controller.php' ); +require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-autosaves-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-taxonomies-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' );