From cc65001816a51427455e7b795b2eb144629d1a48 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Apr 2020 23:29:19 -0600 Subject: [PATCH 1/7] Fork the from Core, filter the REST API This should at least temporarily fix two recurring issues in GitHub issues and wp.org support topics. --- js/blocks/components/edit.js | 8 +- js/blocks/components/index.js | 1 + js/blocks/components/server-side-render.js | 204 +++++++++++++++++++++ package-lock.json | 2 +- package.json | 1 - php/blocks/class-rest.php | 92 ++++++++++ php/class-plugin.php | 1 + 7 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 js/blocks/components/server-side-render.js create mode 100644 php/blocks/class-rest.php diff --git a/js/blocks/components/edit.js b/js/blocks/components/edit.js index bdf2fd76d..31c222ebb 100644 --- a/js/blocks/components/edit.js +++ b/js/blocks/components/edit.js @@ -1,12 +1,7 @@ -/** - * WordPress dependencies - */ -import ServerSideRender from '@wordpress/server-side-render'; - /** * Internal dependencies */ -import { BlockLabInspector, FormControls } from './'; +import { BlockLabInspector, FormControls, ServerSideRender } from './'; import icons from '../../../assets/icons.json'; /** @@ -38,6 +33,7 @@ const Edit = ( { blockProps, block } ) => { block={ `block-lab/${ block.name }` } attributes={ attributes } className="block-lab-editor__ssr" + requestBody={ true } /> ) } diff --git a/js/blocks/components/index.js b/js/blocks/components/index.js index a464122c4..aa92ff43c 100644 --- a/js/blocks/components/index.js +++ b/js/blocks/components/index.js @@ -6,4 +6,5 @@ export { default as Fields } from './fields'; export { default as FormControls } from './form-controls'; export { default as Image } from './image'; export { default as RepeaterRows } from './repeater-rows'; +export { default as ServerSideRender } from './server-side-render'; export { default as TinyMCE } from './tiny-mce'; diff --git a/js/blocks/components/server-side-render.js b/js/blocks/components/server-side-render.js new file mode 100644 index 000000000..4d1b06fdb --- /dev/null +++ b/js/blocks/components/server-side-render.js @@ -0,0 +1,204 @@ +/** + * Forked from Gutenberg, with a minor edit to allow using POST requests instead of GET requests. + * Todo: delete if this is merged: https://github.com/WordPress/gutenberg/pull/21068/ + * + * @see https://github.com/WordPress/gutenberg/blob/c72030189017c8aac44453c1386f4251e45e80df/packages/server-side-render/src/index.js + */ + +/** + * External dependencies + */ +import { isEqual, debounce } from 'lodash'; + +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { Placeholder, Spinner } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; +import { Component, RawHTML, useMemo } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Constants + */ +const EMPTY_OBJECT = {}; + +export function rendererPath( block, attributes = null, urlQueryArgs = {} ) { + return addQueryArgs( `/wp/v2/block-renderer/${ block }`, { + context: 'edit', + ...( null !== attributes ? { attributes } : {} ), + ...urlQueryArgs, + } ); +} + +export class ServerSideRender extends Component { + constructor( props ) { + super( props ); + this.state = { + response: null, + }; + } + + componentDidMount() { + this.isStillMounted = true; + this.fetch( this.props ); + // Only debounce once the initial fetch occurs to ensure that the first + // renders show data as soon as possible. + this.fetch = debounce( this.fetch, 500 ); + } + + componentWillUnmount() { + this.isStillMounted = false; + } + + componentDidUpdate( prevProps ) { + if ( ! isEqual( prevProps, this.props ) ) { + this.fetch( this.props ); + } + } + + fetch( props ) { + if ( ! this.isStillMounted ) { + return; + } + if ( null !== this.state.response ) { + this.setState( { response: null } ); + } + + const { + block, + attributes = null, + requestBody, + urlQueryArgs = {}, + } = props; + + // If requestBody, make a POST request, with the attributes in the request body instead of the URL. + // This allows sending a larger attributes object than in a GET request, where the attributes are in the URL. + const urlAttributes = requestBody ? null : attributes; + const path = rendererPath( block, urlAttributes, urlQueryArgs ); + const method = requestBody ? 'POST' : 'GET'; + const data = requestBody ? attributes : null; + + // Store the latest fetch request so that when we process it, we can + // check if it is the current request, to avoid race conditions on slow networks. + const fetchRequest = ( this.currentFetchRequest = apiFetch( { + path, + method, + data, + } ) + .then( ( response ) => { + if ( + this.isStillMounted && + fetchRequest === this.currentFetchRequest && + response + ) { + this.setState( { response: response.rendered } ); + } + } ) + .catch( ( error ) => { + if ( + this.isStillMounted && + fetchRequest === this.currentFetchRequest + ) { + this.setState( { + response: { + error: true, + errorMsg: error.message, + }, + } ); + } + } ) ); + return fetchRequest; + } + + render() { + const response = this.state.response; + const { + className, + EmptyResponsePlaceholder, + ErrorResponsePlaceholder, + LoadingResponsePlaceholder, + } = this.props; + + if ( response === '' ) { + return ( + + ); + } else if ( ! response ) { + return ( + + ); + } else if ( response.error ) { + return ( + + ); + } + + return ( + + { response } + + ); + } +} + +ServerSideRender.defaultProps = { + EmptyResponsePlaceholder: ( { className } ) => ( + + { __( 'Block rendered as empty.' ) } + + ), + ErrorResponsePlaceholder: ( { response, className } ) => { + const errorMessage = sprintf( + // translators: %s: error message describing the problem + __( 'Error loading block: %s' ), + response.errorMsg + ); + return ( + { errorMessage } + ); + }, + LoadingResponsePlaceholder: ( { className } ) => { + return ( + + + + ); + }, +}; + +export default withSelect( ( select ) => { + const coreEditorSelect = select( 'core/editor' ); + if ( coreEditorSelect ) { + const currentPostId = coreEditorSelect.getCurrentPostId(); + if ( currentPostId ) { + return { + currentPostId, + }; + } + } + return EMPTY_OBJECT; +} )( ( { urlQueryArgs = EMPTY_OBJECT, currentPostId, ...props } ) => { + const newUrlQueryArgs = useMemo( () => { + if ( ! currentPostId ) { + return urlQueryArgs; + } + return { + post_id: currentPostId, + ...urlQueryArgs, + }; + }, [ currentPostId, urlQueryArgs ] ); + + return ; +} ); diff --git a/package-lock.json b/package-lock.json index a4bbfde1d..ac1949516 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "block-lab", - "version": "1.5.3", + "version": "1.5.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9a2c7d264..41fdeaef0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@wordpress/i18n": "3.7.0", "@wordpress/keycodes": "2.7.0", "@wordpress/scripts": "6.1.1", - "@wordpress/server-side-render": "1.5.0", "@wordpress/url": "2.9.0", "autoprefixer": "9.7.3", "babel-core": "6.26.3", diff --git a/php/blocks/class-rest.php b/php/blocks/class-rest.php new file mode 100644 index 000000000..99e4c2fe1 --- /dev/null +++ b/php/blocks/class-rest.php @@ -0,0 +1,92 @@ + $handlers. + * @return array The filtered endpoints, with the Block Lab endpoints allowing POST requests. + */ + public function filter_block_endpoints( $endpoints ) { + foreach ( $endpoints as $route => $handler ) { + if ( 0 === strpos( $route, '/wp/v2/block-renderer/(?Pblock-lab/' ) && isset( $endpoints[ $route ][0] ) ) { + $endpoints[ $route ][0]['methods'] = [ 'GET', 'POST' ]; + $endpoints[ $route ][0]['callback'] = [ $this, 'get_item' ]; + } + } + + return $endpoints; + } + + /** + * Returns block output from block's registered render_callback. + * + * Forked from WP_REST_Block_Renderer_Controller::get_item(), with a simple change to process POST requests. + * + * @todo: revert this if this has been merged and enough version of Core have passed: https://github.com/WordPress/wordpress-develop/pull/196/ + * @see https://github.com/WordPress/wordpress-develop/blob/dfa959bbd58f13b504e269aad45412a85f74e491/src/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php#L121 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; + + if ( 0 < $post_id ) { + $GLOBALS['post'] = get_post( $post_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + // Set up postdata since this will be needed if post_id was set. + setup_postdata( $GLOBALS['post'] ); + } + $registry = \WP_Block_Type_Registry::get_instance(); + + if ( null === $registry->get_registered( $request['name'] ) ) { + return new WP_Error( + 'block_invalid', + __( 'Invalid block.', 'block-lab' ), + [ + 'status' => 404, + ] + ); + } + + $attributes = $request->get_param( 'attributes' ); + + // Create an array representation simulating the output of parse_blocks. + $block = [ + 'blockName' => $request['name'], + 'attrs' => $attributes, + 'innerHTML' => '', + 'innerContent' => [], + ]; + + // Render using render_block to ensure all relevant filters are used. + $data = [ + 'rendered' => render_block( $block ), + ]; + + return rest_ensure_response( $data ); + } +} diff --git a/php/class-plugin.php b/php/class-plugin.php index 2ee27d81b..0ca1e99de 100644 --- a/php/class-plugin.php +++ b/php/class-plugin.php @@ -50,6 +50,7 @@ public function init() { $this->util = new Util(); $this->register_component( $this->util ); $this->register_component( new Post_Types\Block_Post() ); + $this->register_component( new Blocks\Rest() ); $this->loader = new Blocks\Loader(); $this->register_component( $this->loader ); From abbe210ecd293071d8f046d3452996cb07734a88 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Apr 2020 23:31:16 -0600 Subject: [PATCH 2/7] Use 'block-lab' textdomain in forked ServerSideRender This should be more reliable, in case the strings change. --- js/blocks/components/server-side-render.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/blocks/components/server-side-render.js b/js/blocks/components/server-side-render.js index 4d1b06fdb..fc24ff13e 100644 --- a/js/blocks/components/server-side-render.js +++ b/js/blocks/components/server-side-render.js @@ -156,13 +156,13 @@ export class ServerSideRender extends Component { ServerSideRender.defaultProps = { EmptyResponsePlaceholder: ( { className } ) => ( - { __( 'Block rendered as empty.' ) } + { __( 'Block rendered as empty.', 'block-lab' ) } ), ErrorResponsePlaceholder: ( { response, className } ) => { const errorMessage = sprintf( // translators: %s: error message describing the problem - __( 'Error loading block: %s' ), + __( 'Error loading block: %s', 'block-lab' ), response.errorMsg ); return ( From 5b4f5099bc7327df15a468ceb82657d91ced6d8e Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Thu, 2 Apr 2020 23:46:59 -0600 Subject: [PATCH 3/7] Apply the fix from the Core patch to allow POST requests Before, this did not allow POST requests, so add this. --- php/blocks/class-rest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/php/blocks/class-rest.php b/php/blocks/class-rest.php index 99e4c2fe1..832612b05 100644 --- a/php/blocks/class-rest.php +++ b/php/blocks/class-rest.php @@ -32,7 +32,7 @@ public function register_hooks() { public function filter_block_endpoints( $endpoints ) { foreach ( $endpoints as $route => $handler ) { if ( 0 === strpos( $route, '/wp/v2/block-renderer/(?Pblock-lab/' ) && isset( $endpoints[ $route ][0] ) ) { - $endpoints[ $route ][0]['methods'] = [ 'GET', 'POST' ]; + $endpoints[ $route ][0]['methods'] = [ \WP_REST_Server::READABLE, \WP_REST_Server::CREATABLE ]; $endpoints[ $route ][0]['callback'] = [ $this, 'get_item' ]; } } @@ -72,7 +72,8 @@ public function get_item( $request ) { ); } - $attributes = $request->get_param( 'attributes' ); + // In a POST request, the attributes appear as JSON in the request body. + $attributes = \WP_REST_Server::CREATABLE === $request->get_method() ? json_decode( $request->get_body(), true ) : $request->get_param( 'attributes' ); // Create an array representation simulating the output of parse_blocks. $block = [ From 6cfe7c321e67b13039c4e1e54610f9bbc6744698 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Fri, 3 Apr 2020 22:12:42 -0600 Subject: [PATCH 4/7] Add a test class for Rest, including testing filtering Test that the filter of the endpoints works, and that the edited get_items() function works as expected. --- php/blocks/class-rest.php | 2 +- tests/php/unit/blocks/test-class-rest.php | 192 ++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 tests/php/unit/blocks/test-class-rest.php diff --git a/php/blocks/class-rest.php b/php/blocks/class-rest.php index 832612b05..9d1a45dd7 100644 --- a/php/blocks/class-rest.php +++ b/php/blocks/class-rest.php @@ -20,7 +20,7 @@ class Rest extends Component_Abstract { * Register all the hooks. */ public function register_hooks() { - add_action( 'rest_endpoints', [ $this, 'filter_block_endpoints' ] ); + add_filter( 'rest_endpoints', [ $this, 'filter_block_endpoints' ] ); } /** diff --git a/tests/php/unit/blocks/test-class-rest.php b/tests/php/unit/blocks/test-class-rest.php new file mode 100644 index 000000000..68795bfc9 --- /dev/null +++ b/tests/php/unit/blocks/test-class-rest.php @@ -0,0 +1,192 @@ + [ + 'methods' => [ 'GET' ], + 'callback' => [ 'example_callback' ], + ], + ]; + + /** + * Set up each test. + * + * @inheritdoc + */ + public function setUp() { + parent::setUp(); + $this->instance = new Rest(); + $this->register_mock_block(); + } + + /** + * Tears down after each test. + * + * @inheritdoc + */ + public function tearDown() { + unregister_block_type( $this->mock_block_name ); + parent::tearDown(); + } + + /** + * Registers the mock block. + * + * Mainly taken from REST_Block_Renderer_Controller_Test::register_test_block(). + */ + public function register_mock_block() { + register_block_type( + $this->mock_block_name, + [ + 'attributes' => [ + 'example_string' => [ + 'type' => 'string', + 'default' => 'some_default', + ], + 'example_int' => [ + 'type' => 'integer', + ], + ], + 'render_callback' => function( $attributes ) { + return wp_json_encode( $attributes ); + }, + ] + ); + } + + /** + * Test register_hooks. + * + * @covers \Block_Lab\Blocks\Rest::register_hooks() + */ + public function test_register_hooks() { + $this->instance->register_hooks(); + $filtered_endpoints = apply_filters( 'rest_endpoints', rest_get_server()->get_routes() ); + $block_route = $this->rest_api_route . '(?P' . $this->mock_block_name . ')'; + + $this->assertEquals( 10, has_action( 'rest_endpoints', [ $this->instance, 'filter_block_endpoints' ] ) ); + $this->assertEquals( [ 'GET', 'POST' ], $filtered_endpoints[ $block_route ][0]['methods'] ); + $this->assertEquals( [ $this->instance, 'get_item' ], $filtered_endpoints[ $block_route ][0]['callback'] ); + } + + /** + * Test filter_block_endpoints, when there is a non-Block Lab block. + * + * @covers \Block_Lab\Blocks\Rest::filter_block_endpoints() + */ + public function test_filter_block_endpoints_non_block_lab_block() { + $endpoints = [ + $this->rest_api_route . '/(?Pbaz-plugin/example-block-name)' => $this->mock_handler, + $this->rest_api_route . '/(?Panother-plugin/here-is-a-block-name)' => [], + ]; + + $this->assertEquals( + $endpoints, + $this->instance->filter_block_endpoints( $endpoints ) + ); + } + + /** + * Test filter_block_endpoints, when there is a Block Lab block. + * + * @covers \Block_Lab\Blocks\Rest::filter_block_endpoints() + */ + public function test_filter_block_endpoints_block_lab_block() { + $this->assertEquals( + [ + $this->rest_api_route . '(?Pblock-lab/main-hero)' => [ + 0 => [ + 'methods' => [ 'GET', 'POST' ], + 'callback' => [ $this->instance, 'get_item' ], + ], + ], + ], + $this->instance->filter_block_endpoints( + [ $this->rest_api_route . '(?Pblock-lab/main-hero)' => $this->mock_handler ] + ) + ); + } + + /** + * Gets the test data for test_get_item_post_request(). + * + * @return array The test data. + */ + public function get_data_test_get_item() { + return [ + 'get_request' => [ 'GET' ], + 'post_request' => [ 'POST' ], + ]; + } + + /** + * Test get_item. + * + * @dataProvider get_data_test_get_item + * @covers \Block_Lab\Blocks\Rest::get_item() + * + * @param string $request_type The type of request, like 'GET'. + */ + public function test_get_item( $request_type ) { + wp_set_current_user( $this->factory()->user->create( [ 'role' => 'editor' ] ) ); + + $string_attribute = 'Lorem ipsum dolor'; + $int_attribute = 200; + $attributes = [ + 'example_string' => $string_attribute, + 'example_int' => $int_attribute, + ]; + + $request = new WP_REST_Request( $request_type, $this->rest_api_route . $this->mock_block_name ); + $request->set_param( 'context', 'edit' ); + + if ( 'GET' === $request_type ) { + $request->set_param( 'attributes', $attributes ); + } elseif ( 'POST' === $request_type ) { + $request->set_body( wp_json_encode( $attributes ) ); + } + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $actual = $response->get_data()['rendered']; + $this->assertContains( $string_attribute, $actual ); + $this->assertContains( strval( $int_attribute ), $actual ); + } +} From 82b02ae6463651ef04045228583dc95917452494 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Fri, 3 Apr 2020 22:22:22 -0600 Subject: [PATCH 5/7] Add WP versions to the Travis build, may revert The Rest class uses classes from Core, and has an important filter of 'rest_endpoints' that needs to work with all supported versions of Core. --- .travis.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 96dde5da6..5d71f11b6 100755 --- a/.travis.yml +++ b/.travis.yml @@ -20,14 +20,20 @@ matrix: env: WP_VERSION=latest - php: 7.2 env: WP_VERSION=latest + - php: 7.2 + env: WP_VERSION=5.3 - php: 7.1 env: WP_VERSION=latest + - php: 7.1 + env: WP_VERSION=5.2 - php: 7.0 env: WP_VERSION=latest - - php: 5.6 - env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=5.1 - php: 5.6 env: WP_VERSION=trunk + - php: 5.6 + env: WP_VERSION=5.0 - php: 5.6 env: WP_TRAVISCI=lint - php: 7.3 From 5cfbfcb4d9f068c013e1f09547bd3f3d4920fe11 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Fri, 3 Apr 2020 23:03:07 -0600 Subject: [PATCH 6/7] Add lodash to package.json, as ServerSideRender uses it Also, make minor edits to PHPDoc summaries. --- package.json | 1 + tests/php/unit/blocks/test-class-rest.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 41fdeaef0..a8be4514c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "gulp-string-replace": "^1.1.2", "husky": "^4.0.10", "jest-silent-reporter": "0.1.2", + "lodash": "4.17.15", "merge-stream": "^2.0.0", "mini-css-extract-plugin": "0.8.2", "node-sass": "^4.13.1", diff --git a/tests/php/unit/blocks/test-class-rest.php b/tests/php/unit/blocks/test-class-rest.php index 68795bfc9..64b35dc3e 100644 --- a/tests/php/unit/blocks/test-class-rest.php +++ b/tests/php/unit/blocks/test-class-rest.php @@ -46,7 +46,7 @@ class Test_Rest extends \WP_UnitTestCase { ]; /** - * Set up each test. + * Sets up each test. * * @inheritdoc */ @@ -145,7 +145,7 @@ public function test_filter_block_endpoints_block_lab_block() { } /** - * Gets the test data for test_get_item_post_request(). + * Gets the test data for test_get_item(). * * @return array The test data. */ From b288d54192f85ae758b345b6487365072e5d7f21 Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Fri, 3 Apr 2020 23:13:41 -0600 Subject: [PATCH 7/7] Minor edits to PHP DocBlocks, including fixing a typo --- php/blocks/class-rest.php | 2 +- tests/php/unit/blocks/test-class-rest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/php/blocks/class-rest.php b/php/blocks/class-rest.php index 9d1a45dd7..fe0be4321 100644 --- a/php/blocks/class-rest.php +++ b/php/blocks/class-rest.php @@ -45,7 +45,7 @@ public function filter_block_endpoints( $endpoints ) { * * Forked from WP_REST_Block_Renderer_Controller::get_item(), with a simple change to process POST requests. * - * @todo: revert this if this has been merged and enough version of Core have passed: https://github.com/WordPress/wordpress-develop/pull/196/ + * @todo: revert if this has been merged and enough version of Core have passed: https://github.com/WordPress/wordpress-develop/pull/196/ * @see https://github.com/WordPress/wordpress-develop/blob/dfa959bbd58f13b504e269aad45412a85f74e491/src/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php#L121 * * @param WP_REST_Request $request Full details about the request. diff --git a/tests/php/unit/blocks/test-class-rest.php b/tests/php/unit/blocks/test-class-rest.php index 64b35dc3e..90e929490 100644 --- a/tests/php/unit/blocks/test-class-rest.php +++ b/tests/php/unit/blocks/test-class-rest.php @@ -107,7 +107,7 @@ public function test_register_hooks() { } /** - * Test filter_block_endpoints, when there is a non-Block Lab block. + * Test filter_block_endpoints, when there is no Block Lab block. * * @covers \Block_Lab\Blocks\Rest::filter_block_endpoints() */