From 7702b535be8669c971430483154dae9dfddf2716 Mon Sep 17 00:00:00 2001 From: Jeremy Felt Date: Fri, 14 Dec 2018 02:34:28 +0000 Subject: [PATCH] REST API: Add endpoints for blocks. `WP_REST_Block_Renderer_Controller` allows rendering of server-side rendered blocks, whilst `WP_REST_Blocks_Controller` allows retrieving of reusable blocks. Merges [43805] and [43806] from the 5.0 branch to trunk. Props desrosj, danielbachhuber, pento, Presskopp, swissspidy. See #45065, #45098. git-svn-id: https://develop.svn.wordpress.org/trunk@44150 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/post.php | 23 +- src/wp-includes/rest-api.php | 5 + ...lass-wp-rest-block-renderer-controller.php | 177 ++++ .../class-wp-rest-blocks-controller.php | 39 + src/wp-settings.php | 2 + .../rest-block-renderer-controller.php | 529 +++++++++++ .../tests/rest-api/rest-blocks-controller.php | 204 +++++ .../tests/rest-api/rest-schema-setup.php | 10 + tests/qunit/fixtures/wp-api-generated.js | 831 +++++++++++++++++- 9 files changed, 1776 insertions(+), 44 deletions(-) create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php create mode 100644 tests/phpunit/tests/rest-api/rest-block-renderer-controller.php create mode 100644 tests/phpunit/tests/rest-api/rest-blocks-controller.php diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index d8ccf05ffb..5cda6bd5b6 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -255,18 +255,21 @@ function create_initial_post_types() { register_post_type( 'wp_block', array( - 'labels' => array( + 'labels' => array( 'name' => __( 'Blocks' ), 'singular_name' => __( 'Block' ), 'search_items' => __( 'Search Blocks' ), ), - 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'show_ui' => true, - 'show_in_menu' => false, - 'rewrite' => false, - 'capability_type' => 'block', - 'capabilities' => array( + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'show_ui' => true, + 'show_in_menu' => false, + 'rewrite' => false, + 'show_in_rest' => true, + 'rest_base' => 'blocks', + 'rest_controller_class' => 'WP_REST_Blocks_Controller', + 'capability_type' => 'block', + 'capabilities' => array( // You need to be able to edit posts, in order to read blocks in their raw form. 'read' => 'edit_posts', // You need to be able to publish posts, in order to create blocks. @@ -276,8 +279,8 @@ function create_initial_post_types() { 'edit_others_posts' => 'edit_others_posts', 'delete_others_posts' => 'delete_others_posts', ), - 'map_meta_cap' => true, - 'supports' => array( + 'map_meta_cap' => true, + 'supports' => array( 'title', 'editor', ), diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 5ab3857cd2..f8e97065a9 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -249,6 +249,10 @@ function create_initial_rest_routes() { $controller = new WP_REST_Search_Controller( $search_handlers ); $controller->register_routes(); + // Block Renderer. + $controller = new WP_REST_Block_Renderer_Controller; + $controller->register_routes(); + // Settings. $controller = new WP_REST_Settings_Controller; $controller->register_routes(); @@ -256,6 +260,7 @@ function create_initial_rest_routes() { // Themes. $controller = new WP_REST_Themes_Controller; $controller->register_routes(); + } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php new file mode 100644 index 0000000000..f0e1e7db47 --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php @@ -0,0 +1,177 @@ +namespace = 'wp/v2'; + $this->rest_base = 'block-renderer'; + } + + /** + * Registers the necessary REST API routes, one for each dynamic block. + * + * @since 5.0.0 + */ + public function register_routes() { + $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); + + foreach ( $block_types as $block_type ) { + if ( ! $block_type->is_dynamic() ) { + continue; + } + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P' . $block_type->name . ')', + array( + 'args' => array( + 'name' => array( + 'description' => __( 'Unique registered name for the block.' ), + 'type' => 'string', + ), + ), + 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' ) ), + 'attributes' => array( + /* translators: %s is the name of the block */ + 'description' => sprintf( __( 'Attributes for %s block' ), $block_type->name ), + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => $block_type->get_attributes(), + ), + 'post_id' => array( + 'description' => __( 'ID of the post context.' ), + 'type' => 'integer', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + } + + /** + * Checks if a given request has access to read blocks. + * + * @since 5.0.0 + * + * @param WP_REST_Request $request Request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + global $post; + + $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; + + if ( 0 < $post_id ) { + $post = get_post( $post_id ); + + if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { + return new WP_Error( + 'block_cannot_read', + __( 'Sorry, you are not allowed to read blocks of this post.' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + } else { + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( + 'block_cannot_read', + __( 'Sorry, you are not allowed to read blocks as this user.' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + } + + return true; + } + + /** + * Returns block output from block's registered render_callback. + * + * @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 get_item( $request ) { + global $post; + + $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; + + if ( 0 < $post_id ) { + $post = get_post( $post_id ); + + // Set up postdata since this will be needed if post_id was set. + setup_postdata( $post ); + } + $registry = WP_Block_Type_Registry::get_instance(); + $block = $registry->get_registered( $request['name'] ); + + if ( null === $block ) { + return new WP_Error( + 'block_invalid', + __( 'Invalid block.' ), + array( + 'status' => 404, + ) + ); + } + + $data = array( + 'rendered' => $block->render( $request->get_param( 'attributes' ) ), + ); + return rest_ensure_response( $data ); + } + + /** + * Retrieves block's output schema, conforming to JSON Schema. + * + * @since 5.0.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + return array( + '$schema' => 'http://json-schema.org/schema#', + 'title' => 'rendered-block', + 'type' => 'object', + 'properties' => array( + 'rendered' => array( + 'description' => __( 'The rendered block.' ), + 'type' => 'string', + 'required' => true, + 'context' => array( 'edit' ), + ), + ), + ); + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php new file mode 100644 index 0000000000..66a2e7adad --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php @@ -0,0 +1,39 @@ +post_type ); + if ( ! current_user_can( $post_type->cap->read_post, $post->ID ) ) { + return false; + } + + return parent::check_read_permission( $post ); + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 9b1d9f5445..4f15f0edc2 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -236,6 +236,8 @@ require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.p require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-search-controller.php' ); +require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-blocks-controller.php' ); +require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-renderer-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' ); require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php' ); require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' ); diff --git a/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php b/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php new file mode 100644 index 0000000000..472e448b10 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php @@ -0,0 +1,529 @@ +user->create( + array( + 'role' => 'editor', + ) + ); + + self::$author_id = $factory->user->create( + array( + 'role' => 'author', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_title' => 'Test Post', + ) + ); + } + + /** + * Delete test data after our tests run. + * + * @since 5.0.0 + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$user_id ); + } + + /** + * Set up each test method. + * + * @since 5.0.0 + */ + public function setUp() { + $this->register_test_block(); + $this->register_post_context_test_block(); + parent::setUp(); + } + + /** + * Tear down each test method. + * + * @since 5.0.0 + */ + public function tearDown() { + WP_Block_Type_Registry::get_instance()->unregister( self::$block_name ); + WP_Block_Type_Registry::get_instance()->unregister( self::$context_block_name ); + parent::tearDown(); + } + + /** + * Register test block. + * + * @since 5.0.0 + */ + public function register_test_block() { + register_block_type( + self::$block_name, + array( + 'attributes' => array( + 'some_string' => array( + 'type' => 'string', + 'default' => 'some_default', + ), + 'some_int' => array( + 'type' => 'integer', + ), + 'some_array' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + ), + 'render_callback' => array( $this, 'render_test_block' ), + ) + ); + } + + /** + * Register test block with post_id as attribute for post context test. + * + * @since 5.0.0 + */ + public function register_post_context_test_block() { + register_block_type( + self::$context_block_name, + array( + 'attributes' => array(), + 'render_callback' => array( $this, 'render_post_context_test_block' ), + ) + ); + } + + /** + * Test render callback. + * + * @since 5.0.0 + * + * @param array $attributes Props. + * @return string Rendered attributes, which is here just JSON. + */ + public function render_test_block( $attributes ) { + return wp_json_encode( $attributes ); + } + + /** + * Test render callback for testing post context. + * + * @since 5.0.0 + * + * @return string + */ + public function render_post_context_test_block() { + return get_the_title(); + } + + /** + * Check that the route was registered properly. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::register_routes() + */ + public function test_register_routes() { + $dynamic_block_names = get_dynamic_block_names(); + $this->assertContains( self::$block_name, $dynamic_block_names ); + + $routes = rest_get_server()->get_routes(); + foreach ( $dynamic_block_names as $dynamic_block_name ) { + $this->assertArrayHasKey( self::$rest_api_route . "(?P$dynamic_block_name)", $routes ); + } + } + + /** + * Test getting item without permissions. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item() + */ + public function test_get_item_without_permissions() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'block_cannot_read', $response, rest_authorization_required_code() ); + } + + /** + * Test getting item without 'edit' context. + * + * @ticket 45098 + */ + public function test_get_item_with_invalid_context() { + wp_set_current_user( self::$user_id ); + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Test getting item with invalid block name. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item() + */ + public function test_get_item_invalid_block_name() { + wp_set_current_user( self::$user_id ); + $request = new WP_REST_Request( 'GET', self::$rest_api_route . 'core/123' ); + + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_no_route', $response, 404 ); + } + + /** + * Check getting item with an invalid param provided. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item() + */ + public function test_get_item_invalid_attribute() { + wp_set_current_user( self::$user_id ); + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $request->set_param( 'context', 'edit' ); + $request->set_param( + 'attributes', + array( + 'some_string' => array( 'no!' ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Check getting item with an invalid param provided. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item() + */ + public function test_get_item_unrecognized_attribute() { + wp_set_current_user( self::$user_id ); + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $request->set_param( 'context', 'edit' ); + $request->set_param( + 'attributes', + array( + 'unrecognized' => 'yes', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Check getting item with default attributes provided. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item() + */ + public function test_get_item_default_attributes() { + wp_set_current_user( self::$user_id ); + + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); + $defaults = array(); + foreach ( $block_type->attributes as $key => $attribute ) { + $defaults[ $key ] = isset( $attribute['default'] ) ? $attribute['default'] : null; + } + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $request->set_param( 'context', 'edit' ); + $request->set_param( 'attributes', array() ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertEquals( $defaults, json_decode( $data['rendered'], true ) ); + $this->assertEquals( + json_decode( $block_type->render( $defaults ) ), + json_decode( $data['rendered'] ) + ); + } + + /** + * Check getting item with attributes provided. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item() + */ + public function test_get_item() { + wp_set_current_user( self::$user_id ); + + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( self::$block_name ); + $attributes = array( + 'some_int' => '123', + 'some_string' => 'foo', + 'some_array' => array( 1, '2', 3 ), + ); + + $expected_attributes = $attributes; + $expected_attributes['some_int'] = (int) $expected_attributes['some_int']; + $expected_attributes['some_array'] = array_map( 'intval', $expected_attributes['some_array'] ); + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $request->set_param( 'context', 'edit' ); + $request->set_param( 'attributes', $attributes ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertEquals( $expected_attributes, json_decode( $data['rendered'], true ) ); + $this->assertEquals( + json_decode( $block_type->render( $attributes ), true ), + json_decode( $data['rendered'], true ) + ); + } + + + + /** + * Check success response for getting item with layout attribute provided. + * + * @ticket 45098 + */ + public function test_get_item_with_layout() { + wp_set_current_user( self::$user_id ); + + $attributes = array( + 'layout' => 'foo', + ); + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$block_name ); + $request->set_param( 'context', 'edit' ); + $request->set_param( 'attributes', $attributes ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test getting item with post context. + * + * @ticket 45098 + */ + public function test_get_item_with_post_context() { + wp_set_current_user( self::$user_id ); + + $expected_title = 'Test Post'; + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); + $request->set_param( 'context', 'edit' ); + + // Test without post ID. + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertTrue( empty( $data['rendered'] ) ); + + // Now test with post ID. + $request->set_param( 'post_id', self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertEquals( $expected_title, $data['rendered'] ); + } + + /** + * Test getting item with invalid post ID. + * + * @ticket 45098 + */ + public function test_get_item_without_permissions_invalid_post() { + wp_set_current_user( self::$user_id ); + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); + $request->set_param( 'context', 'edit' ); + + // Test with invalid post ID. + $request->set_param( 'post_id', PHP_INT_MAX ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'block_cannot_read', $response, 403 ); + } + + /** + * Test getting item without permissions to edit context post. + * + * @ticket 45098 + */ + public function test_get_item_without_permissions_cannot_edit_post() { + wp_set_current_user( self::$author_id ); + + $request = new WP_REST_Request( 'GET', self::$rest_api_route . self::$context_block_name ); + $request->set_param( 'context', 'edit' ); + + // Test with private post ID. + $request->set_param( 'post_id', self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'block_cannot_read', $response, 403 ); + } + + /** + * Get item schema. + * + * @ticket 45098 + * + * @covers WP_REST_Block_Renderer_Controller::get_item_schema() + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', self::$rest_api_route . self::$block_name ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEqualSets( array( 'GET' ), $data['endpoints'][0]['methods'] ); + $this->assertEqualSets( + array( 'name', 'context', 'attributes', 'post_id' ), + array_keys( $data['endpoints'][0]['args'] ) + ); + $this->assertEquals( 'object', $data['endpoints'][0]['args']['attributes']['type'] ); + + $this->assertArrayHasKey( 'schema', $data ); + $this->assertEquals( 'rendered-block', $data['schema']['title'] ); + $this->assertEquals( 'object', $data['schema']['type'] ); + $this->arrayHasKey( 'rendered', $data['schema']['properties'] ); + $this->arrayHasKey( 'string', $data['schema']['properties']['rendered']['type'] ); + $this->assertEquals( array( 'edit' ), $data['schema']['properties']['rendered']['context'] ); + } + + /** + * The update_item() method does not exist for block rendering. + */ + public function test_update_item() { + $this->markTestSkipped( 'Controller does not implement update_item().' ); + } + + /** + * The create_item() method does not exist for block rendering. + */ + public function test_create_item() { + $this->markTestSkipped( 'Controller does not implement create_item().' ); + } + + /** + * The delete_item() method does not exist for block rendering. + */ + public function test_delete_item() { + $this->markTestSkipped( 'Controller does not implement delete_item().' ); + } + + /** + * The get_items() method does not exist for block rendering. + */ + public function test_get_items() { + $this->markTestSkipped( 'Controller does not implement get_items().' ); + } + + /** + * The context_param() method does not exist for block rendering. + */ + public function test_context_param() { + $this->markTestSkipped( 'Controller does not implement context_param().' ); + } + + /** + * The prepare_item() method does not exist for block rendering. + */ + public function test_prepare_item() { + $this->markTestSkipped( 'Controller does not implement prepare_item().' ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-blocks-controller.php b/tests/phpunit/tests/rest-api/rest-blocks-controller.php new file mode 100644 index 0000000000..70798b2756 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-blocks-controller.php @@ -0,0 +1,204 @@ + 'wp_block', + 'post_status' => 'publish', + 'post_title' => 'My cool block', + 'post_content' => '

Hello!

', + ) + ); + + self::$user_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + } + + /** + * Delete our fake data after our tests run. + * + * @since 5.0.0 + */ + public static function wpTearDownAfterClass() { + wp_delete_post( self::$post_id ); + + self::delete_user( self::$user_id ); + } + + /** + * Test cases for test_capabilities(). + * + * @since 5.0.0 + */ + 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', 403 ), + + 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. + * + * @ticket 45098 + * + * @dataProvider data_capabilities + * + * @param string $action Action to perform in the test. + * @param string $role User role to test. + * @param int $expected_status Expected HTTP response status. + */ + 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 = rest_get_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 = rest_get_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 = rest_get_server()->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . $post_id ); + + $response = rest_get_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 = rest_get_server()->dispatch( $request ); + $this->assertEquals( $expected_status, $response->get_status() ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/blocks/' . self::$post_id ); + + $response = rest_get_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 ); + } + } +} diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 32a8849b9f..0b1811a145 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -99,6 +99,10 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp/v2/pages/(?P[\\d]+)/autosaves/(?P[\\d]+)', '/wp/v2/media', '/wp/v2/media/(?P[\\d]+)', + '/wp/v2/blocks', + '/wp/v2/blocks/(?P[\d]+)', + '/wp/v2/blocks/(?P[\d]+)/autosaves', + '/wp/v2/blocks/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/types', '/wp/v2/types/(?P[\\w-]+)', '/wp/v2/statuses', @@ -115,6 +119,12 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp/v2/comments', '/wp/v2/comments/(?P[\\d]+)', '/wp/v2/search', + '/wp/v2/block-renderer/(?Pcore/block)', + '/wp/v2/block-renderer/(?Pcore/latest-comments)', + '/wp/v2/block-renderer/(?Pcore/archives)', + '/wp/v2/block-renderer/(?Pcore/categories)', + '/wp/v2/block-renderer/(?Pcore/latest-posts)', + '/wp/v2/block-renderer/(?Pcore/shortcode)', '/wp/v2/settings', '/wp/v2/themes', ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index ba068560d1..44d26e0b06 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2271,6 +2271,508 @@ mockedApiResponse.Schema = { } ] }, + "/wp/v2/blocks": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "required": false, + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "required": false, + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "required": false, + "default": 10, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "required": false, + "description": "Limit results to those matching a string.", + "type": "string" + }, + "after": { + "required": false, + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string" + }, + "before": { + "required": false, + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string" + }, + "exclude": { + "required": false, + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "required": false, + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "required": false, + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "required": false, + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "required": false, + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title" + ], + "description": "Sort collection by object attribute.", + "type": "string" + }, + "slug": { + "required": false, + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "required": false, + "default": "publish", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "publish", + "future", + "draft", + "pending", + "private", + "trash", + "auto-draft", + "inherit", + "request-pending", + "request-confirmed", + "request-failed", + "request-completed", + "any" + ], + "type": "string" + } + } + } + }, + { + "methods": [ + "POST" + ], + "args": { + "date": { + "required": false, + "description": "The date the object was published, in the site's timezone.", + "type": "string" + }, + "date_gmt": { + "required": false, + "description": "The date the object was published, as GMT.", + "type": "string" + }, + "slug": { + "required": false, + "description": "An alphanumeric identifier for the object unique to its type.", + "type": "string" + }, + "status": { + "required": false, + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "description": "A named status for the object.", + "type": "string" + }, + "password": { + "required": false, + "description": "A password to protect access to the content and excerpt.", + "type": "string" + }, + "title": { + "required": false, + "description": "The title for the object.", + "type": "object" + }, + "content": { + "required": false, + "description": "The content for the object.", + "type": "object" + }, + "template": { + "required": false, + "description": "The theme file to use to display the object.", + "type": "string" + } + } + } + ], + "_links": { + "self": "http://example.org/index.php?rest_route=/wp/v2/blocks" + } + }, + "/wp/v2/blocks/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "id": { + "required": false, + "description": "Unique identifier for the object.", + "type": "integer" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "password": { + "required": false, + "description": "The password for the post if it is password protected.", + "type": "string" + } + } + }, + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "args": { + "id": { + "required": false, + "description": "Unique identifier for the object.", + "type": "integer" + }, + "date": { + "required": false, + "description": "The date the object was published, in the site's timezone.", + "type": "string" + }, + "date_gmt": { + "required": false, + "description": "The date the object was published, as GMT.", + "type": "string" + }, + "slug": { + "required": false, + "description": "An alphanumeric identifier for the object unique to its type.", + "type": "string" + }, + "status": { + "required": false, + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "description": "A named status for the object.", + "type": "string" + }, + "password": { + "required": false, + "description": "A password to protect access to the content and excerpt.", + "type": "string" + }, + "title": { + "required": false, + "description": "The title for the object.", + "type": "object" + }, + "content": { + "required": false, + "description": "The content for the object.", + "type": "object" + }, + "template": { + "required": false, + "description": "The theme file to use to display the object.", + "type": "string" + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "id": { + "required": false, + "description": "Unique identifier for the object.", + "type": "integer" + }, + "force": { + "required": false, + "default": false, + "description": "Whether to bypass trash and force deletion.", + "type": "boolean" + } + } + } + ] + }, + "/wp/v2/blocks/(?P[\\d]+)/autosaves": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "required": false, + "description": "The ID for the parent of the object.", + "type": "integer" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "page": { + "required": false, + "default": 1, + "description": "Current page of the collection.", + "type": "integer" + }, + "per_page": { + "required": false, + "description": "Maximum number of items to be returned in result set.", + "type": "integer" + }, + "search": { + "required": false, + "description": "Limit results to those matching a string.", + "type": "string" + }, + "exclude": { + "required": false, + "default": [], + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "include": { + "required": false, + "default": [], + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + "offset": { + "required": false, + "description": "Offset the result set by a specific number of items.", + "type": "integer" + }, + "order": { + "required": false, + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "description": "Order sort attribute ascending or descending.", + "type": "string" + }, + "orderby": { + "required": false, + "default": "date", + "enum": [ + "date", + "id", + "include", + "relevance", + "slug", + "include_slugs", + "title" + ], + "description": "Sort collection by object attribute.", + "type": "string" + } + } + }, + { + "methods": [ + "POST" + ], + "args": { + "parent": { + "required": false, + "description": "The ID for the parent of the object.", + "type": "integer" + }, + "author": { + "required": false, + "description": "The ID for the author of the object.", + "type": "integer" + }, + "date": { + "required": false, + "description": "The date the object was published, in the site's timezone.", + "type": "string" + }, + "date_gmt": { + "required": false, + "description": "The date the object was published, as GMT.", + "type": "string" + }, + "id": { + "required": false, + "description": "Unique identifier for the object.", + "type": "integer" + }, + "modified": { + "required": false, + "description": "The date the object was last modified, in the site's timezone.", + "type": "string" + }, + "modified_gmt": { + "required": false, + "description": "The date the object was last modified, as GMT.", + "type": "string" + }, + "slug": { + "required": false, + "description": "An alphanumeric identifier for the object unique to its type.", + "type": "string" + }, + "title": { + "required": false, + "description": "The title for the object.", + "type": "object" + }, + "content": { + "required": false, + "description": "The content for the object.", + "type": "object" + } + } + } + ] + }, + "/wp/v2/blocks/(?P[\\d]+)/autosaves/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "required": false, + "description": "The ID for the parent of the object.", + "type": "integer" + }, + "id": { + "required": false, + "description": "The ID for the object.", + "type": "integer" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "view", + "embed", + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + } + } + } + ] + }, "/wp/v2/types": { "namespace": "wp/v2", "methods": [ @@ -3865,6 +4367,240 @@ mockedApiResponse.Schema = { "self": "http://example.org/index.php?rest_route=/wp/v2/search" } }, + "/wp/v2/block-renderer/(?Pcore/block)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "name": { + "required": false, + "description": "Unique registered name for the block.", + "type": "string" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "attributes": { + "required": false, + "description": "Attributes for core/block block", + "type": "object" + }, + "post_id": { + "required": false, + "description": "ID of the post context.", + "type": "integer" + } + } + } + ] + }, + "/wp/v2/block-renderer/(?Pcore/latest-comments)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "name": { + "required": false, + "description": "Unique registered name for the block.", + "type": "string" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "attributes": { + "required": false, + "description": "Attributes for core/latest-comments block", + "type": "object" + }, + "post_id": { + "required": false, + "description": "ID of the post context.", + "type": "integer" + } + } + } + ] + }, + "/wp/v2/block-renderer/(?Pcore/archives)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "name": { + "required": false, + "description": "Unique registered name for the block.", + "type": "string" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "attributes": { + "required": false, + "description": "Attributes for core/archives block", + "type": "object" + }, + "post_id": { + "required": false, + "description": "ID of the post context.", + "type": "integer" + } + } + } + ] + }, + "/wp/v2/block-renderer/(?Pcore/categories)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "name": { + "required": false, + "description": "Unique registered name for the block.", + "type": "string" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "attributes": { + "required": false, + "description": "Attributes for core/categories block", + "type": "object" + }, + "post_id": { + "required": false, + "description": "ID of the post context.", + "type": "integer" + } + } + } + ] + }, + "/wp/v2/block-renderer/(?Pcore/latest-posts)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "name": { + "required": false, + "description": "Unique registered name for the block.", + "type": "string" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "attributes": { + "required": false, + "description": "Attributes for core/latest-posts block", + "type": "object" + }, + "post_id": { + "required": false, + "description": "ID of the post context.", + "type": "integer" + } + } + } + ] + }, + "/wp/v2/block-renderer/(?Pcore/shortcode)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "name": { + "required": false, + "description": "Unique registered name for the block.", + "type": "string" + }, + "context": { + "required": false, + "default": "view", + "enum": [ + "edit" + ], + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string" + }, + "attributes": { + "required": false, + "description": "Attributes for core/shortcode block", + "type": "object" + }, + "post_id": { + "required": false, + "description": "ID of the post context.", + "type": "integer" + } + } + } + ] + }, "/wp/v2/settings": { "namespace": "wp/v2", "methods": [ @@ -4347,16 +5083,16 @@ mockedApiResponse.postRevisions = [ } }, { - "author": 359, + "author": 375, "date": "2017-02-14T00:00:00", "date_gmt": "2017-02-14T00:00:00", - "id": 3153, + "id": 36744, "modified": "2017-02-14T00:00:00", "modified_gmt": "2017-02-14T00:00:00", - "parent": 3152, - "slug": "3152-revision-v1", + "parent": 36743, + "slug": "36743-revision-v1", "guid": { - "rendered": "http://example.org/?p=3153" + "rendered": "http://example.org/?p=36744" }, "title": { "rendered": "REST API Client Fixture: Post" @@ -4370,7 +5106,7 @@ mockedApiResponse.postRevisions = [ "_links": { "parent": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/posts/3152" + "href": "http://example.org/index.php?rest_route=/wp/v2/posts/36743" } ] } @@ -4402,16 +5138,16 @@ mockedApiResponse.revision = { mockedApiResponse.postAutosaves = [ { - "author": 359, + "author": 375, "date": "2017-02-14T00:00:00", "date_gmt": "2017-02-14T00:00:00", - "id": 3154, + "id": 36745, "modified": "2017-02-14T00:00:00", "modified_gmt": "2017-02-14T00:00:00", - "parent": 3152, - "slug": "3152-autosave-v1", + "parent": 36743, + "slug": "36743-autosave-v1", "guid": { - "rendered": "http://example.org/?p=3154" + "rendered": "http://example.org/?p=36745" }, "title": { "rendered": "" @@ -4425,7 +5161,7 @@ mockedApiResponse.postAutosaves = [ "_links": { "parent": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/posts/3152" + "href": "http://example.org/index.php?rest_route=/wp/v2/posts/36743" } ] } @@ -4433,16 +5169,16 @@ mockedApiResponse.postAutosaves = [ ]; mockedApiResponse.autosave = { - "author": 359, + "author": 375, "date": "2017-02-14T00:00:00", "date_gmt": "2017-02-14T00:00:00", - "id": 3154, + "id": 36745, "modified": "2017-02-14T00:00:00", "modified_gmt": "2017-02-14T00:00:00", - "parent": 3152, - "slug": "3152-autosave-v1", + "parent": 36743, + "slug": "36743-autosave-v1", "guid": { - "rendered": "http://example.org/?p=3154" + "rendered": "http://example.org/?p=36745" }, "title": { "rendered": "" @@ -4607,16 +5343,16 @@ mockedApiResponse.pageRevisions = [ } }, { - "author": 359, + "author": 375, "date": "2017-02-14T00:00:00", "date_gmt": "2017-02-14T00:00:00", - "id": 3156, + "id": 36747, "modified": "2017-02-14T00:00:00", "modified_gmt": "2017-02-14T00:00:00", - "parent": 3155, - "slug": "3155-revision-v1", + "parent": 36746, + "slug": "36746-revision-v1", "guid": { - "rendered": "http://example.org/?p=3156" + "rendered": "http://example.org/?p=36747" }, "title": { "rendered": "REST API Client Fixture: Page" @@ -4630,7 +5366,7 @@ mockedApiResponse.pageRevisions = [ "_links": { "parent": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/pages/3155" + "href": "http://example.org/index.php?rest_route=/wp/v2/pages/36746" } ] } @@ -4662,16 +5398,16 @@ mockedApiResponse.pageRevision = { mockedApiResponse.pageAutosaves = [ { - "author": 359, + "author": 375, "date": "2017-02-14T00:00:00", "date_gmt": "2017-02-14T00:00:00", - "id": 3157, + "id": 36748, "modified": "2017-02-14T00:00:00", "modified_gmt": "2017-02-14T00:00:00", - "parent": 3155, - "slug": "3155-autosave-v1", + "parent": 36746, + "slug": "36746-autosave-v1", "guid": { - "rendered": "http://example.org/?p=3157" + "rendered": "http://example.org/?p=36748" }, "title": { "rendered": "" @@ -4685,7 +5421,7 @@ mockedApiResponse.pageAutosaves = [ "_links": { "parent": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/pages/3155" + "href": "http://example.org/index.php?rest_route=/wp/v2/pages/36746" } ] } @@ -4693,16 +5429,16 @@ mockedApiResponse.pageAutosaves = [ ]; mockedApiResponse.pageAutosave = { - "author": 359, + "author": 375, "date": "2017-02-14T00:00:00", "date_gmt": "2017-02-14T00:00:00", - "id": 3157, + "id": 36748, "modified": "2017-02-14T00:00:00", "modified_gmt": "2017-02-14T00:00:00", - "parent": 3155, - "slug": "3155-autosave-v1", + "parent": 36746, + "slug": "36746-autosave-v1", "guid": { - "rendered": "http://example.org/?p=3157" + "rendered": "http://example.org/?p=36748" }, "title": { "rendered": "" @@ -4898,6 +5634,33 @@ mockedApiResponse.TypesCollection = { } ] } + }, + "wp_block": { + "description": "", + "hierarchical": false, + "name": "Blocks", + "slug": "wp_block", + "taxonomies": [], + "rest_base": "blocks", + "_links": { + "collection": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/types" + } + ], + "wp:items": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/blocks" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } } };