From cd587762a0ed02bc76bbb78d6333efd655649207 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Mon, 12 Mar 2018 19:52:03 +0200 Subject: [PATCH 01/29] Add blocks-renderer API endpoint. Add skeleton for ServerSideRender component. --- components/index.js | 1 + components/server-side-render/index.js | 14 ++ ...ass-wp-rest-blocks-renderer-controller.php | 130 ++++++++++++++++++ lib/load.php | 1 + lib/register.php | 11 ++ 5 files changed, 157 insertions(+) create mode 100644 components/server-side-render/index.js create mode 100644 lib/class-wp-rest-blocks-renderer-controller.php diff --git a/components/index.js b/components/index.js index 512e2b8babb18..5ce8c9dce32b9 100644 --- a/components/index.js +++ b/components/index.js @@ -39,6 +39,7 @@ export { default as ResponsiveWrapper } from './responsive-wrapper'; export { default as SandBox } from './sandbox'; export { default as SelectControl } from './select-control'; export { default as Spinner } from './spinner'; +export { default as ServerSideRender } from './server-side-render'; export { default as TabPanel } from './tab-panel'; export { default as TextControl } from './text-control'; export { default as TextareaControl } from './textarea-control'; diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js new file mode 100644 index 0000000000000..3c9feac83ef3c --- /dev/null +++ b/components/server-side-render/index.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { Component, compose, renderToString } from '@wordpress/element'; + +export class ServerSideRender extends Component { + render() { + return ( +
Here will be SSR!
+ ); + } +} + +export default ServerSideRender; \ No newline at end of file diff --git a/lib/class-wp-rest-blocks-renderer-controller.php b/lib/class-wp-rest-blocks-renderer-controller.php new file mode 100644 index 0000000000000..a1c0a6e2b99ff --- /dev/null +++ b/lib/class-wp-rest-blocks-renderer-controller.php @@ -0,0 +1,130 @@ +namespace for the namespace keyword. + $this->namespace = 'gutenberg/v1'; + $this->rest_base = 'blocks-renderer'; + } + + /** + * Registers the necessary REST API routes. + * + * @access public + */ + public function register_routes() { + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+\/[\w-]+)', 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_output' ), + 'permission_callback' => array( $this, 'get_item_output_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Checks if a given request has access to read blocks. + * + * @since ? + * @access public + * + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_output_permissions_check() { + return true; + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( + 'status' => rest_authorization_required_code(), + ) ); + } + + return true; + } + + /** + * Returns block output from block's registered render_callback. + * + * @since ? + * @access public + * + * @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_output( $request ) { + if ( ! isset( $request['name'] ) ) { + return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.' ), array( 'status' => 404 ) ); + } + + $registry = WP_Block_Type_Registry::get_instance(); + $block = $registry->get_registered( $request['name'] ); + + if ( ! $block || ! $block instanceof WP_Block_Type) { + return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.' ), array( 'status' => 404 ) ); + } + + if ( isset( $request['attributes'] ) && is_array( $request['attributes'] ) ) { + $atts = $request['attributes']; + } else { + $atts = array(); + } + + $data = array( + 'output' => $block->render( $atts ), + ); + return rest_ensure_response( $data ); + } + + /** + * Retrieves block's output schema, conforming to JSON Schema. + * + * @since ? + * @access public + * + * @return array Item schema data. + */ + public function get_item_schema() { + return array( + '$schema' => 'http://json-schema.org/schema#', + 'title' => 'shortcode-block', + 'type' => 'object', + 'properties' => array( + 'html' => array( + 'description' => __( 'The block\'s output.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ),'required' => true, + ), + ); + } +} \ No newline at end of file diff --git a/lib/load.php b/lib/load.php index 0da31fddf577c..b6b4f870ed552 100644 --- a/lib/load.php +++ b/lib/load.php @@ -13,6 +13,7 @@ require dirname( __FILE__ ) . '/class-wp-block-type.php'; require dirname( __FILE__ ) . '/class-wp-block-type-registry.php'; require dirname( __FILE__ ) . '/class-wp-rest-blocks-controller.php'; +require dirname( __FILE__ ) . '/class-wp-rest-blocks-renderer-controller.php'; require dirname( __FILE__ ) . '/blocks.php'; require dirname( __FILE__ ) . '/client-assets.php'; require dirname( __FILE__ ) . '/compat.php'; diff --git a/lib/register.php b/lib/register.php index 8e6b73b2f608e..bd5da8025e339 100644 --- a/lib/register.php +++ b/lib/register.php @@ -441,6 +441,17 @@ function gutenberg_register_post_types() { } add_action( 'init', 'gutenberg_register_post_types' ); +/** + * Registers the REST API routes needed by the Gutenberg editor. + * + * @since ? + */ +function gutenberg_register_rest_routes() { + $controller = new WP_REST_Blocks_Renderer_Controller(); + $controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_rest_routes' ); + /** * Gets revisions details for the selected post. * From b0ede76b2daac80052f287b4322eecfa1fc503d1 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Tue, 13 Mar 2018 16:42:43 +0200 Subject: [PATCH 02/29] Update SSR preview when editing attributes. --- components/server-side-render/index.js | 61 ++++++++++++++++++- ...ass-wp-rest-blocks-renderer-controller.php | 24 ++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 3c9feac83ef3c..45155359c2e1c 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -1,12 +1,69 @@ /** * WordPress dependencies */ -import { Component, compose, renderToString } from '@wordpress/element'; +import { + Component, + compose, + RawHTML +} from '@wordpress/element'; +import { Spinner } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; export class ServerSideRender extends Component { + + constructor( props ) { + super( props ); + this.state = { + response: {}, + attributes: props.attributes, + }; + } + + componentDidMount() { + this.getOutput(); + } + + componentWillReceiveProps( nextProps ) { + if ( JSON.stringify( nextProps.attributes ) !== JSON.stringify( this.props.attributes ) ) { + this.setState( { attributes: nextProps.attributes }, this.getOutput ); + } + } + + getOutput() { + this.setState( { response: {} } ); + const { block } = this.props; + const attributes = this.state.attributes; + const apiURL = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/blocks-renderer/' + block, { ...attributes } ); + return window.fetch( apiURL, { + credentials: 'include', + _wpnonce: wpApiSettings.nonce, + } ).then( response => { + response.json().then( data => ( { + data: data, + status: response.status, + } ) ).then( res => { + if ( res.status === 200 ) { + this.setState( { response: res } ); + } + } ); + } ); + } + render() { + const response = this.state.response; + if ( response.isLoading || ! response.data ) { + return ( +
+ +

{ __( 'Loading...' ) }

+
+ ); + } + + const html = response.data.output; return ( -
Here will be SSR!
+ { html } ); } } diff --git a/lib/class-wp-rest-blocks-renderer-controller.php b/lib/class-wp-rest-blocks-renderer-controller.php index a1c0a6e2b99ff..2e9e6133612bc 100644 --- a/lib/class-wp-rest-blocks-renderer-controller.php +++ b/lib/class-wp-rest-blocks-renderer-controller.php @@ -93,11 +93,7 @@ public function get_item_output( $request ) { return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.' ), array( 'status' => 404 ) ); } - if ( isset( $request['attributes'] ) && is_array( $request['attributes'] ) ) { - $atts = $request['attributes']; - } else { - $atts = array(); - } + $atts = $this->prepare_attributes( $request->get_params() ); $data = array( 'output' => $block->render( $atts ), @@ -105,6 +101,24 @@ public function get_item_output( $request ) { return rest_ensure_response( $data ); } + /** + * Fix potential boolean value issues. The values come as strings and "false" and "true" might generate issues if left like this. + * + * @param array $attributes Attributes. + * @return mixed Attributes. + */ + public function prepare_attributes( $attributes ) { + foreach ( $attributes as $key => $value ) { + if ( "false" === $value ) { + $attributes[ $key ] = false; + } elseif ( "true" === $value ) { + $attributes[ $key ] = true; + } + } + + return $attributes; + } + /** * Retrieves block's output schema, conforming to JSON Schema. * From 340136ea5371016875b0ff3a9feb4bf58ca9d7fc Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Tue, 13 Mar 2018 17:01:06 +0200 Subject: [PATCH 03/29] Fix binding preview change. --- components/server-side-render/index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 45155359c2e1c..574dc8547507e 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -18,18 +18,14 @@ export class ServerSideRender extends Component { response: {}, attributes: props.attributes, }; + + this.getOutput = this.getOutput.bind( this ); } componentDidMount() { this.getOutput(); } - componentWillReceiveProps( nextProps ) { - if ( JSON.stringify( nextProps.attributes ) !== JSON.stringify( this.props.attributes ) ) { - this.setState( { attributes: nextProps.attributes }, this.getOutput ); - } - } - getOutput() { this.setState( { response: {} } ); const { block } = this.props; From 514dd4f093e56cdbe50fd6d05801024d5809c3b7 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Tue, 13 Mar 2018 17:11:34 +0200 Subject: [PATCH 04/29] Fix binding preview change. --- components/server-side-render/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 574dc8547507e..9e9244d1a9826 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -18,16 +18,19 @@ export class ServerSideRender extends Component { response: {}, attributes: props.attributes, }; - - this.getOutput = this.getOutput.bind( this ); } componentDidMount() { this.getOutput(); } + componentWillReceiveProps( nextProps ) { + if ( JSON.stringify( nextProps.attributes ) !== JSON.stringify( this.props.attributes ) ) { + this.setState( { attributes: nextProps.attributes }, this.getOutput ); + } + } + getOutput() { - this.setState( { response: {} } ); const { block } = this.props; const attributes = this.state.attributes; const apiURL = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/blocks-renderer/' + block, { ...attributes } ); From a90f40d6a10046a415c81ac8072033516b4e99cf Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Tue, 13 Mar 2018 23:19:45 +0200 Subject: [PATCH 05/29] Add tests. Fix permission check for get_item. --- components/server-side-render/README.md | 24 +++ components/server-side-render/index.js | 13 +- ...ass-wp-rest-blocks-renderer-controller.php | 7 +- ...s-rest-blocks-renderer-controller-test.php | 183 ++++++++++++++++++ 4 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 components/server-side-render/README.md create mode 100644 phpunit/class-rest-blocks-renderer-controller-test.php diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md new file mode 100644 index 0000000000000..c9c8becb2786e --- /dev/null +++ b/components/server-side-render/README.md @@ -0,0 +1,24 @@ +ServerSideRender +======= + +ServerSideRender component is used for server-side-rendering preview in Gutenberg editor, specifically for dynamic blocks. + + +## Usage + +Render core/latest-posts preview. +```jsx + +``` + +## Output + +Output is using the `render_callback` set when defining the block. For example if `block="core/latest-posts"` as in the example then the output will match `render_callback` output of that block. + +## API Endpoint +API endpoint for getting the output for ServerSideRender is `/gutenberg/v1/blocks-renderer/:block`. It accepts any params which are used as `attributes` for the block's `render_callback` method. + + diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 9e9244d1a9826..5d943fc9211f1 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -16,7 +16,7 @@ export class ServerSideRender extends Component { super( props ); this.state = { response: {}, - attributes: props.attributes, + attributes: props, }; } @@ -25,18 +25,21 @@ export class ServerSideRender extends Component { } componentWillReceiveProps( nextProps ) { - if ( JSON.stringify( nextProps.attributes ) !== JSON.stringify( this.props.attributes ) ) { - this.setState( { attributes: nextProps.attributes }, this.getOutput ); + if ( JSON.stringify( nextProps ) !== JSON.stringify( this.props ) ) { + debugger; + this.setState( { attributes: nextProps }, this.getOutput ); } } getOutput() { const { block } = this.props; const attributes = this.state.attributes; - const apiURL = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/blocks-renderer/' + block, { ...attributes } ); + const apiURL = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/blocks-renderer/' + block, { + ...attributes, + _wpnonce: wpApiSettings.nonce, + } ); return window.fetch( apiURL, { credentials: 'include', - _wpnonce: wpApiSettings.nonce, } ).then( response => { response.json().then( data => ( { data: data, diff --git a/lib/class-wp-rest-blocks-renderer-controller.php b/lib/class-wp-rest-blocks-renderer-controller.php index 2e9e6133612bc..4ec7e43215293 100644 --- a/lib/class-wp-rest-blocks-renderer-controller.php +++ b/lib/class-wp-rest-blocks-renderer-controller.php @@ -62,7 +62,6 @@ public function register_routes() { * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ public function get_item_output_permissions_check() { - return true; if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), @@ -130,14 +129,14 @@ public function prepare_attributes( $attributes ) { public function get_item_schema() { return array( '$schema' => 'http://json-schema.org/schema#', - 'title' => 'shortcode-block', + 'title' => 'blocks-renderer', 'type' => 'object', 'properties' => array( - 'html' => array( + 'output' => array( 'description' => __( 'The block\'s output.', 'gutenberg' ), 'type' => 'string', 'required' => true, - ),'required' => true, + ), ), ); } diff --git a/phpunit/class-rest-blocks-renderer-controller-test.php b/phpunit/class-rest-blocks-renderer-controller-test.php new file mode 100644 index 0000000000000..1ec16891539bf --- /dev/null +++ b/phpunit/class-rest-blocks-renderer-controller-test.php @@ -0,0 +1,183 @@ +user->create( + array( + 'role' => 'editor', + ) + ); + } + + /** + * Register test block. + */ + public function register_test_block() { + register_block_type( self::$block_name, array( + 'attributes' => array( + 'foo' => array( + 'type' => 'string', + ), + ), + 'render_callback' => array( $this, 'render_test_block' ), + ) ); + } + + /** + * Test render callback. + * + * @param array $attributes Props. + * @return bool|string + */ + public function render_test_block( $attributes ) { + if ( isset( $attributes['foo'] ) ) { + return 'Expected test result'; + } else { + return false; + } + } + + /** + * Delete test data after our tests run. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$user_id ); + } + + /** + * Check that the route was registered properly. + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( '/gutenberg/v1/blocks-renderer/(?P[\w-]+\/[\w-]+)', $routes ); + } + + /** + * Test getting item without permissions. + */ + public function test_get_item_output_without_permissions() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/blocks-renderer/' . self::$block_name ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, rest_authorization_required_code() ); + } + + /** + * Test getting item with invalid block name. + */ + public function test_get_item_output_invalid_block_name() { + wp_set_current_user( self::$user_id ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/blocks-renderer/core/123' ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'rest_block_invalid_name', $response, 404 ); + } + + /** + * Check getting the correct block output. + * Test get_item_output(). + * + * @covers test_get_item(). + */ + public function test_get_item_output() { + $this->register_test_block(); + wp_set_current_user( self::$user_id ); + + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/blocks-renderer/' . self::$block_name ); + $request->set_param( 'foo', 'bar' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 'Expected test result', $data['output'] ); + + } + + /** + * NA. + */ + public function test_update_item() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_create_item() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_delete_item() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_get_item() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_get_items() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_get_item_schema() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_context_param() { + $this->markTestSkipped(); + } + + /** + * NA. + */ + public function test_prepare_item() { + $this->markTestSkipped(); + } +} From 0c127e1a2729f6fa94f89d65d42f8e031bc9686a Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 14 Mar 2018 11:12:22 +0200 Subject: [PATCH 06/29] Remove debugger line. --- components/server-side-render/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 5d943fc9211f1..40456b0309760 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -26,7 +26,6 @@ export class ServerSideRender extends Component { componentWillReceiveProps( nextProps ) { if ( JSON.stringify( nextProps ) !== JSON.stringify( this.props ) ) { - debugger; this.setState( { attributes: nextProps }, this.getOutput ); } } From b0b977a733333f0e1470a5afda62cd4cb997f601 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 14 Mar 2018 11:29:30 +0200 Subject: [PATCH 07/29] Fix some phpcs. --- ...ass-wp-rest-blocks-renderer-controller.php | 19 ++++++++++--------- ...s-rest-blocks-renderer-controller-test.php | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/class-wp-rest-blocks-renderer-controller.php b/lib/class-wp-rest-blocks-renderer-controller.php index 4ec7e43215293..22885ec4f7cc7 100644 --- a/lib/class-wp-rest-blocks-renderer-controller.php +++ b/lib/class-wp-rest-blocks-renderer-controller.php @@ -34,10 +34,11 @@ public function __construct() { */ public function register_routes() { + // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+\/[\w-]+)', array( - 'args' => array( + 'args' => array( 'name' => array( - 'description' => __( 'Unique registered name for the block.' ), + 'description' => __( 'Unique registered name for the block.', 'gutenberg' ), 'type' => 'string', ), ), @@ -82,14 +83,14 @@ public function get_item_output_permissions_check() { */ public function get_item_output( $request ) { if ( ! isset( $request['name'] ) ) { - return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.' ), array( 'status' => 404 ) ); + return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.', 'gutenberg' ), array( 'status' => 404 ) ); } $registry = WP_Block_Type_Registry::get_instance(); $block = $registry->get_registered( $request['name'] ); - if ( ! $block || ! $block instanceof WP_Block_Type) { - return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.' ), array( 'status' => 404 ) ); + if ( ! $block || ! $block instanceof WP_Block_Type ) { + return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.', 'gutenberg' ), array( 'status' => 404 ) ); } $atts = $this->prepare_attributes( $request->get_params() ); @@ -108,9 +109,9 @@ public function get_item_output( $request ) { */ public function prepare_attributes( $attributes ) { foreach ( $attributes as $key => $value ) { - if ( "false" === $value ) { + if ( 'false' === $value ) { $attributes[ $key ] = false; - } elseif ( "true" === $value ) { + } elseif ( 'true' === $value ) { $attributes[ $key ] = true; } } @@ -132,7 +133,7 @@ public function get_item_schema() { 'title' => 'blocks-renderer', 'type' => 'object', 'properties' => array( - 'output' => array( + 'output' => array( 'description' => __( 'The block\'s output.', 'gutenberg' ), 'type' => 'string', 'required' => true, @@ -140,4 +141,4 @@ public function get_item_schema() { ), ); } -} \ No newline at end of file +} diff --git a/phpunit/class-rest-blocks-renderer-controller-test.php b/phpunit/class-rest-blocks-renderer-controller-test.php index 1ec16891539bf..b1e5acc5e11d9 100644 --- a/phpunit/class-rest-blocks-renderer-controller-test.php +++ b/phpunit/class-rest-blocks-renderer-controller-test.php @@ -43,7 +43,7 @@ public static function wpSetUpBeforeClass( $factory ) { */ public function register_test_block() { register_block_type( self::$block_name, array( - 'attributes' => array( + 'attributes' => array( 'foo' => array( 'type' => 'string', ), From 395a16832292e35b1f4d28a9d2ad69ce716157a0 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 14 Mar 2018 11:37:32 +0200 Subject: [PATCH 08/29] Fix some jscs. --- components/server-side-render/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 40456b0309760..00b722ef86143 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -3,15 +3,12 @@ */ import { Component, - compose, - RawHTML + RawHTML, } from '@wordpress/element'; -import { Spinner } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; export class ServerSideRender extends Component { - constructor( props ) { super( props ); this.state = { @@ -69,4 +66,4 @@ export class ServerSideRender extends Component { } } -export default ServerSideRender; \ No newline at end of file +export default ServerSideRender; From 1d05c2d35fcd8ff48c04f59ff964bf2a2e768eff Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 14 Mar 2018 12:57:54 +0200 Subject: [PATCH 09/29] Show 'loading' when changing response. --- components/server-side-render/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 00b722ef86143..830c88e9648b9 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -28,6 +28,7 @@ export class ServerSideRender extends Component { } getOutput() { + this.setState( { response: {} } ); const { block } = this.props; const attributes = this.state.attributes; const apiURL = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/blocks-renderer/' + block, { From c40dd25770c990bdfa7a9cff77cb87cdec454f97 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 14 Mar 2018 14:10:20 +0200 Subject: [PATCH 10/29] Use wp.apiRequest for consistency. --- components/server-side-render/index.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 830c88e9648b9..bf64c0da6f94f 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -31,27 +31,21 @@ export class ServerSideRender extends Component { this.setState( { response: {} } ); const { block } = this.props; const attributes = this.state.attributes; - const apiURL = addQueryArgs( wpApiSettings.root + 'gutenberg/v1/blocks-renderer/' + block, { + const apiURL = addQueryArgs( '/gutenberg/v1/blocks-renderer/' + block, { ...attributes, _wpnonce: wpApiSettings.nonce, } ); - return window.fetch( apiURL, { - credentials: 'include', - } ).then( response => { - response.json().then( data => ( { - data: data, - status: response.status, - } ) ).then( res => { - if ( res.status === 200 ) { - this.setState( { response: res } ); - } - } ); + + return wp.apiRequest( { path: apiURL } ).then( response => { + if ( response && response.output ) { + this.setState( { response: response.output } ); + } } ); } render() { const response = this.state.response; - if ( response.isLoading || ! response.data ) { + if ( ! response.length ) { return (
@@ -60,9 +54,8 @@ export class ServerSideRender extends Component { ); } - const html = response.data.output; return ( - { html } + { response } ); } } From f323651b9e9994ab6ca4a239e3bae902ad5f2829 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 14 Mar 2018 11:26:20 -0700 Subject: [PATCH 11/29] Update ServerSideRender readme to note intended usage --- components/server-side-render/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md index c9c8becb2786e..b01d2c0f5f2f3 100644 --- a/components/server-side-render/README.md +++ b/components/server-side-render/README.md @@ -1,15 +1,16 @@ ServerSideRender ======= -ServerSideRender component is used for server-side-rendering preview in Gutenberg editor, specifically for dynamic blocks. +ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. +New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. ## Usage Render core/latest-posts preview. ```jsx ``` From c4abc690e3bca9508e0030c40849b593775a97be Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 14 Mar 2018 11:48:10 -0700 Subject: [PATCH 12/29] Rename classes, methods, and property to be more aligned with the rest of the REST API * Singularize "blocks renderer" to "block renderer" * Rename 'output' property to 'rendered' property. * Re-use get_item_permissions_check and get_item method names. --- components/server-side-render/README.md | 2 +- components/server-side-render/index.js | 2 +- ...ass-wp-rest-block-renderer-controller.php} | 22 ++++++------- lib/load.php | 2 +- lib/register.php | 2 +- ...s-rest-block-renderer-controller-test.php} | 31 +++++++------------ 6 files changed, 27 insertions(+), 34 deletions(-) rename lib/{class-wp-rest-blocks-renderer-controller.php => class-wp-rest-block-renderer-controller.php} (85%) rename phpunit/{class-rest-blocks-renderer-controller-test.php => class-rest-block-renderer-controller-test.php} (77%) diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md index b01d2c0f5f2f3..53d5f9f08c947 100644 --- a/components/server-side-render/README.md +++ b/components/server-side-render/README.md @@ -20,6 +20,6 @@ Render core/latest-posts preview. Output is using the `render_callback` set when defining the block. For example if `block="core/latest-posts"` as in the example then the output will match `render_callback` output of that block. ## API Endpoint -API endpoint for getting the output for ServerSideRender is `/gutenberg/v1/blocks-renderer/:block`. It accepts any params which are used as `attributes` for the block's `render_callback` method. +API endpoint for getting the output for ServerSideRender is `/gutenberg/v1/block-renderer/:block`. It accepts any params which are used as `attributes` for the block's `render_callback` method. diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index bf64c0da6f94f..944be4e69699b 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -31,7 +31,7 @@ export class ServerSideRender extends Component { this.setState( { response: {} } ); const { block } = this.props; const attributes = this.state.attributes; - const apiURL = addQueryArgs( '/gutenberg/v1/blocks-renderer/' + block, { + const apiURL = addQueryArgs( '/gutenberg/v1/block-renderer/' + block, { ...attributes, _wpnonce: wpApiSettings.nonce, } ); diff --git a/lib/class-wp-rest-blocks-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php similarity index 85% rename from lib/class-wp-rest-blocks-renderer-controller.php rename to lib/class-wp-rest-block-renderer-controller.php index 22885ec4f7cc7..79a7f0297a876 100644 --- a/lib/class-wp-rest-blocks-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -1,19 +1,19 @@ namespace for the namespace keyword. $this->namespace = 'gutenberg/v1'; - $this->rest_base = 'blocks-renderer'; + $this->rest_base = 'block-renderer'; } /** @@ -44,8 +44,8 @@ public function register_routes() { ), array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item_output' ), - 'permission_callback' => array( $this, 'get_item_output_permissions_check' ), + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), @@ -62,7 +62,7 @@ public function register_routes() { * * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ - public function get_item_output_permissions_check() { + public function get_item_permissions_check() { if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), @@ -81,7 +81,7 @@ public function get_item_output_permissions_check() { * @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_output( $request ) { + public function get_item( $request ) { if ( ! isset( $request['name'] ) ) { return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.', 'gutenberg' ), array( 'status' => 404 ) ); } @@ -96,7 +96,7 @@ public function get_item_output( $request ) { $atts = $this->prepare_attributes( $request->get_params() ); $data = array( - 'output' => $block->render( $atts ), + 'rendered' => $block->render( $atts ), ); return rest_ensure_response( $data ); } @@ -130,10 +130,10 @@ public function prepare_attributes( $attributes ) { public function get_item_schema() { return array( '$schema' => 'http://json-schema.org/schema#', - 'title' => 'blocks-renderer', + 'title' => 'block-renderer', 'type' => 'object', 'properties' => array( - 'output' => array( + 'rendered' => array( 'description' => __( 'The block\'s output.', 'gutenberg' ), 'type' => 'string', 'required' => true, diff --git a/lib/load.php b/lib/load.php index b6b4f870ed552..35f00281fb91e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -13,7 +13,7 @@ require dirname( __FILE__ ) . '/class-wp-block-type.php'; require dirname( __FILE__ ) . '/class-wp-block-type-registry.php'; require dirname( __FILE__ ) . '/class-wp-rest-blocks-controller.php'; -require dirname( __FILE__ ) . '/class-wp-rest-blocks-renderer-controller.php'; +require dirname( __FILE__ ) . '/class-wp-rest-block-renderer-controller.php'; require dirname( __FILE__ ) . '/blocks.php'; require dirname( __FILE__ ) . '/client-assets.php'; require dirname( __FILE__ ) . '/compat.php'; diff --git a/lib/register.php b/lib/register.php index bd5da8025e339..88cc7f421a4b3 100644 --- a/lib/register.php +++ b/lib/register.php @@ -447,7 +447,7 @@ function gutenberg_register_post_types() { * @since ? */ function gutenberg_register_rest_routes() { - $controller = new WP_REST_Blocks_Renderer_Controller(); + $controller = new WP_REST_Block_Renderer_Controller(); $controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_rest_routes' ); diff --git a/phpunit/class-rest-blocks-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php similarity index 77% rename from phpunit/class-rest-blocks-renderer-controller-test.php rename to phpunit/class-rest-block-renderer-controller-test.php index b1e5acc5e11d9..9267893da7fe4 100644 --- a/phpunit/class-rest-blocks-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -1,14 +1,14 @@ server->get_routes(); - $this->assertArrayHasKey( '/gutenberg/v1/blocks-renderer/(?P[\w-]+\/[\w-]+)', $routes ); + $this->assertArrayHasKey( '/gutenberg/v1/block-renderer/(?P[\w-]+\/[\w-]+)', $routes ); } /** * Test getting item without permissions. */ - public function test_get_item_output_without_permissions() { + public function test_get_item_without_permissions() { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/blocks-renderer/' . self::$block_name ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, rest_authorization_required_code() ); @@ -96,9 +96,9 @@ public function test_get_item_output_without_permissions() { /** * Test getting item with invalid block name. */ - public function test_get_item_output_invalid_block_name() { + public function test_get_item_invalid_block_name() { wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/blocks-renderer/core/123' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/core/123' ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'rest_block_invalid_name', $response, 404 ); @@ -106,22 +106,22 @@ public function test_get_item_output_invalid_block_name() { /** * Check getting the correct block output. - * Test get_item_output(). + * Test get_item(). * * @covers test_get_item(). */ - public function test_get_item_output() { + public function test_get_item() { $this->register_test_block(); wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/blocks-renderer/' . self::$block_name ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); $request->set_param( 'foo', 'bar' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'Expected test result', $data['output'] ); + $this->assertEquals( 'Expected test result', $data['rendered'] ); } @@ -146,13 +146,6 @@ public function test_delete_item() { $this->markTestSkipped(); } - /** - * NA. - */ - public function test_get_item() { - $this->markTestSkipped(); - } - /** * NA. */ From 655787740e2d21e57142642f3b0cece83c3691d7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 14 Mar 2018 15:56:33 -0700 Subject: [PATCH 13/29] Register block-renderer endpoint for each dynamic block * Supply block attributes schema as endpoint schema. * Introduce attributes endpoint property and let REST API schema validate and sanitize them. * Ensure that attribute values are sanitized in addition to validated. --- lib/class-wp-block-type-registry.php | 4 +- lib/class-wp-block-type.php | 2 +- ...lass-wp-rest-block-renderer-controller.php | 88 ++++---- ...ss-rest-block-renderer-controller-test.php | 189 ++++++++++++++---- 4 files changed, 195 insertions(+), 88 deletions(-) diff --git a/lib/class-wp-block-type-registry.php b/lib/class-wp-block-type-registry.php index 2e91b53933a72..f95dec6ce4b9d 100644 --- a/lib/class-wp-block-type-registry.php +++ b/lib/class-wp-block-type-registry.php @@ -17,7 +17,7 @@ final class WP_Block_Type_Registry { * * @since 0.6.0 * @access private - * @var array + * @var WP_Block_Type[] */ private $registered_block_types = array(); @@ -142,7 +142,7 @@ public function get_registered( $name ) { * @since 0.6.0 * @access public * - * @return array Associative array of `$block_type_name => $block_type` pairs. + * @return WP_Block_Type[] Associative array of `$block_type_name => $block_type` pairs. */ public function get_all_registered() { return $this->registered_block_types; diff --git a/lib/class-wp-block-type.php b/lib/class-wp-block-type.php index 39f08e54fddc2..84aa40aed4e47 100644 --- a/lib/class-wp-block-type.php +++ b/lib/class-wp-block-type.php @@ -137,7 +137,7 @@ public function prepare_attributes_for_render( $attributes ) { if ( isset( $attributes[ $attribute_name ] ) ) { $is_valid = rest_validate_value_from_schema( $attributes[ $attribute_name ], $schema ); if ( ! is_wp_error( $is_valid ) ) { - $value = $attributes[ $attribute_name ]; + $value = rest_sanitize_value_from_schema( $attributes[ $attribute_name ], $schema ); } } diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index 79a7f0297a876..b2314087e99fe 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -28,30 +28,44 @@ public function __construct() { } /** - * Registers the necessary REST API routes. + * Registers the necessary REST API routes, one for each dynamic block. * * @access public */ public function register_routes() { - // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. - register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+\/[\w-]+)', array( - 'args' => array( - 'name' => array( - 'description' => __( 'Unique registered name for the block.', 'gutenberg' ), - 'type' => 'string', + $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); + foreach ( $block_types as $block_type ) { + if ( ! $block_type->is_dynamic() ) { + continue; + } + + // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P' . $block_type->name . ')', array( + 'args' => array( + 'name' => array( + 'description' => __( 'Unique registered name for the block.', 'gutenberg' ), + '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' ) ), + 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', 'gutenberg' ), $block_type->name ), + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => $block_type->attributes, + ), + ), ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } } /** @@ -60,9 +74,10 @@ public function register_routes() { * @since ? * @access public * + * @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() { + public function get_item_permissions_check( $request ) { if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), @@ -82,43 +97,14 @@ public function get_item_permissions_check() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { - if ( ! isset( $request['name'] ) ) { - return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.', 'gutenberg' ), array( 'status' => 404 ) ); - } - $registry = WP_Block_Type_Registry::get_instance(); $block = $registry->get_registered( $request['name'] ); - - if ( ! $block || ! $block instanceof WP_Block_Type ) { - return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.', 'gutenberg' ), array( 'status' => 404 ) ); - } - - $atts = $this->prepare_attributes( $request->get_params() ); - - $data = array( - 'rendered' => $block->render( $atts ), + $data = array( + 'rendered' => $block->render( $request->get_param( 'attributes' ) ), ); return rest_ensure_response( $data ); } - /** - * Fix potential boolean value issues. The values come as strings and "false" and "true" might generate issues if left like this. - * - * @param array $attributes Attributes. - * @return mixed Attributes. - */ - public function prepare_attributes( $attributes ) { - foreach ( $attributes as $key => $value ) { - if ( 'false' === $value ) { - $attributes[ $key ] = false; - } elseif ( 'true' === $value ) { - $attributes[ $key ] = true; - } - } - - return $attributes; - } - /** * Retrieves block's output schema, conforming to JSON Schema. * @@ -130,11 +116,11 @@ public function prepare_attributes( $attributes ) { public function get_item_schema() { return array( '$schema' => 'http://json-schema.org/schema#', - 'title' => 'block-renderer', + 'title' => 'rendered-block', 'type' => 'object', 'properties' => array( 'rendered' => array( - 'description' => __( 'The block\'s output.', 'gutenberg' ), + 'description' => __( 'The rendered block.', 'gutenberg' ), 'type' => 'string', 'required' => true, ), diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php index 9267893da7fe4..a40133bb67c89 100644 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -7,6 +7,8 @@ /** * Tests for WP_REST_Block_Renderer_Controller. + * + * @covers WP_REST_Block_Renderer_Controller */ class REST_Block_Renderer_Controller_Test extends WP_Test_REST_Controller_Testcase { @@ -30,7 +32,6 @@ class REST_Block_Renderer_Controller_Test extends WP_Test_REST_Controller_Testca * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. */ public static function wpSetUpBeforeClass( $factory ) { - self::$user_id = $factory->user->create( array( 'role' => 'editor', @@ -38,14 +39,49 @@ public static function wpSetUpBeforeClass( $factory ) { ); } + /** + * Delete test data after our tests run. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$user_id ); + } + + /** + * Set up. + * + * @see gutenberg_register_rest_routes() + */ + public function setUp() { + $this->register_test_block(); + parent::setUp(); + } + + /** + * Tear down. + */ + public function tearDown() { + WP_Block_Type_Registry::get_instance()->unregister( self::$block_name ); + parent::tearDown(); + } + /** * Register test block. */ public function register_test_block() { register_block_type( self::$block_name, array( 'attributes' => array( - 'foo' => array( - 'type' => 'string', + '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' ), @@ -56,37 +92,35 @@ public function register_test_block() { * Test render callback. * * @param array $attributes Props. - * @return bool|string + * @return string Rendered attributes, which is here just JSON. */ public function render_test_block( $attributes ) { - if ( isset( $attributes['foo'] ) ) { - return 'Expected test result'; - } else { - return false; - } - } - - /** - * Delete test data after our tests run. - */ - public static function wpTearDownAfterClass() { - self::delete_user( self::$user_id ); + return wp_json_encode( $attributes ); } /** * Check that the route was registered properly. + * + * @covers WP_REST_Block_Renderer_Controller::register_routes() */ public function test_register_routes() { - $routes = $this->server->get_routes(); + $dynamic_block_names = get_dynamic_block_names(); + $this->assertContains( self::$block_name, $dynamic_block_names ); - $this->assertArrayHasKey( '/gutenberg/v1/block-renderer/(?P[\w-]+\/[\w-]+)', $routes ); + $routes = $this->server->get_routes(); + foreach ( $dynamic_block_names as $dynamic_block_name ) { + $this->assertArrayHasKey( "/gutenberg/v1/block-renderer/(?P$dynamic_block_name)", $routes ); + } } /** * Test getting item without permissions. + * + * @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', '/gutenberg/v1/block-renderer/' . self::$block_name ); $response = $this->server->dispatch( $request ); @@ -95,68 +129,155 @@ public function test_get_item_without_permissions() { /** * Test getting item with invalid block name. + * + * @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', '/gutenberg/v1/block-renderer/core/123' ); $response = $this->server->dispatch( $request ); - $this->assertErrorResponse( 'rest_block_invalid_name', $response, 404 ); + $this->assertErrorResponse( 'rest_no_route', $response, 404 ); } /** - * Check getting the correct block output. - * Test get_item(). + * Check getting item with an invalid param provided. * - * @covers test_get_item(). + * @covers WP_REST_Block_Renderer_Controller::get_item() */ - public function test_get_item() { - $this->register_test_block(); + public function test_get_item_invalid_attribute() { wp_set_current_user( self::$user_id ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'attributes', array( + 'some_string' => array( 'no!' ), + ) ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + /** + * Check getting item with an invalid param provided. + * + * @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', '/gutenberg/v1/block-renderer/' . self::$block_name ); - $request->set_param( 'foo', 'bar' ); + $request->set_param( 'attributes', array( + 'unrecognized' => 'yes', + ) ); $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Check getting item with default attributes provided. + * + * @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', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'attributes', array() ); + $response = $this->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. + * + * @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', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'attributes', $attributes ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'Expected test result', $data['rendered'] ); + $this->assertEquals( $expected_attributes, json_decode( $data['rendered'], true ) ); + $this->assertEquals( + json_decode( $block_type->render( $attributes ), true ), + json_decode( $data['rendered'], true ) + ); } /** - * NA. + * Get item schema. + * + * @covers WP_REST_Block_Renderer_Controller::get_item_schema() */ - public function test_update_item() { - $this->markTestSkipped(); + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEqualSets( array( 'GET' ), $data['endpoints'][0]['methods'] ); + $this->assertEqualSets( + array( 'name', 'context', 'attributes' ), + 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'] ); } /** * NA. */ - public function test_create_item() { + public function test_update_item() { $this->markTestSkipped(); } /** * NA. */ - public function test_delete_item() { + public function test_create_item() { $this->markTestSkipped(); } /** * NA. */ - public function test_get_items() { + public function test_delete_item() { $this->markTestSkipped(); } /** * NA. */ - public function test_get_item_schema() { + public function test_get_items() { $this->markTestSkipped(); } @@ -164,7 +285,7 @@ public function test_get_item_schema() { * NA. */ public function test_context_param() { - $this->markTestSkipped(); + $this->markTestIncomplete(); } /** From 228c75688f14aed5c1ec10536b0d308947ed42e9 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Fri, 16 Mar 2018 13:15:14 +0200 Subject: [PATCH 14/29] Fixes to README. Use isEqual. Renaming method. --- components/server-side-render/README.md | 9 +++--- components/server-side-render/index.js | 25 +++++++++------- ...lass-wp-rest-block-renderer-controller.php | 2 -- ...ss-rest-block-renderer-controller-test.php | 30 ++++--------------- 4 files changed, 24 insertions(+), 42 deletions(-) diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md index 53d5f9f08c947..ae6be3fbf55a1 100644 --- a/components/server-side-render/README.md +++ b/components/server-side-render/README.md @@ -3,11 +3,11 @@ ServerSideRender ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. -New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. +New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. ## Usage -Render core/latest-posts preview. +Render core/archives preview. ```jsx { - if ( response && response.output ) { - this.setState( { response: response.output } ); + if ( response && response.rendered ) { + this.setState( { response: response.rendered } ); } } ); } diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index b2314087e99fe..49787cfc40e81 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -21,7 +21,6 @@ class WP_REST_Block_Renderer_Controller extends WP_REST_Controller { * @access public */ public function __construct() { - // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. $this->namespace = 'gutenberg/v1'; $this->rest_base = 'block-renderer'; @@ -33,7 +32,6 @@ public function __construct() { * @access public */ 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() ) { diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php index a40133bb67c89..01f05a1eaa6cb 100644 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -253,45 +253,27 @@ public function test_get_item_schema() { $this->arrayHasKey( 'string', $data['schema']['properties']['rendered']['type'] ); } - /** - * NA. - */ public function test_update_item() { - $this->markTestSkipped(); + $this->markTestSkipped( 'Controller doesn\'t implement update_item().' ); } - /** - * NA. - */ public function test_create_item() { - $this->markTestSkipped(); + $this->markTestSkipped( 'Controller doesn\'t implement create_item().' ); } - /** - * NA. - */ public function test_delete_item() { - $this->markTestSkipped(); + $this->markTestSkipped( 'Controller doesn\'t implement delete_item().' ); } - /** - * NA. - */ public function test_get_items() { - $this->markTestSkipped(); + $this->markTestSkipped( 'Controller doesn\'t implement get_items().' ); } - /** - * NA. - */ public function test_context_param() { - $this->markTestIncomplete(); + $this->markTestIncomplete( 'Controller doesn\'t implement context_param().' ); } - /** - * NA. - */ public function test_prepare_item() { - $this->markTestSkipped(); + $this->markTestSkipped( 'Controller doesn\'t implement prepare_item().' ); } } From b48ef816e615644b07455273936da2a572ac76cc Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Fri, 16 Mar 2018 14:56:06 +0200 Subject: [PATCH 15/29] Fix checking for existing response. --- components/server-side-render/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 7b073a42d46d1..6399f5db0dced 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -48,7 +48,7 @@ export class ServerSideRender extends Component { render() { const response = this.state.response; - if ( ! response.length ) { + if ( ! response || ! response.length ) { return (
From e39332d7468cd0eb81ec8d24a7093bf3b4d6698c Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Fri, 16 Mar 2018 15:52:42 +0200 Subject: [PATCH 16/29] Add putting together query URL from object. --- components/server-side-render/index.js | 30 ++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 6399f5db0dced..314b0c3f883d9 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -11,7 +11,6 @@ import { RawHTML, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; export class ServerSideRender extends Component { constructor( props ) { @@ -34,11 +33,17 @@ export class ServerSideRender extends Component { fetch() { this.setState( { response: null } ); const { block } = this.props; - const apiURL = addQueryArgs( '/gutenberg/v1/block-renderer/' + block, { - ...this.props, - _wpnonce: wpApiSettings.nonce, + const attributes = Object.assign( {}, this.props ); + + // Delete 'block' from attributes, only registered block attributes are allowed. + delete attributes[ 'block' ]; + + let apiURL = this.getQueryUrlFromObject( { + attributes: attributes, } ); + apiURL = '/gutenberg/v1/block-renderer/' + block + '?' + apiURL; + return wp.apiRequest( { path: apiURL } ).then( response => { if ( response && response.rendered ) { this.setState( { response: response.rendered } ); @@ -46,6 +51,23 @@ export class ServerSideRender extends Component { } ); } + getQueryUrlFromObject( obj, prefix ) { + let str = [], + param; + for ( param in obj ) { + if ( obj.hasOwnProperty( param ) ) { + let key = prefix ? prefix + '[' + param + ']' : param, + value = obj[ param ]; + str.push( + ( value !== null && 'object' === typeof value ) ? + this.getQueryUrlFromObject( value, key ) : + encodeURIComponent( key ) + '=' + encodeURIComponent( value ) + ); + } + } + return str.join( '&' ); + } + render() { const response = this.state.response; if ( ! response || ! response.length ) { From 46506a903f02ff4a48802a274636a77eaef59b6d Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Fri, 16 Mar 2018 16:02:26 +0200 Subject: [PATCH 17/29] Fix some jscs. --- components/server-side-render/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 314b0c3f883d9..0c05119e0e337 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -36,7 +36,7 @@ export class ServerSideRender extends Component { const attributes = Object.assign( {}, this.props ); // Delete 'block' from attributes, only registered block attributes are allowed. - delete attributes[ 'block' ]; + delete attributes.block; let apiURL = this.getQueryUrlFromObject( { attributes: attributes, @@ -52,16 +52,16 @@ export class ServerSideRender extends Component { } getQueryUrlFromObject( obj, prefix ) { - let str = [], - param; + const str = []; + let param; for ( param in obj ) { if ( obj.hasOwnProperty( param ) ) { - let key = prefix ? prefix + '[' + param + ']' : param, + const key = prefix ? prefix + '[' + param + ']' : param, value = obj[ param ]; str.push( ( value !== null && 'object' === typeof value ) ? - this.getQueryUrlFromObject( value, key ) : - encodeURIComponent( key ) + '=' + encodeURIComponent( value ) + this.getQueryUrlFromObject( value, key ) : + encodeURIComponent( key ) + '=' + encodeURIComponent( value ) ); } } From ce1f7c6915983f7269d0198e9bf21c9b5c2d57f9 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Fri, 16 Mar 2018 16:21:52 +0200 Subject: [PATCH 18/29] Fix using correct props with fetching. --- components/server-side-render/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 0c05119e0e337..1d9a9daeefeb0 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -21,19 +21,19 @@ export class ServerSideRender extends Component { } componentDidMount() { - this.fetch(); + this.fetch( this.props ); } componentWillReceiveProps( nextProps ) { if ( ! isEqual( nextProps, this.props ) ) { - this.fetch(); + this.fetch( nextProps ); } } - fetch() { + fetch( props ) { this.setState( { response: null } ); - const { block } = this.props; - const attributes = Object.assign( {}, this.props ); + const { block } = props; + const attributes = Object.assign( {}, props ); // Delete 'block' from attributes, only registered block attributes are allowed. delete attributes.block; From e0fec83b254732ede34a69e54eaf531b86360528 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Sat, 17 Mar 2018 13:33:03 +0200 Subject: [PATCH 19/29] Improve putting together query URL. Use attributes as separate param. --- components/server-side-render/README.md | 2 +- components/server-side-render/index.js | 28 +++++++------------------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md index ae6be3fbf55a1..5b360aa31764d 100644 --- a/components/server-side-render/README.md +++ b/components/server-side-render/README.md @@ -11,7 +11,7 @@ Render core/archives preview. ```jsx ``` diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 1d9a9daeefeb0..71364378149da 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -1,7 +1,7 @@ /** * External dependencies. */ -import { isEqual } from 'lodash'; +import { isEqual, isObject, map } from 'lodash'; /** * WordPress dependencies @@ -32,11 +32,7 @@ export class ServerSideRender extends Component { fetch( props ) { this.setState( { response: null } ); - const { block } = props; - const attributes = Object.assign( {}, props ); - - // Delete 'block' from attributes, only registered block attributes are allowed. - delete attributes.block; + const { block, attributes } = props; let apiURL = this.getQueryUrlFromObject( { attributes: attributes, @@ -52,20 +48,12 @@ export class ServerSideRender extends Component { } getQueryUrlFromObject( obj, prefix ) { - const str = []; - let param; - for ( param in obj ) { - if ( obj.hasOwnProperty( param ) ) { - const key = prefix ? prefix + '[' + param + ']' : param, - value = obj[ param ]; - str.push( - ( value !== null && 'object' === typeof value ) ? - this.getQueryUrlFromObject( value, key ) : - encodeURIComponent( key ) + '=' + encodeURIComponent( value ) - ); - } - } - return str.join( '&' ); + return map( obj, ( paramValue, paramName ) => { + const key = prefix ? prefix + '[' + paramName + ']' : paramName, + value = obj[ paramName ]; + return isObject( paramValue ) ? this.getQueryUrlFromObject( value, key ) : + encodeURIComponent( key ) + '=' + encodeURIComponent( value ) + } ).join( '&' ); } render() { From 17e404a84e119b79ba9c22e9e6119684d8fb6e4c Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Sat, 17 Mar 2018 13:45:10 +0200 Subject: [PATCH 20/29] Make path constant more readable. --- components/server-side-render/index.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 71364378149da..c0d4f2f7f3651 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -34,13 +34,9 @@ export class ServerSideRender extends Component { this.setState( { response: null } ); const { block, attributes } = props; - let apiURL = this.getQueryUrlFromObject( { - attributes: attributes, - } ); - - apiURL = '/gutenberg/v1/block-renderer/' + block + '?' + apiURL; + const path = '/gutenberg/v1/block-renderer/' + block + '?' + this.getQueryUrlFromObject( { attributes } ); - return wp.apiRequest( { path: apiURL } ).then( response => { + return wp.apiRequest( { path: path } ).then( response => { if ( response && response.rendered ) { this.setState( { response: response.rendered } ); } @@ -52,7 +48,7 @@ export class ServerSideRender extends Component { const key = prefix ? prefix + '[' + paramName + ']' : paramName, value = obj[ paramName ]; return isObject( paramValue ) ? this.getQueryUrlFromObject( value, key ) : - encodeURIComponent( key ) + '=' + encodeURIComponent( value ) + encodeURIComponent( key ) + '=' + encodeURIComponent( value ); } ).join( '&' ); } From 39b867a415331a18f64fd3017f20b11a951fce5f Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Thu, 29 Mar 2018 17:29:21 +0300 Subject: [PATCH 21/29] Add setting global if is present. --- ...lass-wp-rest-block-renderer-controller.php | 25 +++++-- ...ss-rest-block-renderer-controller-test.php | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index 49787cfc40e81..ddb66f14571de 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -76,10 +76,27 @@ public function register_routes() { * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'edit_posts' ) ) { - return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( - 'status' => rest_authorization_required_code(), - ) ); + 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( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks of this post', 'gutenberg' ), array( + 'status' => rest_authorization_required_code(), + ) ); + } + + // Set up postdata since this will be needed if post_ID was set. + setup_postdata( $post ); + + } else { + if ( ! current_user_can( 'edit_posts' ) ) { + return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( + 'status' => rest_authorization_required_code(), + ) ); + } } return true; diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php index 01f05a1eaa6cb..4b0af8099dac8 100644 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -19,6 +19,13 @@ class REST_Block_Renderer_Controller_Test extends WP_Test_REST_Controller_Testca */ protected static $block_name = 'core/test-block'; + /** + * Test post context block's name. + * + * @var string + */ + protected static $context_block_name = 'core/context-test-block'; + /** * Test API user's ID. * @@ -26,6 +33,13 @@ class REST_Block_Renderer_Controller_Test extends WP_Test_REST_Controller_Testca */ protected static $user_id; + /** + * Test post ID. + * + * @var int + */ + protected static $post_id; + /** * Create test data before the tests run. * @@ -37,6 +51,10 @@ public static function wpSetUpBeforeClass( $factory ) { 'role' => 'editor', ) ); + + self::$post_id = $factory->post->create( array( + 'post_title' => 'Test Post', + ) ); } /** @@ -53,6 +71,7 @@ public static function wpTearDownAfterClass() { */ public function setUp() { $this->register_test_block(); + $this->register_post_context_test_block(); parent::setUp(); } @@ -61,6 +80,7 @@ public function setUp() { */ 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(); } @@ -88,6 +108,16 @@ public function register_test_block() { ) ); } + /** + * Register test block with post_id as attribute for post context test. + */ + 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. * @@ -98,6 +128,15 @@ public function render_test_block( $attributes ) { return wp_json_encode( $attributes ); } + /** + * Test render callback for testing post context. + * + * @return string + */ + public function render_post_context_test_block() { + return get_the_title(); + } + /** * Check that the route was registered properly. * @@ -229,6 +268,33 @@ public function test_get_item() { ); } + /** + * Test getting item with post context. + */ + 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', '/gutenberg/v1/block-renderer/' . self::$context_block_name ); + + // Test without post ID. + $response = $this->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 = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertEquals( $expected_title, $data['rendered'] ); + } + /** * Get item schema. * From 3c60d4b1c668cfca524605dce8fca208273e2fce Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Fri, 6 Apr 2018 17:35:38 +0300 Subject: [PATCH 22/29] Update Readme. --- components/server-side-render/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md index 5b360aa31764d..041abc4a61b03 100644 --- a/components/server-side-render/README.md +++ b/components/server-side-render/README.md @@ -1,7 +1,13 @@ ServerSideRender ======= -ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. +ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be mostly limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. + +Usage of ServerSideRender component could also be justified in the following two cases: +* Lack of potential to take some existing widget-like functionality and improve its user-facing editing experience. +* Unwillingness to create a full JS experience for a block considered legacy. + +Note that ServerSideRender should be regarded as a fallback. New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. From a10bfacd4de9ff159807ecf1f05d13323a7eecef Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Mon, 16 Apr 2018 22:33:02 +0300 Subject: [PATCH 23/29] Register post_id. Add context. Fix tests. --- ...lass-wp-rest-block-renderer-controller.php | 28 +++++++++++++---- ...ss-rest-block-renderer-controller-test.php | 30 ++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index ddb66f14571de..8ea05e3872ce6 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -59,6 +59,10 @@ public function register_routes() { 'additionalProperties' => false, 'properties' => $block_type->attributes, ), + 'post_id' => array( + 'description' => __( 'ID of the post context.', 'travel' ), + 'type' => 'integer', + ), ), ), 'schema' => array( $this, 'get_public_item_schema' ), @@ -87,10 +91,6 @@ public function get_item_permissions_check( $request ) { 'status' => rest_authorization_required_code(), ) ); } - - // Set up postdata since this will be needed if post_ID was set. - setup_postdata( $post ); - } else { if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( @@ -112,9 +112,26 @@ public function get_item_permissions_check( $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'] ); - $data = array( + + if ( null === $block ) { + return new WP_Error( 'gutenberg_block_invalid', __( 'Invalid block.', 'gutenberg' ), array( + 'status' => 404 + ) ); + } + + $data = array( 'rendered' => $block->render( $request->get_param( 'attributes' ) ), ); return rest_ensure_response( $data ); @@ -138,6 +155,7 @@ public function get_item_schema() { 'description' => __( 'The rendered block.', 'gutenberg' ), 'type' => 'string', 'required' => true, + 'context' => array( 'edit' ), ), ), ); diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php index 4b0af8099dac8..1d1b19b735efc 100644 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -160,12 +160,26 @@ public function test_register_routes() { public function test_get_item_without_permissions() { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'context', 'edit' ); + $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, rest_authorization_required_code() ); } + /** + * Test getting item without 'edit' context. + */ + public function test_get_item_with_invalid_context() { + wp_set_current_user( self::$user_id ); + + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + /** * Test getting item with invalid block name. * @@ -173,7 +187,9 @@ public function test_get_item_without_permissions() { */ public function test_get_item_invalid_block_name() { wp_set_current_user( self::$user_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/core/123' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/core/123' ); + + $request->set_param( 'context', 'edit' ); $response = $this->server->dispatch( $request ); $this->assertErrorResponse( 'rest_no_route', $response, 404 ); @@ -187,6 +203,7 @@ public function test_get_item_invalid_block_name() { public function test_get_item_invalid_attribute() { wp_set_current_user( self::$user_id ); $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'context', 'edit' ); $request->set_param( 'attributes', array( 'some_string' => array( 'no!' ), ) ); @@ -202,6 +219,7 @@ public function test_get_item_invalid_attribute() { public function test_get_item_unrecognized_attribute() { wp_set_current_user( self::$user_id ); $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'context', 'edit' ); $request->set_param( 'attributes', array( 'unrecognized' => 'yes', ) ); @@ -224,6 +242,7 @@ public function test_get_item_default_attributes() { } $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'context', 'edit' ); $request->set_param( 'attributes', array() ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); @@ -256,6 +275,7 @@ public function test_get_item() { $expected_attributes['some_array'] = array_map( 'intval', $expected_attributes['some_array'] ); $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$block_name ); + $request->set_param( 'context', 'edit' ); $request->set_param( 'attributes', $attributes ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); @@ -276,6 +296,7 @@ public function test_get_item_with_post_context() { $expected_title = 'Test Post'; $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$context_block_name ); + $request->set_param( 'context', 'edit' ); // Test without post ID. $response = $this->server->dispatch( $request ); @@ -307,7 +328,7 @@ public function test_get_item_schema() { $this->assertEqualSets( array( 'GET' ), $data['endpoints'][0]['methods'] ); $this->assertEqualSets( - array( 'name', 'context', 'attributes' ), + array( 'name', 'context', 'attributes', 'post_id' ), array_keys( $data['endpoints'][0]['args'] ) ); $this->assertEquals( 'object', $data['endpoints'][0]['args']['attributes']['type'] ); @@ -317,6 +338,7 @@ public function test_get_item_schema() { $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'] ); } public function test_update_item() { @@ -336,7 +358,7 @@ public function test_get_items() { } public function test_context_param() { - $this->markTestIncomplete( 'Controller doesn\'t implement context_param().' ); + $this->markTestSkipped( 'Controller doesn\'t implement context_param().' ); } public function test_prepare_item() { From a166324fc7c811ab8dee4678f91c7ac04e1225aa Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Mon, 16 Apr 2018 22:47:58 +0300 Subject: [PATCH 24/29] Fix CS. --- lib/class-wp-rest-block-renderer-controller.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index 8ea05e3872ce6..bf0e15e9b872f 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -59,9 +59,9 @@ public function register_routes() { 'additionalProperties' => false, 'properties' => $block_type->attributes, ), - 'post_id' => array( - 'description' => __( 'ID of the post context.', 'travel' ), - 'type' => 'integer', + 'post_id' => array( + 'description' => __( 'ID of the post context.', 'gutenberg' ), + 'type' => 'integer', ), ), ), @@ -127,7 +127,7 @@ public function get_item( $request ) { if ( null === $block ) { return new WP_Error( 'gutenberg_block_invalid', __( 'Invalid block.', 'gutenberg' ), array( - 'status' => 404 + 'status' => 404, ) ); } From 6d1ee653146b28727f6a20f388c1a55938a93ab8 Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 25 Apr 2018 13:13:40 +0300 Subject: [PATCH 25/29] Add more tests. Add exception for namespace to as ruleset. --- ...lass-wp-rest-block-renderer-controller.php | 14 +++--- phpcs.xml.dist | 3 ++ ...ss-rest-block-renderer-controller-test.php | 45 +++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index bf0e15e9b872f..5a8b95bdd0a41 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -21,7 +21,6 @@ class WP_REST_Block_Renderer_Controller extends WP_REST_Controller { * @access public */ public function __construct() { - // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. $this->namespace = 'gutenberg/v1'; $this->rest_base = 'block-renderer'; } @@ -38,7 +37,6 @@ public function register_routes() { continue; } - // @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P' . $block_type->name . ')', array( 'args' => array( 'name' => array( @@ -82,10 +80,10 @@ public function register_routes() { public function get_item_permissions_check( $request ) { global $post; - $post_ID = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; + $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; - if ( 0 < $post_ID ) { - $post = get_post( $post_ID ); + if ( 0 < $post_id ) { + $post = get_post( $post_id ); if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks of this post', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), @@ -114,10 +112,10 @@ public function get_item_permissions_check( $request ) { public function get_item( $request ) { global $post; - $post_ID = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; + $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; - if ( 0 < $post_ID ) { - $post = get_post( $post_ID ); + 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 ); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ba92711ca1228..c6b4ba4e8d634 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -20,6 +20,9 @@ ./phpunit gutenberg.php + + lib/class-wp-rest-block-renderer-controller.php + gutenberg.php diff --git a/phpunit/class-rest-block-renderer-controller-test.php b/phpunit/class-rest-block-renderer-controller-test.php index 1d1b19b735efc..338736c394834 100644 --- a/phpunit/class-rest-block-renderer-controller-test.php +++ b/phpunit/class-rest-block-renderer-controller-test.php @@ -40,6 +40,13 @@ class REST_Block_Renderer_Controller_Test extends WP_Test_REST_Controller_Testca */ protected static $post_id; + /** + * Author test user ID. + * + * @var int + */ + protected static $author_id; + /** * Create test data before the tests run. * @@ -52,6 +59,12 @@ public static function wpSetUpBeforeClass( $factory ) { ) ); + self::$author_id = $factory->user->create( + array( + 'role' => 'author', + ) + ); + self::$post_id = $factory->post->create( array( 'post_title' => 'Test Post', ) ); @@ -316,6 +329,38 @@ public function test_get_item_with_post_context() { $this->assertEquals( $expected_title, $data['rendered'] ); } + /** + * Test getting item with invalid post ID. + */ + public function test_get_item_without_permissions_invalid_post() { + wp_set_current_user( self::$user_id ); + + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$context_block_name ); + $request->set_param( 'context', 'edit' ); + + // Test with invalid post ID. + $request->set_param( 'post_id', PHP_INT_MAX ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, 403 ); + } + + /** + * Test getting item without permissions to edit context post. + */ + public function test_get_item_without_permissions_cannot_edit_post() { + wp_set_current_user( self::$author_id ); + + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/block-renderer/' . self::$context_block_name ); + $request->set_param( 'context', 'edit' ); + + // Test with private post ID. + $request->set_param( 'post_id', self::$post_id ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, 403 ); + } + /** * Get item schema. * From 2bbbeb7336c7aa0b4bcdc01e34e73450a123985f Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 25 Apr 2018 13:24:18 +0300 Subject: [PATCH 26/29] Fix eslint. --- components/server-side-render/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index c0d4f2f7f3651..561912a5c9508 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -36,7 +36,7 @@ export class ServerSideRender extends Component { const path = '/gutenberg/v1/block-renderer/' + block + '?' + this.getQueryUrlFromObject( { attributes } ); - return wp.apiRequest( { path: path } ).then( response => { + return wp.apiRequest( { path: path } ).then( ( response ) => { if ( response && response.rendered ) { this.setState( { response: response.rendered } ); } From 67af6b7c2a15732b75c784f75dd6973a19cfcdbf Mon Sep 17 00:00:00 2001 From: Miina Sikk Date: Wed, 25 Apr 2018 13:42:05 +0300 Subject: [PATCH 27/29] Add context param to API request. --- components/server-side-render/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/server-side-render/index.js b/components/server-side-render/index.js index 561912a5c9508..d0128f53c1b41 100644 --- a/components/server-side-render/index.js +++ b/components/server-side-render/index.js @@ -34,7 +34,7 @@ export class ServerSideRender extends Component { this.setState( { response: null } ); const { block, attributes } = props; - const path = '/gutenberg/v1/block-renderer/' + block + '?' + this.getQueryUrlFromObject( { attributes } ); + const path = '/gutenberg/v1/block-renderer/' + block + '?context=edit&' + this.getQueryUrlFromObject( { attributes } ); return wp.apiRequest( { path: path } ).then( ( response ) => { if ( response && response.rendered ) { From 007fb1da227d04c57726b17cd565ab73a0ea349b Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Thu, 26 Apr 2018 14:07:40 +1000 Subject: [PATCH 28/29] Add version numbers to the phpdocs --- lib/class-wp-rest-block-renderer-controller.php | 10 +++++----- lib/register.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-rest-block-renderer-controller.php b/lib/class-wp-rest-block-renderer-controller.php index 5a8b95bdd0a41..43f381e80170a 100644 --- a/lib/class-wp-rest-block-renderer-controller.php +++ b/lib/class-wp-rest-block-renderer-controller.php @@ -3,13 +3,13 @@ * Block Renderer REST API: WP_REST_Block_Renderer_Controller class * * @package gutenberg - * @since ? + * @since 2.8.0 */ /** * Controller which provides REST endpoint for rendering a block. * - * @since ? + * @since 2.8.0 * * @see WP_REST_Controller */ @@ -71,7 +71,7 @@ public function register_routes() { /** * Checks if a given request has access to read blocks. * - * @since ? + * @since 2.8.0 * @access public * * @param WP_REST_Request $request Request. @@ -103,7 +103,7 @@ public function get_item_permissions_check( $request ) { /** * Returns block output from block's registered render_callback. * - * @since ? + * @since 2.8.0 * @access public * * @param WP_REST_Request $request Full details about the request. @@ -138,7 +138,7 @@ public function get_item( $request ) { /** * Retrieves block's output schema, conforming to JSON Schema. * - * @since ? + * @since 2.8.0 * @access public * * @return array Item schema data. diff --git a/lib/register.php b/lib/register.php index 905922ccf6e1c..0103fd9ff9e0c 100644 --- a/lib/register.php +++ b/lib/register.php @@ -434,7 +434,7 @@ function gutenberg_register_post_types() { /** * Registers the REST API routes needed by the Gutenberg editor. * - * @since ? + * @since 2.8.0 */ function gutenberg_register_rest_routes() { $controller = new WP_REST_Block_Renderer_Controller(); From 1c9ef5616928357a67137ef42037b1c3c93a505e Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Thu, 26 Apr 2018 14:07:53 +1000 Subject: [PATCH 29/29] Tweak the README a little --- components/server-side-render/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/server-side-render/README.md b/components/server-side-render/README.md index 041abc4a61b03..02c507ca3819d 100644 --- a/components/server-side-render/README.md +++ b/components/server-side-render/README.md @@ -1,19 +1,18 @@ ServerSideRender ======= -ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be mostly limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. +ServerSideRender is a component used for server-side rendering a preview of dynamic blocks to display in the editor. Server-side rendering in a block's `edit` function should be limited to blocks that are heavily dependent on existing PHP rendering logic that is heavily intertwined with data, particularly when there are no endpoints available. -Usage of ServerSideRender component could also be justified in the following two cases: -* Lack of potential to take some existing widget-like functionality and improve its user-facing editing experience. -* Unwillingness to create a full JS experience for a block considered legacy. +ServerSideRender may also be used when a legacy block is provided as a backwards compatibility measure, rather than needing to re-write the deprecated code that the block may depend on. -Note that ServerSideRender should be regarded as a fallback. +ServerSideRender should be regarded as a fallback or legacy mechanism, it is not appropriate for developing new features against. -New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. +New blocks should be built in conjunction with any necessary REST API endpoints, so that JavaScript can be used for rendering client-side in the `edit` function. This gives the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint, so that both the client-side JavaScript and server-side PHP logic should require a mininal amount of differences. ## Usage Render core/archives preview. + ```jsx