Skip to content

Commit

Permalink
Add reusable blocks REST API (#2503)
Browse files Browse the repository at this point in the history
* Stub out WP_REST_Reusable_Blocks_Controller
* Implement read and edit route
* Implement permissions checking
* Implement browse route
* Add tests for the reusable blocks REST controller
  • Loading branch information
noisysocks authored and youknowriad committed Oct 6, 2017
1 parent efaeff3 commit b8f1874
Show file tree
Hide file tree
Showing 4 changed files with 663 additions and 0 deletions.
1 change: 1 addition & 0 deletions gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// Load API functions, register scripts and actions, etc.
require_once dirname( __FILE__ ) . '/lib/class-wp-block-type.php';
require_once dirname( __FILE__ ) . '/lib/class-wp-block-type-registry.php';
require_once dirname( __FILE__ ) . '/lib/class-wp-rest-reusable-blocks-controller.php';
require_once dirname( __FILE__ ) . '/lib/blocks.php';
require_once dirname( __FILE__ ) . '/lib/client-assets.php';
require_once dirname( __FILE__ ) . '/lib/compat.php';
Expand Down
353 changes: 353 additions & 0 deletions lib/class-wp-rest-reusable-blocks-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
<?php
/**
* Reusable Blocks REST API: WP_REST_Reusable_Blocks_Controller class
*
* @package gutenberg
* @since 0.10.0
*/

/**
* Controller which provides a REST endpoint for Gutenberg to read, create and edit reusable blocks. Reusable blocks are
* stored as posts with a custom post type.
*
* @since 0.10.0
*
* @see WP_REST_Controller
*/
class WP_REST_Reusable_Blocks_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*
* @since 0.10.0
* @access public
*/
public function __construct() {
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword
$this->namespace = 'gutenberg/v1';
$this->rest_base = 'reusable-blocks';
}

/**
* Registers the necessary REST API routes.
*
* @since 0.10.0
* @access public
*/
public function register_routes() {
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword
$namespace = $this->namespace;

register_rest_route( $namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );

register_rest_route( $namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}

/**
* Checks if a given request has access to read reusable blocks.
*
* @since 0.10.0
* @access public
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'gutenberg_reusable_block_cannot_read', __( 'Sorry, you are not allowed to read reusable blocks as this user.', 'gutenberg' ), array(
'status' => rest_authorization_required_code(),
) );
}

return true;
}

/**
* Retrieves a collection of reusable blocks.
*
* @since 0.10.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_items( $request ) {
$reusable_blocks = get_posts( array(
'post_type' => 'gb_reusable_block',
) );

$collection = array();

foreach ( $reusable_blocks as $reusable_block ) {
$response = $this->prepare_item_for_response( $reusable_block, $request );
$collection[] = $this->prepare_response_for_collection( $response );
}

return rest_ensure_response( $collection );
}

/**
* Checks if a given request has access to read a reusable block.
*
* @since 0.10.0
* @access public
*
* @param WP_REST_Request $request Full details about the request.
* @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_reusable_block_cannot_read', __( 'Sorry, you are not allowed to read reusable blocks as this user.', 'gutenberg' ), array(
'status' => rest_authorization_required_code(),
) );
}

return true;
}

/**
* Retrieves a single reusable block.
*
* @since 0.10.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 ) {
$uuid = $request['id'];
if ( ! $this->is_valid_uuid4( $uuid ) ) {
return new WP_Error( 'gutenberg_reusable_block_invalid_id', __( 'ID is not a valid UUID v4.', 'gutenberg' ), array(
'status' => 404,
) );
}

$reusable_block = $this->get_reusable_block( $uuid );
if ( ! $reusable_block ) {
return new WP_Error( 'gutenberg_reusable_block_not_found', __( 'No reusable block with that ID found.', 'gutenberg' ), array(
'status' => 404,
) );
}

return $this->prepare_item_for_response( $reusable_block, $request );
}

/**
* Checks if a given request has access to update a reusable block.
*
* @since 0.10.0
* @access public
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'gutenberg_reusable_block_cannot_edit', __( 'Sorry, you are not allowed to edit reusable blocks as this user.', 'gutenberg' ), array(
'status' => rest_authorization_required_code(),
) );
}

return true;
}

/**
* Updates a single reusable block.
*
* @since 0.10.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 update_item( $request ) {
$uuid = $request['id'];
if ( ! $this->is_valid_uuid4( $uuid ) ) {
return new WP_Error( 'gutenberg_reusable_block_invalid_id', __( 'ID is not a valid UUID v4.', 'gutenberg' ), array(
'status' => 404,
) );
}

$reusable_block = $this->prepare_item_for_database( $request );
if ( is_wp_error( $reusable_block ) ) {
return $reusable_block;
}

// wp_insert_post will unslash its input, so we have to slash it first.
$post_id = wp_insert_post( wp_slash( (array) $reusable_block ), true );
if ( is_wp_error( $post_id ) ) {
return $post_id;
}

$reusable_block = get_post( $post_id );

return $this->prepare_item_for_response( $reusable_block, $request );
}

/**
* Prepares a single reusable block for update.
*
* @since 0.10.0
* @access protected
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Object suitable for passing to wp_insert_post, or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
$prepared_reusable_block = new stdClass();

$existing_reusable_block = $this->get_reusable_block( $request['id'] );
if ( $existing_reusable_block ) {
$prepared_reusable_block->ID = $existing_reusable_block->ID;
}

$prepared_reusable_block->post_type = 'gb_reusable_block';
$prepared_reusable_block->post_status = 'publish';

// ID. We already validated this in self::update_item().
$prepared_reusable_block->post_name = $request['id'];

// Name.
if ( isset( $request['name'] ) && is_string( $request['name'] ) ) {
$prepared_reusable_block->post_title = $request['name'];
} else {
return new WP_Error( 'gutenberg_reusable_block_invalid_field', __( 'Invalid reusable block name.', 'gutenberg' ), array(
'status' => 400,
) );
}

// Content.
if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
$prepared_reusable_block->post_content = $request['content'];
} else {
return new WP_Error( 'gutenberg_reusable_block_invalid_field', __( 'Invalid reusable block content.', 'gutenberg' ), array(
'status' => 400,
) );
}

return $prepared_reusable_block;
}

/**
* Prepares a single reusable block output for response.
*
* @since 0.10.0
* @access protected
*
* @param WP_Post $reusable_block The reusable block.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $reusable_block, $request ) {
$data = array(
'id' => $reusable_block->post_name,
'name' => $reusable_block->post_title,
'content' => $reusable_block->post_content,
);

return rest_ensure_response( $data );
}

/**
* Prepares a response for insertion into a collection.
*
* @since 0.10.0
* @access public
*
* @param WP_REST_Response $response Response object.
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function prepare_response_for_collection( $response ) {
return (array) $response->get_data();
}

/**
* Retrieves a reusable block's schema, conforming to JSON Schema.
*
* @since 0.10.0
* @access public
*
* @return array Item schema data.
*/
public function get_item_schema() {
return array(
'$schema' => 'http://json-schema.org/schema#',
'title' => 'reusable-block',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'UUID that identifies this reusable block.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Name that identifies this reusable block', 'gutenberg' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'required' => true,
),
'content' => array(
'description' => __( 'The block\'s HTML content.', 'gutenberg' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'required' => true,
),
),
);
}

/**
* Fetches a reusable block by its UUID ID. Reusable blocks are stored as posts with a custom post type.
*
* @since 0.10.0
* @access private
*
* @param string $uuid A UUID string that uniquely identifies the reusable block.
*
* @return WP_Post|null The block (a WP_Post), or null if none was found.
*/
private function get_reusable_block( $uuid ) {
$reusable_blocks = get_posts( array(
'post_type' => 'gb_reusable_block',
'name' => $uuid,
) );

return array_shift( $reusable_blocks );
}

/**
* Checks if the given value is a valid UUID v4 string.
*
* @since 0.10.0
* @access private
*
* @param mixed $uuid The value to validate.
* @return bool Whether or not the string is a valid UUID v4 string.
*/
private function is_valid_uuid4( $uuid ) {
if ( ! is_string( $uuid ) ) {
return false;
}

return (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid );
}
}
24 changes: 24 additions & 0 deletions lib/register.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,27 @@ function gutenberg_add_gutenberg_post_state( $post_states, $post ) {
return $post_states;
}
add_filter( 'display_post_states', 'gutenberg_add_gutenberg_post_state', 10, 2 );

/**
* Registers custom post types required by the Gutenberg editor.
*
* @since 0.10.0
*/
function gutenberg_register_post_types() {
register_post_type( 'gb_reusable_block', array(
'public' => false,
) );
}
add_action( 'init', 'gutenberg_register_post_types' );

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

Loading

0 comments on commit b8f1874

Please sign in to comment.