wp/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php
changeset 7 cf61fcea0001
child 9 177826044cd9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wp/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php	Mon Oct 14 17:39:30 2019 +0200
@@ -0,0 +1,565 @@
+<?php
+/**
+ * REST API: WP_REST_Revisions_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 4.7.0
+ */
+
+/**
+ * Core class used to access revisions via the REST API.
+ *
+ * @since 4.7.0
+ *
+ * @see WP_REST_Controller
+ */
+class WP_REST_Revisions_Controller extends WP_REST_Controller {
+
+	/**
+	 * Parent post type.
+	 *
+	 * @since 4.7.0
+	 * @var string
+	 */
+	private $parent_post_type;
+
+	/**
+	 * Parent controller.
+	 *
+	 * @since 4.7.0
+	 * @var WP_REST_Controller
+	 */
+	private $parent_controller;
+
+	/**
+	 * The base of the parent controller's route.
+	 *
+	 * @since 4.7.0
+	 * @var string
+	 */
+	private $parent_base;
+
+	/**
+	 * Constructor.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param string $parent_post_type Post type of the parent.
+	 */
+	public function __construct( $parent_post_type ) {
+		$this->parent_post_type = $parent_post_type;
+		$this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
+		$this->namespace = 'wp/v2';
+		$this->rest_base = 'revisions';
+		$post_type_object = get_post_type_object( $parent_post_type );
+		$this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
+	}
+
+	/**
+	 * Registers routes for revisions based on post types supporting revisions.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @see register_rest_route()
+	 */
+	public function register_routes() {
+
+		register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\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, 'get_items_permissions_check' ),
+				'args'                => $this->get_collection_params(),
+			),
+			'schema' => array( $this, 'get_public_item_schema' ),
+		) );
+
+		register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
+			'args' => array(
+				'parent' => array(
+					'description' => __( 'The ID for the parent of the object.' ),
+					'type'        => 'integer',
+				),
+				'id' => array(
+					'description' => __( 'Unique identifier for the object.' ),
+					'type'        => 'integer',
+				),
+			),
+			array(
+				'methods'             => WP_REST_Server::READABLE,
+				'callback'            => array( $this, 'get_item' ),
+				'permission_callback' => array( $this, 'get_item_permissions_check' ),
+				'args'                => array(
+					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+				),
+			),
+			array(
+				'methods'             => WP_REST_Server::DELETABLE,
+				'callback'            => array( $this, 'delete_item' ),
+				'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+				'args'                => array(
+					'force' => array(
+						'type'        => 'boolean',
+						'default'     => false,
+						'description' => __( 'Required to be true, as revisions do not support trashing.' ),
+					),
+				),
+			),
+			'schema' => array( $this, 'get_public_item_schema' ),
+		));
+
+	}
+
+	/**
+	 * Get the parent post, if the ID is valid.
+	 *
+	 * @since 4.7.2
+	 *
+	 * @param int $id Supplied ID.
+	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
+	 */
+	protected function get_parent( $parent ) {
+		$error = new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent ID.' ), array( 'status' => 404 ) );
+		if ( (int) $parent <= 0 ) {
+			return $error;
+		}
+
+		$parent = get_post( (int) $parent );
+		if ( empty( $parent ) || empty( $parent->ID ) || $this->parent_post_type !== $parent->post_type ) {
+			return $error;
+		}
+
+		return $parent;
+	}
+
+	/**
+	 * Checks if a given request has access to get revisions.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+	 */
+	public function get_items_permissions_check( $request ) {
+		$parent = $this->get_parent( $request['parent'] );
+		if ( is_wp_error( $parent ) ) {
+			return $parent;
+		}
+
+		$parent_post_type_obj = get_post_type_object( $parent->post_type );
+		if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
+			return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Get the revision, if the ID is valid.
+	 *
+	 * @since 4.7.2
+	 *
+	 * @param int $id Supplied ID.
+	 * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
+	 */
+	protected function get_revision( $id ) {
+		$error = new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision ID.' ), array( 'status' => 404 ) );
+		if ( (int) $id <= 0 ) {
+			return $error;
+		}
+
+		$revision = get_post( (int) $id );
+		if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) {
+			return $error;
+		}
+
+		return $revision;
+	}
+
+	/**
+	 * Gets a collection of revisions.
+	 *
+	 * @since 4.7.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['parent'] );
+		if ( is_wp_error( $parent ) ) {
+			return $parent;
+		}
+
+		$revisions = wp_get_post_revisions( $request['parent'] );
+
+		$response = array();
+		foreach ( $revisions as $revision ) {
+			$data = $this->prepare_item_for_response( $revision, $request );
+			$response[] = $this->prepare_response_for_collection( $data );
+		}
+		return rest_ensure_response( $response );
+	}
+
+	/**
+	 * Checks if a given request has access to get a specific revision.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param WP_REST_Request $request Full data about the request.
+	 * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
+	 */
+	public function get_item_permissions_check( $request ) {
+		return $this->get_items_permissions_check( $request );
+	}
+
+	/**
+	 * Retrieves one revision from the collection.
+	 *
+	 * @since 4.7.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_item( $request ) {
+		$parent = $this->get_parent( $request['parent'] );
+		if ( is_wp_error( $parent ) ) {
+			return $parent;
+		}
+
+		$revision = $this->get_revision( $request['id'] );
+		if ( is_wp_error( $revision ) ) {
+			return $revision;
+		}
+
+		$response = $this->prepare_item_for_response( $revision, $request );
+		return rest_ensure_response( $response );
+	}
+
+	/**
+	 * Checks if a given request has access to delete a revision.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param  WP_REST_Request $request Full details about the request.
+	 * @return bool|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
+	 */
+	public function delete_item_permissions_check( $request ) {
+		$parent = $this->get_parent( $request['parent'] );
+		if ( is_wp_error( $parent ) ) {
+			return $parent;
+		}
+
+		$revision = $this->get_revision( $request['id'] );
+		if ( is_wp_error( $revision ) ) {
+			return $revision;
+		}
+
+		$response = $this->get_items_permissions_check( $request );
+		if ( ! $response || is_wp_error( $response ) ) {
+			return $response;
+		}
+
+		$post_type = get_post_type_object( 'revision' );
+		return current_user_can( $post_type->cap->delete_post, $revision->ID );
+	}
+
+	/**
+	 * Deletes a single revision.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @return true|WP_Error True on success, or WP_Error object on failure.
+	 */
+	public function delete_item( $request ) {
+		$revision = $this->get_revision( $request['id'] );
+		if ( is_wp_error( $revision ) ) {
+			return $revision;
+		}
+
+		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
+
+		// We don't support trashing for revisions.
+		if ( ! $force ) {
+			/* translators: %s: force=true */
+			return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Revisions do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
+		}
+
+		$previous = $this->prepare_item_for_response( $revision, $request );
+
+		$result = wp_delete_post( $request['id'], true );
+
+		/**
+		 * Fires after a revision is deleted via the REST API.
+		 *
+		 * @since 4.7.0
+		 *
+		 * @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
+		 *                        or false (failure). If the revision was moved to the trash, $result represents
+		 *                        its new state; if it was deleted, $result represents its state before deletion.
+		 * @param WP_REST_Request $request The request sent to the API.
+		 */
+		do_action( 'rest_delete_revision', $result, $request );
+
+		if ( ! $result ) {
+			return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
+		}
+
+		$response = new WP_REST_Response();
+		$response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
+		return $response;
+	}
+
+	/**
+	 * Prepares the revision for the REST response.
+	 *
+	 * @since 4.7.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 ) {
+		$GLOBALS['post'] = $post;
+
+		setup_postdata( $post );
+
+		$fields = $this->get_fields_for_response( $request );
+		$data   = array();
+
+		if ( in_array( 'author', $fields, true ) ) {
+			$data['author'] = (int) $post->post_author;
+		}
+
+		if ( in_array( 'date', $fields, true ) ) {
+			$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
+		}
+
+		if ( in_array( 'date_gmt', $fields, true ) ) {
+			$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
+		}
+
+		if ( in_array( 'id', $fields, true ) ) {
+			$data['id'] = $post->ID;
+		}
+
+		if ( in_array( 'modified', $fields, true ) ) {
+			$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
+		}
+
+		if ( in_array( 'modified_gmt', $fields, true ) ) {
+			$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
+		}
+
+		if ( in_array( 'parent', $fields, true ) ) {
+			$data['parent'] = (int) $post->post_parent;
+		}
+
+		if ( in_array( 'slug', $fields, true ) ) {
+			$data['slug'] = $post->post_name;
+		}
+
+		if ( in_array( 'guid', $fields, true ) ) {
+			$data['guid'] = array(
+				/** This filter is documented in wp-includes/post-template.php */
+				'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
+				'raw'      => $post->guid,
+			);
+		}
+
+		if ( in_array( 'title', $fields, true ) ) {
+			$data['title'] = array(
+				'raw'      => $post->post_title,
+				'rendered' => get_the_title( $post->ID ),
+			);
+		}
+
+		if ( in_array( 'content', $fields, true ) ) {
+
+			$data['content'] = array(
+				'raw'      => $post->post_content,
+				/** This filter is documented in wp-includes/post-template.php */
+				'rendered' => apply_filters( 'the_content', $post->post_content ),
+			);
+		}
+
+		if ( in_array( 'excerpt', $fields, true ) ) {
+			$data['excerpt'] = array(
+				'raw'      => $post->post_excerpt,
+				'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
+			);
+		}
+
+		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+		$data = $this->add_additional_fields_to_object( $data, $request );
+		$data = $this->filter_response_by_context( $data, $context );
+		$response = rest_ensure_response( $data );
+
+		if ( ! empty( $data['parent'] ) ) {
+			$response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
+		}
+
+		/**
+		 * Filters a revision returned from the API.
+		 *
+		 * Allows modification of the revision right before it is returned.
+		 *
+		 * @since 4.7.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_revision', $response, $post, $request );
+	}
+
+	/**
+	 * Checks the post_date_gmt or modified_gmt and prepare any post or
+	 * modified date for single post output.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param string      $date_gmt GMT publication time.
+	 * @param string|null $date     Optional. Local publication time. Default null.
+	 * @return string|null ISO8601/RFC3339 formatted datetime, otherwise null.
+	 */
+	protected function prepare_date_response( $date_gmt, $date = null ) {
+		if ( '0000-00-00 00:00:00' === $date_gmt ) {
+			return null;
+		}
+
+		if ( isset( $date ) ) {
+			return mysql_to_rfc3339( $date );
+		}
+
+		return mysql_to_rfc3339( $date_gmt );
+	}
+
+	/**
+	 * Retrieves the revision's schema, conforming to JSON Schema.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @return array Item schema data.
+	 */
+	public function get_item_schema() {
+		$schema = array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => "{$this->parent_post_type}-revision",
+			'type'       => 'object',
+			// Base properties for every Revision.
+			'properties' => array(
+				'author'          => array(
+					'description' => __( 'The ID for the author of the object.' ),
+					'type'        => 'integer',
+					'context'     => array( 'view', 'edit', 'embed' ),
+				),
+				'date'            => array(
+					'description' => __( "The date the object was published, in the site's timezone." ),
+					'type'        => 'string',
+					'format'      => 'date-time',
+					'context'     => array( 'view', 'edit', 'embed' ),
+				),
+				'date_gmt'        => array(
+					'description' => __( 'The date the object was published, as GMT.' ),
+					'type'        => 'string',
+					'format'      => 'date-time',
+					'context'     => array( 'view', 'edit' ),
+				),
+				'guid'            => array(
+					'description' => __( 'GUID for the object, as it exists in the database.' ),
+					'type'        => 'string',
+					'context'     => array( 'view', 'edit' ),
+				),
+				'id'              => array(
+					'description' => __( 'Unique identifier for the object.' ),
+					'type'        => 'integer',
+					'context'     => array( 'view', 'edit', 'embed' ),
+				),
+				'modified'        => array(
+					'description' => __( "The date the object was last modified, in the site's timezone." ),
+					'type'        => 'string',
+					'format'      => 'date-time',
+					'context'     => array( 'view', 'edit' ),
+				),
+				'modified_gmt'    => array(
+					'description' => __( 'The date the object was last modified, as GMT.' ),
+					'type'        => 'string',
+					'format'      => 'date-time',
+					'context'     => array( 'view', 'edit' ),
+				),
+				'parent'          => array(
+					'description' => __( 'The ID for the parent of the object.' ),
+					'type'        => 'integer',
+					'context'     => array( 'view', 'edit', 'embed' ),
+					),
+				'slug'            => array(
+					'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
+					'type'        => 'string',
+					'context'     => array( 'view', 'edit', 'embed' ),
+				),
+			),
+		);
+
+		$parent_schema = $this->parent_controller->get_item_schema();
+
+		if ( ! empty( $parent_schema['properties']['title'] ) ) {
+			$schema['properties']['title'] = $parent_schema['properties']['title'];
+		}
+
+		if ( ! empty( $parent_schema['properties']['content'] ) ) {
+			$schema['properties']['content'] = $parent_schema['properties']['content'];
+		}
+
+		if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
+			$schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
+		}
+
+		if ( ! empty( $parent_schema['properties']['guid'] ) ) {
+			$schema['properties']['guid'] = $parent_schema['properties']['guid'];
+		}
+
+		return $this->add_additional_fields_schema( $schema );
+	}
+
+	/**
+	 * Retrieves the query params for collections.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @return array Collection parameters.
+	 */
+	public function get_collection_params() {
+		return array(
+			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+		);
+	}
+
+	/**
+	 * Checks the post excerpt and prepare it for single post output.
+	 *
+	 * @since 4.7.0
+	 *
+	 * @param string  $excerpt The post excerpt.
+	 * @param WP_Post $post    Post revision object.
+	 * @return string Prepared excerpt or empty string.
+	 */
+	protected function prepare_excerpt_response( $excerpt, $post ) {
+
+		/** This filter is documented in wp-includes/post-template.php */
+		$excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
+
+		if ( empty( $excerpt ) ) {
+			return '';
+		}
+
+		return $excerpt;
+	}
+}