From bfcbd4e66fb6c302b753495c592b7bb01ac30c1e Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 5 Jan 2022 01:59:46 +0000 Subject: [PATCH 1/4] First pass as post lock controller. --- lib/class-wp-rest-post-lock-controller.php | 455 +++++++++++++++++++++ lib/load.php | 3 + lib/rest-api.php | 19 + 3 files changed, 477 insertions(+) create mode 100644 lib/class-wp-rest-post-lock-controller.php diff --git a/lib/class-wp-rest-post-lock-controller.php b/lib/class-wp-rest-post-lock-controller.php new file mode 100644 index 0000000000000..8dbcd5b4d51d4 --- /dev/null +++ b/lib/class-wp-rest-post-lock-controller.php @@ -0,0 +1,455 @@ + true ); + + /** + * Constructor. + * + * @param string $post_type Post type. + */ + public function __construct( $post_type ) { + $this->post_type = $post_type; + $this->rest_base = 'lock'; + $post_type_object = get_post_type_object( $post_type ); + $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; + $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; + $this->parent_controller = $post_type_object->get_rest_controller(); + + if ( ! $this->parent_controller ) { + $this->parent_controller = new WP_REST_Posts_Controller( $post_type ); + } + } + + /** + * Registers the routes for post locking. + * + * @since 4.7.0 + * + * @see register_rest_route() + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'gutenberg' ), + '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::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::DELETABLE ), + ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves a single lock. + * + * @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 get_item( $request ) { + $lock = $this->get_lock( $request['id'] ); + if ( is_wp_error( $lock ) ) { + return $lock; + } + + $data = $this->prepare_item_for_response( $lock, $request ); + + return rest_ensure_response( $data ); + } + + /** + * Update post lock + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error Response object on success. + */ + public function update_item( $request ) { + if ( ! function_exists( 'wp_set_post_lock' ) ) { + require_once ABSPATH . 'wp-admin/includes/post.php'; + } + + $post_id = $request['id']; + + wp_set_post_lock( $post_id ); + $lock = $this->get_lock( $post_id ); + if ( is_wp_error( $lock ) ) { + return $lock; + } + + return $this->prepare_item_for_response( $lock, $request ); + } + + /** + * Delete post lock. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response Response object on success. + */ + public function delete_item( $request ) { + $post_id = $request['id']; + + $lock = $this->get_lock( $post_id ); + $data = array(); + if ( ! is_wp_error( $lock ) ) { + $previous = $this->prepare_item_for_response( $lock, $request ); + $data = $previous->get_data(); + } + + $result = delete_post_meta( $post_id, '_edit_lock' ); + $response = new WP_REST_Response(); + $response->set_data( + array( + 'deleted' => $result, + 'previous' => $data, + ) + ); + + return $response; + } + + /** + * Prepare a post lock it's output in an API response. + * + * @param array $item Post lock array. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response + */ + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + + // Base fields for every post. + $data = array(); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $item['post_id']; + } + + if ( rest_is_field_included( 'date', $fields ) ) { + $data['date'] = $this->prepare_date_response( $item['time'] ); + } + + if ( rest_is_field_included( 'author', $fields ) ) { + $data['author'] = (int) $item['user']; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + + /** + * Post locking filter. + * + * @param WP_REST_Response $response The response object. + * @param Array $item Lock object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "rest_prepare_lock_{$this->post_type}", $response, $item, $request ); + } + + /** + * Permission check. + * + * @param WP_REST_Request $request Request object. + * + * @return bool|WP_Error + */ + protected function permissions_check( $request ) { + if ( is_callable( array( $this->parent_controller, 'update_item_permissions_check' ) ) ) { + return $this->parent_controller->update_item_permissions_check( $request ); + } + + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( current_user_can( 'edit_post', $post->ID ) ) { + return new WP_Error( + 'rest_cannot_edit', + __( 'Sorry, you are not allowed to edit this post.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a given request has access to read a lock. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return true|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->permissions_check( $request ); + } + + /** + * Checks if a given request has access to update a lock. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. + */ + public function update_item_permissions_check( $request ) { + return $this->permissions_check( $request ); + } + + /** + * Checks if a given request has access to delete a lock. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. + */ + public function delete_item_permissions_check( $request ) { + $result = $this->permissions_check( $request ); + if ( is_wp_error( $result ) ) { + return $result; + } + + $post_id = $request['id']; + + $lock = $this->get_lock( $post_id ); + + if ( is_wp_error( $lock ) ) { + return $lock; + } + + if ( is_array( $lock ) && isset( $lock['user'] ) && get_current_user_id() !== (int) $lock['user'] ) { + return new WP_Error( + 'rest_cannot_delete_others_lock', + __( 'Sorry, you are not allowed delete others lock.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Prepares links for the request. + * + * @param array $lock Lock object. + * + * @return array Links for the given post. + */ + protected function prepare_links( $lock ) { + $self = sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $lock['post_id'], $this->rest_base ); + + // Entity meta. + $links = array( + 'self' => array( + 'href' => rest_url( $self ), + ), + 'author' => array( + 'href' => rest_url( 'wp/v2/users/' . $lock['user'] ), + 'embeddable' => true, + ), + 'up' => array( + 'href' => rest_url( rest_get_route_for_post( $lock['post_id'] ) ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * Get post lock. + * + * @param int $post_id Post id. + * + * @return array|WP_Error Post lock array or WP_Error. + */ + protected function get_lock( $post_id ) { + $post = $this->get_post( $post_id ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $lock = get_post_meta( $post->ID, '_edit_lock', true ); + if ( ! $lock ) { + return new WP_Error( + 'rest_invalid_lock', + __( 'Invalid lock.', 'gutenberg' ), + array( 'status' => 404 ) + ); + } + + $lock = explode( ':', $lock ); + $time = (int) $lock[0]; + $user = isset( $lock[1] ) ? $lock[1] : get_post_meta( $post->ID, '_edit_last', true ); + + if ( ! get_userdata( $user ) ) { + return new WP_Error( + 'rest_invalid_user', + __( 'Invalid user.', 'gutenberg' ), + array( 'status' => 404 ) + ); + } + + /** This filter is documented in wp-admin/includes/ajax-actions.php */ + $time_window = apply_filters( 'wp_check_post_lock_window', 150 ); + + if ( $time && $time > time() - $time_window ) { + return compact( 'time', 'user', 'post_id' ); + } + + return new WP_Error( + 'rest_expired_lock', + __( 'Expired lock', 'gutenberg' ), + array( 'status' => 404 ) + ); + } + + /** + * Get the post, if the ID is valid. + * + * @param int $id Supplied ID. + * + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_post( $id ) { + $error = new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.', 'gutenberg' ), + array( 'status' => 404 ) + ); + + if ( (int) $id <= 0 ) { + return $error; + } + + $post = get_post( (int) $id ); + if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return $error; + } + + return $post; + } + + /** + * Format date. + * + * @param string $time Time stamp. + * + * @return string + */ + protected function prepare_date_response( $time ) { + return mysql_to_rfc3339( gmdate( 'Y-m-d H:i:s', $time ) ); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved + } + + /** + * Retrieves the post's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'lock', + 'type' => 'object', + 'properties' => array( + 'date' => array( + 'description' => __( 'The date the lock expires', 'gutenberg' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'author' => array( + 'description' => __( 'The ID for the author of the lock.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/lib/load.php b/lib/load.php index 3c3b29ff3b29e..e2ad5228105fa 100644 --- a/lib/load.php +++ b/lib/load.php @@ -58,6 +58,9 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_REST_Menu_Locations_Controller' ) ) { require_once __DIR__ . '/class-wp-rest-menu-locations-controller.php'; } + if ( ! class_exists( 'WP_REST_Post_Lock_Controller' ) ) { + require_once __DIR__ . '/class-wp-rest-post-lock-controller.php'; + } if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { require_once __DIR__ . '/class-wp-rest-customizer-nonces.php'; } diff --git a/lib/rest-api.php b/lib/rest-api.php index 49f82e2eb4b0e..8c31d2eccccc6 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -10,6 +10,25 @@ die( 'Silence is golden.' ); } +/** + * Registers the REST API routes for Post locking. + */ +function gutenberg_register_post_locking_details_routes() { + foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { + $controller = $post_type->get_rest_controller(); + + if ( ! $controller ) { + continue; + } + + $controller->register_routes(); + + $lock_controller = new WP_REST_Post_Lock_Controller( $post_type->name ); + $lock_controller->register_routes(); + } +} + +add_action( 'rest_api_init', 'gutenberg_register_post_locking_details_routes' ); /** * Registers the REST API routes for URL Details. From 6e3b9590a80688f6d2f93adcc83cec7fb01de307 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 5 Jan 2022 06:08:36 +0000 Subject: [PATCH 2/4] Tweaks --- lib/class-wp-rest-post-lock-controller.php | 30 ++++++++-------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/class-wp-rest-post-lock-controller.php b/lib/class-wp-rest-post-lock-controller.php index 8dbcd5b4d51d4..2dc8f4be9399a 100644 --- a/lib/class-wp-rest-post-lock-controller.php +++ b/lib/class-wp-rest-post-lock-controller.php @@ -183,11 +183,10 @@ public function delete_item( $request ) { public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); - // Base fields for every post. $data = array(); if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = $item['post_id']; + $data['id'] = (int) $item['post_id']; } if ( rest_is_field_included( 'date', $fields ) ) { @@ -209,13 +208,15 @@ public function prepare_item_for_response( $item, $request ) { $response->add_links( $links ); /** - * Post locking filter. + * Filters a post lock before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * * @param WP_REST_Response $response The response object. - * @param Array $item Lock object. - * @param WP_REST_Request $request Request object. + * @param Array $item Lock object. + * @param WP_REST_Request $request Request object. */ - return apply_filters( "rest_prepare_lock_{$this->post_type}", $response, $item, $request ); + return apply_filters( "rest_prepare_{$this->post_type}_lock", $response, $item, $request ); } /** @@ -226,24 +227,12 @@ public function prepare_item_for_response( $item, $request ) { * @return bool|WP_Error */ protected function permissions_check( $request ) { - if ( is_callable( array( $this->parent_controller, 'update_item_permissions_check' ) ) ) { - return $this->parent_controller->update_item_permissions_check( $request ); - } - $post = $this->get_post( $request['id'] ); if ( is_wp_error( $post ) ) { return $post; } - if ( current_user_can( 'edit_post', $post->ID ) ) { - return new WP_Error( - 'rest_cannot_edit', - __( 'Sorry, you are not allowed to edit this post.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; + return $this->parent_controller->update_item_permissions_check( $request ); } /** @@ -351,7 +340,6 @@ protected function get_lock( $post_id ) { } $lock = explode( ':', $lock ); - $time = (int) $lock[0]; $user = isset( $lock[1] ) ? $lock[1] : get_post_meta( $post->ID, '_edit_last', true ); if ( ! get_userdata( $user ) ) { @@ -362,6 +350,8 @@ protected function get_lock( $post_id ) { ); } + $time = (int) $lock[0]; + /** This filter is documented in wp-admin/includes/ajax-actions.php */ $time_window = apply_filters( 'wp_check_post_lock_window', 150 ); From 5df7d8702868e4a8cba10a16a53134615c6f289e Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 5 Jan 2022 21:59:40 +0000 Subject: [PATCH 3/4] Add unit tests. --- ...lass-wp-rest-post-lock-controller-test.php | 422 ++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 phpunit/class-wp-rest-post-lock-controller-test.php diff --git a/phpunit/class-wp-rest-post-lock-controller-test.php b/phpunit/class-wp-rest-post-lock-controller-test.php new file mode 100644 index 0000000000000..ba27371458d08 --- /dev/null +++ b/phpunit/class-wp-rest-post-lock-controller-test.php @@ -0,0 +1,422 @@ +user->create( + array( + 'role' => 'contributor', + ) + ); + + self::$editor = $factory->user->create( + array( + 'role' => 'editor', + 'user_email' => 'editor@example.com', + ) + ); + } + + /** + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/wp/v2/posts/(?P[\d]+)/lock', $routes ); + $this->assertCount( 3, $routes['/wp/v2/posts/(?P[\d]+)/lock'] ); + } + + /** + * @covers ::get_context_param + */ + public function test_context_param() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/999/lock' ); + $response = rest_get_server()->dispatch( $request ); + $patterns = $response->get_data(); + + $this->assertSame( 'view', $patterns['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $patterns['endpoints'][0]['args']['context']['enum'] ); + } + + public function test_get_items() { + $this->markTestSkipped( 'Controller does not have get_items route.' ); + } + + /** + * @covers ::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$editor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + $request = new WP_REST_Request( WP_REST_Server::READABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_lock', $response, 404 ); + + $now = time(); + $user_id = self::$editor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( WP_REST_Server::READABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'author', $data ); + $this->assertArrayHasKey( 'id', $data ); + $this->assertArrayHasKey( 'date', $data ); + $this->assertSame( $data['id'], $post_id ); + $this->assertSame( $data['author'], $user_id ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'author', $links ); + } + + /** + * @covers ::get_item + */ + public function test_get_item_expired() { + wp_set_current_user( self::$editor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + + $now = time() - 1000; + $user_id = self::$editor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( WP_REST_Server::READABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_expired_lock', $response, 404 ); + } + + /** + * @covers ::get_item + * @covers ::prepare_item_for_response + * @covers ::get_item_permissions_check + */ + public function test_get_item_no_post() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( WP_REST_Server::READABLE, '/wp/v2/posts/99999/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers ::create_item + */ + public function test_create_item() { + wp_set_current_user( self::$editor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + $request = new WP_REST_Request( WP_REST_Server::CREATABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'author', $data ); + $this->assertArrayHasKey( 'id', $data ); + $this->assertArrayHasKey( 'date', $data ); + $this->assertSame( $data['id'], $post_id ); + $this->assertSame( $data['author'], self::$editor ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'author', $links ); + } + + /** + * @covers ::update_item + */ + public function test_create_item_invalid_user() { + wp_set_current_user( self::$contributor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + + $request = new WP_REST_Request( WP_REST_Server::CREATABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + + /** + * @covers ::update_item + */ + public function test_create_item_no_user() { + wp_set_current_user( 0 ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + + $request = new WP_REST_Request( WP_REST_Server::CREATABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + } + + /** + * @covers ::create_item + */ + public function test_create_item_no_post() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( WP_REST_Server::CREATABLE, '/wp/v2/posts/99999/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers ::update_item + */ + public function test_update_item() { + wp_set_current_user( self::$editor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + $now = time(); + $user_id = self::$editor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'author', $data ); + $this->assertArrayHasKey( 'id', $data ); + $this->assertArrayHasKey( 'date', $data ); + $this->assertSame( $data['id'], $post_id ); + $this->assertSame( $data['author'], self::$editor ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'author', $links ); + } + + /** + * @covers ::update_item + */ + public function test_update_item_invalid_user() { + wp_set_current_user( self::$contributor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + $now = time(); + $user_id = self::$editor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + + /** + * @covers ::update_item + */ + public function test_update_item_no_user() { + wp_set_current_user( 0 ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + $now = time(); + $user_id = self::$editor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + } + + /** + * @covers ::update_item + */ + public function test_update_item_no_post() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/posts/99999/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers ::delete_item + */ + public function test_delete_item() { + wp_set_current_user( self::$editor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + $now = time(); + $user_id = self::$editor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( WP_REST_Server::DELETABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'deleted', $data ); + $this->assertArrayHasKey( 'previous', $data ); + $this->assertArrayHasKey( 'id', $data['previous'] ); + $this->assertTrue( $data['deleted'] ); + } + + /** + * @covers ::delete_item + */ + public function test_delete_item_different_user() { + wp_set_current_user( self::$editor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$contributor, + ) + ); + $now = time(); + $user_id = self::$contributor; + $lock = "$now:$user_id"; + + update_post_meta( $post_id, '_edit_lock', $lock ); + + $request = new WP_REST_Request( WP_REST_Server::DELETABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_delete_others_lock', $response, 403 ); + } + + /** + * @covers ::delete_item + */ + public function test_delete_item_no_post() { + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( WP_REST_Server::DELETABLE, '/wp/v2/posts/99999/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers ::delete_item + */ + public function test_delete_item_invalid_user() { + wp_set_current_user( self::$contributor ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + + $request = new WP_REST_Request( WP_REST_Server::DELETABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + + /** + * @covers ::delete_item + */ + public function test_delete_item_no_user() { + wp_set_current_user( 0 ); + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor, + ) + ); + + $request = new WP_REST_Request( WP_REST_Server::DELETABLE, '/wp/v2/posts/' . $post_id . '/lock' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + } + + public function test_prepare_item() { + $this->markTestSkipped( 'Controller does not implement prepare_item().' ); + } + + + /** + * @covers ::get_item_schema + */ + public function test_get_item_schema() { + wp_set_current_user( self::$editor ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/99999/lock' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 3, $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date', $properties ); + $this->assertArrayHasKey( 'author', $properties ); + } +} From 0b7350c63202c41b76c689b613d397ba2cc64c95 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 16 Mar 2022 23:12:32 +0000 Subject: [PATCH 4/4] Apply suggestions from code review --- lib/class-wp-rest-post-lock-controller.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/class-wp-rest-post-lock-controller.php b/lib/class-wp-rest-post-lock-controller.php index 2dc8f4be9399a..4de08230a2327 100644 --- a/lib/class-wp-rest-post-lock-controller.php +++ b/lib/class-wp-rest-post-lock-controller.php @@ -61,7 +61,6 @@ public function __construct( $post_type ) { /** * Registers the routes for post locking. * - * @since 4.7.0 * * @see register_rest_route() */