From f4a8d92a8595870b63395de8fe6faf94840fc03d Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 29 Jan 2018 17:16:13 +1100 Subject: [PATCH 1/3] Add capabilities for reusable blocks Adds capabilities for creating, reading, updating and deleting reusable blocks. The capabilities are mapped like so: | | Editors^ | Authors | Contributors | Subscribers* | | ------ | -------- | ------- | ------------ | ------------ | | Create | Yes | Yes | No | No | | Read | Yes | Yes | Yes | No | | Update | Yes | Own | Own | No | | Delete | Yes | Own | Own | No | ^ Includes administrators. * Includes visitors that are not logged in. --- lib/class-wp-rest-blocks-controller.php | 73 ++++++----- lib/register.php | 39 +++++- phpunit/class-rest-blocks-controller-test.php | 122 ++++++++++++++++++ 3 files changed, 200 insertions(+), 34 deletions(-) diff --git a/lib/class-wp-rest-blocks-controller.php b/lib/class-wp-rest-blocks-controller.php index 2b19e2a5f0b0e7..1e656641c73959 100644 --- a/lib/class-wp-rest-blocks-controller.php +++ b/lib/class-wp-rest-blocks-controller.php @@ -16,6 +16,39 @@ * @see WP_REST_Controller */ class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller { + /** + * Checks if a block can be read. + * + * @since 2.1.0 + * + * @param object $post Post object that backs the block. + * @return bool Whether the block can be read. + */ + public function check_read_permission( $post ) { + // Ensure that the user is logged in and has the read_blocks capability. + $post_type = get_post_type_object( $post->post_type ); + if ( ! current_user_can( $post_type->cap->read_post, $post->ID ) ) { + return false; + } + + return parent::check_read_permission( $post ); + } + + /** + * Handle a DELETE request. + * + * @since 1.10.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 delete_item( $request ) { + // Always hard-delete a block. + $request->set_param( 'force', true ); + + return parent::delete_item( $request ); + } + /** * Given an update or create request, build the post object that is saved to * the database. @@ -25,33 +58,22 @@ class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller { * @param WP_REST_Request $request Request object. * @return stdClass|WP_Error Post object or WP_Error. */ - protected function prepare_item_for_database( $request ) { - $prepared_post = new stdClass; - - if ( isset( $request['id'] ) ) { - $existing_post = $this->get_post( $request['id'] ); - if ( is_wp_error( $existing_post ) ) { - return $existing_post; - } - - $prepared_post->ID = $existing_post->ID; - } + public function prepare_item_for_database( $request ) { + $prepared_post = parent::prepare_item_for_database( $request ); - $prepared_post->post_title = $request['title']; - $prepared_post->post_content = $request['content']; - $prepared_post->post_type = $this->post_type; - $prepared_post->post_status = 'publish'; + // Force blocks to always be published. + $prepared_post->post_status = 'publish'; - return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request ); + return $prepared_post; } /** - * Given a post from the database, build the array that is returned from an + * Given a block from the database, build the array that is returned from an * API response. * * @since 1.10.0 * - * @param WP_Post $post Post object. + * @param WP_Post $post Post object that backs the block. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ @@ -67,21 +89,6 @@ public function prepare_item_for_response( $post, $request ) { return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request ); } - /** - * Handle a DELETE request. - * - * @since 1.10.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 delete_item( $request ) { - // Always hard-delete a block. - $request->set_param( 'force', true ); - - return parent::delete_item( $request ); - } - /** * Builds the block's schema, conforming to JSON Schema. * diff --git a/lib/register.php b/lib/register.php index 56f0d809d9b9fa..209d47b3810ca3 100644 --- a/lib/register.php +++ b/lib/register.php @@ -401,11 +401,48 @@ function gutenberg_register_post_types() { 'singular_name' => 'Block', ), 'public' => false, - 'capability_type' => 'post', 'show_in_rest' => true, 'rest_base' => 'blocks', 'rest_controller_class' => 'WP_REST_Blocks_Controller', + 'capability_type' => 'block', + 'capabilities' => array( + 'read' => 'read_blocks', + 'create_posts' => 'create_blocks', + ), + 'map_meta_cap' => true, ) ); + + foreach ( array( 'administrator', 'editor' ) as $role_name ) { + $editor = get_role( $role_name ); + $editor->add_cap( 'edit_blocks' ); + $editor->add_cap( 'edit_others_blocks' ); + $editor->add_cap( 'publish_blocks' ); + $editor->add_cap( 'read_private_blocks' ); + $editor->add_cap( 'read_blocks' ); + $editor->add_cap( 'delete_blocks' ); + $editor->add_cap( 'delete_private_blocks' ); + $editor->add_cap( 'delete_published_blocks' ); + $editor->add_cap( 'delete_others_blocks' ); + $editor->add_cap( 'edit_private_blocks' ); + $editor->add_cap( 'edit_published_blocks' ); + $editor->add_cap( 'create_blocks' ); + } + + $author = get_role( 'author' ); + $author->add_cap( 'edit_blocks' ); + $author->add_cap( 'publish_blocks' ); + $author->add_cap( 'read_blocks' ); + $author->add_cap( 'delete_blocks' ); + $author->add_cap( 'delete_published_blocks' ); + $author->add_cap( 'edit_published_blocks' ); + $author->add_cap( 'create_blocks' ); + + $contributor = get_role( 'contributor' ); + $contributor->add_cap( 'edit_blocks' ); + $contributor->add_cap( 'read_blocks' ); + $contributor->add_cap( 'delete_blocks' ); + $contributor->add_cap( 'delete_published_blocks' ); + $contributor->add_cap( 'edit_published_blocks' ); } add_action( 'init', 'gutenberg_register_post_types' ); diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php index d45071adb39f67..3fa19b73905621 100644 --- a/phpunit/class-rest-blocks-controller-test.php +++ b/phpunit/class-rest-blocks-controller-test.php @@ -197,6 +197,128 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'content', $properties ); } + /** + * Test cases for test_capabilities(). + */ + public function data_capabilities() { + return array( + array( 'create', 'editor', 201 ), + array( 'create', 'author', 201 ), + array( 'create', 'contributor', 403 ), + array( 'create', null, 401 ), + + array( 'read', 'editor', 200 ), + array( 'read', 'author', 200 ), + array( 'read', 'contributor', 200 ), + array( 'read', null, 401 ), + + array( 'update_delete_own', 'editor', 200 ), + array( 'update_delete_own', 'author', 200 ), + array( 'update_delete_own', 'contributor', 200 ), + + array( 'update_delete_others', 'editor', 200 ), + array( 'update_delete_others', 'author', 403 ), + array( 'update_delete_others', 'contributor', 403 ), + array( 'update_delete_others', null, 401 ), + ); + } + + /** + * Exhaustively check that each role either can or cannot create, edit, + * update, and delete reusable blocks. + * + * @dataProvider data_capabilities + */ + public function test_capabilities( $action, $role, $expected_status ) { + if ( $role ) { + $user_id = $this->factory->user->create( array( 'role' => $role ) ); + wp_set_current_user( $user_id ); + } else { + wp_set_current_user( 0 ); + } + + switch ( $action ) { + case 'create': + $request = new WP_REST_Request( 'POST', '/wp/v2/blocks' ); + $request->set_body_params( + array( + 'title' => 'Test', + 'content' => '

Test

', + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + break; + + case 'read': + $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + break; + + case 'update_delete_own': + $post_id = wp_insert_post( + array( + 'post_type' => 'wp_block', + 'post_status' => 'publish', + 'post_title' => 'My cool block', + 'post_content' => '

Hello!

', + 'post_author' => $user_id, + ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . $post_id ); + $request->set_body_params( + array( + 'title' => 'Test', + 'content' => '

Test

', + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + wp_delete_post( $post_id ); + + break; + + case 'update_delete_others': + $request = new WP_REST_Request( 'PUT', '/wp/v2/blocks/' . self::$post_id ); + $request->set_body_params( + array( + 'title' => 'Test', + 'content' => '

Test

', + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + break; + + default: + $this->fail( "'$action' is not a valid action." ); + } + + if ( isset( $user_id ) ) { + self::delete_user( $user_id ); + } + } + public function test_context_param() { $this->markTestSkipped( 'Controller doesn\'t implement get_context_param().' ); } From a206f515f83b9121ce0b497a1a9a71317b2d7699 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 8 Mar 2018 12:37:28 +1100 Subject: [PATCH 2/3] Only call add_cap() when necessary Avoid calling $role->add_cap() when the role already has the given capability. This avoids an unnecessary database write. --- lib/register.php | 75 ++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/lib/register.php b/lib/register.php index 209d47b3810ca3..fa7b0ae0912553 100644 --- a/lib/register.php +++ b/lib/register.php @@ -412,37 +412,50 @@ function gutenberg_register_post_types() { 'map_meta_cap' => true, ) ); - foreach ( array( 'administrator', 'editor' ) as $role_name ) { - $editor = get_role( $role_name ); - $editor->add_cap( 'edit_blocks' ); - $editor->add_cap( 'edit_others_blocks' ); - $editor->add_cap( 'publish_blocks' ); - $editor->add_cap( 'read_private_blocks' ); - $editor->add_cap( 'read_blocks' ); - $editor->add_cap( 'delete_blocks' ); - $editor->add_cap( 'delete_private_blocks' ); - $editor->add_cap( 'delete_published_blocks' ); - $editor->add_cap( 'delete_others_blocks' ); - $editor->add_cap( 'edit_private_blocks' ); - $editor->add_cap( 'edit_published_blocks' ); - $editor->add_cap( 'create_blocks' ); - } - - $author = get_role( 'author' ); - $author->add_cap( 'edit_blocks' ); - $author->add_cap( 'publish_blocks' ); - $author->add_cap( 'read_blocks' ); - $author->add_cap( 'delete_blocks' ); - $author->add_cap( 'delete_published_blocks' ); - $author->add_cap( 'edit_published_blocks' ); - $author->add_cap( 'create_blocks' ); - - $contributor = get_role( 'contributor' ); - $contributor->add_cap( 'edit_blocks' ); - $contributor->add_cap( 'read_blocks' ); - $contributor->add_cap( 'delete_blocks' ); - $contributor->add_cap( 'delete_published_blocks' ); - $contributor->add_cap( 'edit_published_blocks' ); + $editor_caps = array( + 'edit_blocks', + 'edit_others_blocks', + 'publish_blocks', + 'read_private_blocks', + 'read_blocks', + 'delete_blocks', + 'delete_private_blocks', + 'delete_published_blocks', + 'delete_others_blocks', + 'edit_private_blocks', + 'edit_published_blocks', + 'create_blocks', + ); + + $caps_map = array( + 'administrator' => $editor_caps, + 'editor' => $editor_caps, + 'author' => array( + 'edit_blocks', + 'publish_blocks', + 'read_blocks', + 'delete_blocks', + 'delete_published_blocks', + 'edit_published_blocks', + 'create_blocks', + ), + 'contributor' => array( + 'edit_blocks', + 'read_blocks', + 'delete_blocks', + 'delete_published_blocks', + 'edit_published_blocks', + ), + ); + + foreach ( $caps_map as $role_name => $caps ) { + $role = get_role( $role_name ); + foreach ( $caps as $cap ) { + if ( ! $role->has_cap( $cap ) ) { + $role->add_cap( $cap ); + } + } + } } add_action( 'init', 'gutenberg_register_post_types' ); From 6faa40a164ff2a6c27639cbd664ee978714e34fa Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 8 Mar 2018 12:42:03 +1100 Subject: [PATCH 3/3] Prevent contributors from editing reusable blocks Make reusable block permissions more consistent by preventing contributors from *editing* blocks - not just creating. --- lib/register.php | 4 ---- phpunit/class-rest-blocks-controller-test.php | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/register.php b/lib/register.php index fa7b0ae0912553..67259437781158 100644 --- a/lib/register.php +++ b/lib/register.php @@ -440,11 +440,7 @@ function gutenberg_register_post_types() { 'create_blocks', ), 'contributor' => array( - 'edit_blocks', 'read_blocks', - 'delete_blocks', - 'delete_published_blocks', - 'edit_published_blocks', ), ); diff --git a/phpunit/class-rest-blocks-controller-test.php b/phpunit/class-rest-blocks-controller-test.php index 3fa19b73905621..29b89b4f8385b4 100644 --- a/phpunit/class-rest-blocks-controller-test.php +++ b/phpunit/class-rest-blocks-controller-test.php @@ -214,7 +214,7 @@ public function data_capabilities() { array( 'update_delete_own', 'editor', 200 ), array( 'update_delete_own', 'author', 200 ), - array( 'update_delete_own', 'contributor', 200 ), + array( 'update_delete_own', 'contributor', 403 ), array( 'update_delete_others', 'editor', 200 ), array( 'update_delete_others', 'author', 403 ),