diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 08cba90a23..5dda1a9360 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -11,6 +11,15 @@ * Base Global Styles REST API Controller. */ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { + + /** + * Post type. + * + * @since 5.9.0 + * @var string + */ + protected $post_type; + /** * Constructor. * @since 5.9.0 @@ -18,6 +27,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { public function __construct() { $this->namespace = 'wp/v2'; $this->rest_base = 'global-styles'; + $this->post_type = 'wp_global_styles'; } /** @@ -75,22 +85,32 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { } /** - * Checks if the user has permissions to make the request. + * Checks if a given request has access to read a single global style. * * @since 5.9.0 * + * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ - protected function permissions_check() { - // Verify if the current user has edit_theme_options capability. - // This capability is required to edit/view/delete templates. - if ( ! current_user_can( 'edit_theme_options' ) ) { + public function get_item_permissions_check( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { return new WP_Error( - 'rest_cannot_manage_global_styles', - __( 'Sorry, you are not allowed to access the global styles on this site.' ), - array( - 'status' => rest_authorization_required_code(), - ) + 'rest_forbidden_context', + __( 'Sorry, you are not allowed to edit this global style.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( ! $this->check_read_permission( $post ) ) { + return new WP_Error( + 'rest_cannot_view', + __( 'Sorry, you are not allowed to view this global style.' ), + array( 'status' => rest_authorization_required_code() ) ); } @@ -98,13 +118,15 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { } /** - * Checks if a given request has access to read a single global styles config. + * Checks if a global style can be read. * - * @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. + * @since 5.9.0 + * + * @param WP_Post $post Post object. + * @return bool Whether the post can be read. */ - public function get_item_permissions_check( $request ) { - return $this->permissions_check( $request ); + protected function check_read_permission( $post ) { + return current_user_can( 'read_post', $post->ID ); } /** @@ -117,9 +139,9 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { - $post = get_post( $request['id'] ); - if ( ! $post || 'wp_global_styles' !== $post->post_type ) { - return new WP_Error( 'rest_global_styles_not_found', __( 'No global styles config exist with that id.' ), array( 'status' => 404 ) ); + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; } return $this->prepare_item_for_response( $post, $request ); @@ -134,7 +156,32 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. */ public function update_item_permissions_check( $request ) { - return $this->permissions_check( $request ); + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( $post && ! $this->check_update_permission( $post ) ) { + return new WP_Error( + 'rest_cannot_edit', + __( 'Sorry, you are not allowed to edit this global style.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a global style can be edited. + * + * @since 5.9.0 + * + * @param WP_Post $post Post object. + * @return bool Whether the post can be edited. + */ + protected function check_update_permission( $post ) { + return current_user_can( 'edit_post', $post->ID ); } /** @@ -146,9 +193,9 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function update_item( $request ) { - $post_before = get_post( $request['id'] ); - if ( ! $post_before || 'wp_global_styles' !== $post_before->post_type ) { - return new WP_Error( 'rest_global_styles_not_found', __( 'No global styles config exist with that id.' ), array( 'status' => 404 ) ); + $post_before = $this->get_post( $request['id'] ); + if ( is_wp_error( $post_before ) ) { + return $post_before; } $changes = $this->prepare_item_for_database( $request ); @@ -289,6 +336,34 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { return $response; } + /** + * Get the post, if the ID is valid. + * + * @since 5.9.0 + * + * @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_global_styles_not_found', + __( 'No global styles config exist with that id.' ), + array( 'status' => 404 ) + ); + + $id = (int) $id; + if ( $id <= 0 ) { + return $error; + } + + $post = get_post( $id ); + if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return $error; + } + + return $post; + } + /** * Prepares links for the request. @@ -323,7 +398,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { protected function get_available_actions() { $rels = array(); - $post_type = get_post_type_object( 'wp_global_styles' ); + $post_type = get_post_type_object( $this->post_type ); if ( current_user_can( $post_type->cap->publish_posts ) ) { $rels[] = 'https://api.w.org/action-publish'; } @@ -371,7 +446,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'wp_global_styles', + 'title' => $this->post_type, 'type' => 'object', 'properties' => array( 'id' => array( @@ -426,7 +501,19 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ public function get_theme_item_permissions_check( $request ) { - return $this->permissions_check( $request ); + // Verify if the current user has edit_theme_options capability. + // This capability is required to edit/view/delete templates. + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'rest_cannot_manage_global_styles', + __( 'Sorry, you are not allowed to access the global styles on this site.' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + return true; } /** diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index 356380ec85..e99232985b 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -7,6 +7,7 @@ */ /** + * @covers WP_REST_Global_Styles_Controller * @group restapi-global-styles * @group restapi */ @@ -16,11 +17,21 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test */ protected static $admin_id; + /** + * @var int + */ + protected static $subscriber_id; + /** * @var int */ protected static $global_styles_id; + /** + * @var int + */ + protected static $post_id; + private function find_and_normalize_global_styles_by_id( $global_styles, $id ) { foreach ( $global_styles as $style ) { if ( $style['id'] === $id ) { @@ -48,8 +59,15 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test 'role' => 'administrator', ) ); + + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + // This creates the global styles for the current theme. - self::$global_styles_id = wp_insert_post( + self::$global_styles_id = $factory->post->create( array( 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', 'post_status' => 'publish', @@ -59,25 +77,147 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test 'tax_input' => array( 'wp_theme' => 'tt1-blocks', ), - ), - true + ) ); + + self::$post_id = $factory->post->create(); } + /** + * + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$subscriber_id ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::register_routes + */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/global-styles/(?P[\/\w-]+)', $routes ); + $this->assertCount( 2, $routes['/wp/v2/global-styles/(?P[\/\w-]+)'] ); + $this->assertArrayHasKey( '/wp/v2/global-styles/themes/(?P[^.\/]+(?:\/[^.\/]+)?)', $routes ); + $this->assertCount( 1, $routes['/wp/v2/global-styles/themes/(?P[^.\/]+(?:\/[^.\/]+)?)'] ); } public function test_context_param() { - // TODO: Implement test_context_param() method. - $this->markTestIncomplete(); + $this->markTestSkipped( 'Controller does not implement context_param().' ); } public function test_get_items() { - $this->markTestIncomplete(); + $this->markTestSkipped( 'Controller does not implement get_items().' ); } + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54516 + */ + public function test_get_theme_item_no_user() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54516 + */ + public function test_get_theme_item_permission_check() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 403 ); + } + + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54516 + */ + public function test_get_theme_item_invalid() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/invalid' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_theme_not_found', $response, 404 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + */ + public function test_get_theme_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['_links'] ); + + $this->assertArrayHasKey( 'settings', $data ); + $this->assertArrayHasKey( 'styles', $data ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_no_user() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_invalid_post() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_global_styles_not_found', $response, 404 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_permission_check() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_no_user_edit() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_permission_check_edit() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + */ public function test_get_item() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); @@ -100,9 +240,13 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test } public function test_create_item() { - $this->markTestIncomplete(); + $this->markTestSkipped( 'Controller does not implement create_item().' ); } + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ public function test_update_item() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); @@ -116,17 +260,61 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test $this->assertEquals( 'My new global styles title', $data['title']['raw'] ); } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item_no_user() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item_invalid_post() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_global_styles_not_found', $response, 404 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item_permission_check() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + public function test_delete_item() { - $this->markTestIncomplete(); + $this->markTestSkipped( 'Controller does not implement delete_item().' ); } public function test_prepare_item() { - // TODO: Implement test_prepare_item() method. - $this->markTestIncomplete(); + $this->markTestSkipped( 'Controller does not implement prepare_item().' ); } + /** + * @covers WP_REST_Global_Styles_Controller::get_item_schema + * @ticket 54516 + */ public function test_get_item_schema() { - // TODO: Implement test_get_item_schema() method. - $this->markTestIncomplete(); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties, 'Schema properties array does not have exactly 4 elements' ); + $this->assertArrayHasKey( 'id', $properties, 'Schema properties array does not have "id" key' ); + $this->assertArrayHasKey( 'styles', $properties, 'Schema properties array does not have "styles" key' ); + $this->assertArrayHasKey( 'settings', $properties, 'Schema properties array does not have "settings" key' ); + $this->assertArrayHasKey( 'title', $properties, 'Schema properties array does not have "title" key' ); } }