Skip to content

Commit

Permalink
Add Server Side Render component and endpoint. (#5602)
Browse files Browse the repository at this point in the history
Fixes #780.
  • Loading branch information
miina authored and pento committed Apr 26, 2018
1 parent 5dafcbd commit d7ddb69
Show file tree
Hide file tree
Showing 10 changed files with 694 additions and 3 deletions.
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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';
Expand Down
30 changes: 30 additions & 0 deletions components/server-side-render/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ServerSideRender
=======

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.

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.

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. 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
<ServerSideRender
block="core/archives"
attributes={ this.props.attributes }
/>
```

## Output

Output uses the block's `render_callback` function, set when defining the block.

## API Endpoint

The 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.

72 changes: 72 additions & 0 deletions components/server-side-render/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* External dependencies.
*/
import { isEqual, isObject, map } from 'lodash';

/**
* WordPress dependencies
*/
import {
Component,
RawHTML,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';

export class ServerSideRender extends Component {
constructor( props ) {
super( props );
this.state = {
response: null,
};
}

componentDidMount() {
this.fetch( this.props );
}

componentWillReceiveProps( nextProps ) {
if ( ! isEqual( nextProps, this.props ) ) {
this.fetch( nextProps );
}
}

fetch( props ) {
this.setState( { response: null } );
const { block, attributes } = props;

const path = '/gutenberg/v1/block-renderer/' + block + '?context=edit&' + this.getQueryUrlFromObject( { attributes } );

return wp.apiRequest( { path: path } ).then( ( response ) => {
if ( response && response.rendered ) {
this.setState( { response: response.rendered } );
}
} );
}

getQueryUrlFromObject( obj, prefix ) {
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() {
const response = this.state.response;
if ( ! response || ! response.length ) {
return (
<div key="loading" className="wp-block-embed is-loading">

<p>{ __( 'Loading...' ) }</p>
</div>
);
}

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

export default ServerSideRender;
4 changes: 2 additions & 2 deletions lib/class-wp-block-type-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion lib/class-wp-block-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}

Expand Down
161 changes: 161 additions & 0 deletions lib/class-wp-rest-block-renderer-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php
/**
* Block Renderer REST API: WP_REST_Block_Renderer_Controller class
*
* @package gutenberg
* @since 2.8.0
*/

/**
* Controller which provides REST endpoint for rendering a block.
*
* @since 2.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Renderer_Controller extends WP_REST_Controller {

/**
* Constructs the controller.
*
* @access public
*/
public function __construct() {
$this->namespace = 'gutenberg/v1';
$this->rest_base = 'block-renderer';
}

/**
* Registers the necessary REST API routes, one for each dynamic block.
*
* @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() ) {
continue;
}

register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<name>' . $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' ) ),
'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,
),
'post_id' => array(
'description' => __( 'ID of the post context.', 'gutenberg' ),
'type' => 'integer',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
}

/**
* Checks if a given request has access to read blocks.
*
* @since 2.8.0
* @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( $request ) {
global $post;

$post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0;

if ( 0 < $post_id ) {
$post = get_post( $post_id );
if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks of this post', 'gutenberg' ), array(
'status' => rest_authorization_required_code(),
) );
}
} 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;
}

/**
* Returns block output from block's registered render_callback.
*
* @since 2.8.0
* @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( $request ) {
global $post;

$post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0;

if ( 0 < $post_id ) {
$post = get_post( $post_id );

// Set up postdata since this will be needed if post_id was set.
setup_postdata( $post );
}
$registry = WP_Block_Type_Registry::get_instance();
$block = $registry->get_registered( $request['name'] );

if ( null === $block ) {
return new WP_Error( 'gutenberg_block_invalid', __( 'Invalid block.', 'gutenberg' ), array(
'status' => 404,
) );
}

$data = array(
'rendered' => $block->render( $request->get_param( 'attributes' ) ),
);
return rest_ensure_response( $data );
}

/**
* Retrieves block's output schema, conforming to JSON Schema.
*
* @since 2.8.0
* @access public
*
* @return array Item schema data.
*/
public function get_item_schema() {
return array(
'$schema' => 'https://json-schema.org/schema#',
'title' => 'rendered-block',
'type' => 'object',
'properties' => array(
'rendered' => array(
'description' => __( 'The rendered block.', 'gutenberg' ),
'type' => 'string',
'required' => true,
'context' => array( 'edit' ),
),
),
);
}
}
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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-block-renderer-controller.php';
require dirname( __FILE__ ) . '/blocks.php';
require dirname( __FILE__ ) . '/client-assets.php';
require dirname( __FILE__ ) . '/compat.php';
Expand Down
11 changes: 11 additions & 0 deletions lib/register.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,17 @@ function gutenberg_register_post_types() {
}
add_action( 'init', 'gutenberg_register_post_types' );

/**
* Registers the REST API routes needed by the Gutenberg editor.
*
* @since 2.8.0
*/
function gutenberg_register_rest_routes() {
$controller = new WP_REST_Block_Renderer_Controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_routes' );

/**
* Gets revisions details for the selected post.
*
Expand Down
3 changes: 3 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<file>./phpunit</file>
<file>gutenberg.php</file>

<rule ref="PHPCompatibility.PHP.NewKeywords.t_namespaceFound">
<exclude-pattern>lib/class-wp-rest-block-renderer-controller.php</exclude-pattern>
</rule>
<!-- These special comments are markers for the build process -->
<rule ref="Squiz.Commenting.InlineComment.WrongStyle">
<exclude-pattern>gutenberg.php</exclude-pattern>
Expand Down
Loading

0 comments on commit d7ddb69

Please sign in to comment.