Skip to content
This repository has been archived by the owner on Nov 6, 2022. It is now read-only.

Prevent an error for a block with a large attributes object #524

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new class Rest uses some classes from Core, and has an important filter of 'rest_endpoints' that needs to work with WP 5.0+

So I added more WP versions to the matrix. If this causes builds to take a lot longer, we could revert this change to .travis.yml.

- php: 5.6
env: WP_TRAVISCI=lint
- php: 7.3
Expand Down
8 changes: 2 additions & 6 deletions js/blocks/components/edit.js
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -38,6 +33,7 @@ const Edit = ( { blockProps, block } ) => {
block={ `block-lab/${ block.name }` }
attributes={ attributes }
className="block-lab-editor__ssr"
requestBody={ true }
/>
) }
</div>
Expand Down
1 change: 1 addition & 0 deletions js/blocks/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
204 changes: 204 additions & 0 deletions js/blocks/components/server-side-render.js
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied almost verbatim from Gutenberg.

index.js and server-side-render.js are copied into this file.

*/

/**
* 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,
} )
Comment on lines +77 to +90
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change from Gutenberg. It's what allows sending a POST request.

.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 (
<EmptyResponsePlaceholder
response={ response }
{ ...this.props }
/>
);
} else if ( ! response ) {
return (
<LoadingResponsePlaceholder
response={ response }
{ ...this.props }
/>
);
} else if ( response.error ) {
return (
<ErrorResponsePlaceholder
response={ response }
{ ...this.props }
/>
);
}

return (
<RawHTML key="html" className={ className }>
{ response }
</RawHTML>
);
}
}

ServerSideRender.defaultProps = {
EmptyResponsePlaceholder: ( { className } ) => (
<Placeholder className={ className }>
{ __( 'Block rendered as empty.', 'block-lab' ) }
</Placeholder>
),
ErrorResponsePlaceholder: ( { response, className } ) => {
const errorMessage = sprintf(
// translators: %s: error message describing the problem
__( 'Error loading block: %s', 'block-lab' ),
response.errorMsg
);
return (
<Placeholder className={ className }>{ errorMessage }</Placeholder>
);
},
LoadingResponsePlaceholder: ( { className } ) => {
return (
<Placeholder className={ className }>
<Spinner />
</Placeholder>
);
},
};

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 <ServerSideRender urlQueryArgs={ newUrlQueryArgs } { ...props } />;
} );
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 93 additions & 0 deletions php/blocks/class-rest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* REST API handling.
*
* @package Block_Lab
* @copyright Copyright(c) 2020, Block Lab
* @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
*/

namespace Block_Lab\Blocks;

use Block_Lab\Component_Abstract;

/**
* Class Block
*/
class Rest extends Component_Abstract {

/**
* Register all the hooks.
*/
public function register_hooks() {
add_filter( 'rest_endpoints', [ $this, 'filter_block_endpoints' ] );
}

/**
* Filters the Block Lab endpoints to allow POST requests.
*
* @param array $endpoints The REST API endpoints, an associative array of $route => $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/(?P<name>block-lab/' ) && isset( $endpoints[ $route ][0] ) ) {
$endpoints[ $route ][0]['methods'] = [ \WP_REST_Server::READABLE, \WP_REST_Server::CREATABLE ];
$endpoints[ $route ][0]['callback'] = [ $this, 'get_item' ];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should be removed.

}
}

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 ) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method shouldn't be needed, per the feedback in the Core patch: https://core.trac.wordpress.org/ticket/49680#comment:11

filter_block_endpoints() will probably still be needed for years, though.

$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,
]
);
}

// 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 = [
'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 );
}
}
1 change: 1 addition & 0 deletions php/class-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
Loading