Skip to content

Commit

Permalink
Pattern Directory: Add API endpoints for browsing and searching. (#26578
Browse files Browse the repository at this point in the history
)

Similar to the Block Directory, the endpoints in Gutenberg proxy the api.wordpress.org endpoints, to prevent w.org from receiving the user's IP address. This will also be necessary in the future to combine locally-registered patterns with those hosted in the wordpress.org directory.

Fixes #26577
  • Loading branch information
iandunn authored Jan 28, 2021
1 parent 6a69e9f commit 3ff214f
Show file tree
Hide file tree
Showing 7 changed files with 694 additions and 0 deletions.
288 changes: 288 additions & 0 deletions lib/class-wp-rest-pattern-directory-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
<?php
/**
* Start: Include for phase 2
* Block Pattern Directory REST API: WP_REST_Pattern_Directory_Controller class
*
* @since 5.8.0
* @package gutenberg
*/

/**
* Controller which provides REST endpoint for block patterns.
*
* This simply proxies the endpoint at http://api.wordpress.org/patterns/1.0/. That isn't necessary for
* functionality, but is desired for privacy. It prevents api.wordpress.org from knowing the user's IP address.
*
* This class can be removed when plugin support requires WordPress 5.8.0+.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*/
public function __construct() {
$this->namespace = '__experimental';
$this->rest_base = 'pattern-directory';
}

/**
* Registers the necessary REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/patterns',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Checks whether a given request has permission to view the local pattern directory.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_Error|bool True if the request has permission, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Method must match signature of parent class.
if ( current_user_can( 'edit_posts' ) ) {
return true;
}

foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}

return new WP_Error(
'rest_pattern_directory_cannot_view',
__( 'Sorry, you are not allowed to browse the local block pattern directory.', 'gutenberg' ),
array( 'status' => rest_authorization_required_code() )
);
}

/**
* Search and retrieve block patterns metadata
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$query_args = array();
$category_ids = $request['category'];
$search_term = $request['search'];

if ( $category_ids ) {
$query_args['pattern-categories'] = $category_ids;
}

if ( $search_term ) {
$query_args['search'] = $search_term;
}

$api_url = add_query_arg(
array_map( 'rawurlencode', $query_args ),
'http://api.wordpress.org/patterns/1.0/'
);

if ( wp_http_supports( array( 'ssl' ) ) ) {
$api_url = set_url_scheme( $api_url, 'https' );
}

$wporg_response = wp_remote_get( $api_url );
$raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) );

if ( is_wp_error( $wporg_response ) ) {
$wporg_response->add_data( array( 'status' => 500 ) );

return $wporg_response;
}

// Make sure w.org returned valid data.
if ( ! is_array( $raw_patterns ) ) {
return new WP_Error(
'pattern_api_failed',
sprintf(
/* translators: %s: Support forums URL. */
__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.', 'gutenberg' ),
__( 'https://wordpress.org/support/forums/', 'gutenberg' )
),
array(
'status' => 500,
'response' => wp_remote_retrieve_body( $wporg_response ),
)
);
}

$response = array();

if ( $raw_patterns ) {
foreach ( $raw_patterns as $pattern ) {
$response[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $pattern, $request )
);
}
}

return new WP_REST_Response( $response );
}

/**
* Prepare a raw pattern before it's output in an API response.
*
* @since 5.8.0
*
* @param object $raw_pattern A pattern from api.wordpress.org, before any changes.
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response
*/
public function prepare_item_for_response( $raw_pattern, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Method must match signature of parent class.
$prepared_pattern = array(
'id' => absint( $raw_pattern->id ),
'title' => sanitize_text_field( $raw_pattern->title->rendered ),
'content' => wp_kses_post( $raw_pattern->content->rendered ),
'categories' => array_map( 'sanitize_title', $raw_pattern->category_slugs ),
'keywords' => array_map( 'sanitize_title', $raw_pattern->keyword_slugs ),
'description' => sanitize_text_field( $raw_pattern->meta->wpop_description ),
'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ),

);

$prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request );

$response = new WP_REST_Response( $prepared_pattern );

/**
* Filters the REST API response for a pattern.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param object $raw_pattern The unprepared pattern.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_application_password', $response, $raw_pattern, $request );
}

/**
* Retrieves the pattern's schema, conforming to JSON Schema.
*
* @since 5.8.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}

$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'pattern-directory-item',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'The pattern ID.', 'gutenberg' ),
'type' => 'integer',
'minimum' => 1,
'context' => array( 'view', 'embed' ),
),

'title' => array(
'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'embed' ),
),

'content' => array(
'description' => __( 'The pattern content.', 'gutenberg' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'embed' ),
),

'categories' => array(
'description' => __( "The pattern's category slugs.", 'gutenberg' ),
'type' => 'array',
'uniqueItems' => true,
'items' => array( 'type' => 'string' ),
'context' => array( 'view', 'embed' ),
),

'keywords' => array(
'description' => __( "The pattern's keyword slugs.", 'gutenberg' ),
'type' => 'array',
'uniqueItems' => true,
'items' => array( 'type' => 'string' ),
'context' => array( 'view', 'embed' ),
),

'description' => array(
'description' => __( 'A description of the pattern.', 'gutenberg' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'embed' ),
),

'viewport_width' => array(
'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.', 'gutenberg' ),
'type' => 'integer',
'context' => array( 'view', 'embed' ),
),
),
);

return $this->add_additional_fields_schema( $this->schema );
}

/**
* Retrieves the search params for the patterns collection.
*
* @since 5.5.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();

// Pagination is not supported.
unset( $query_params['page'] );
unset( $query_params['per_page'] );

$query_params['search']['minLength'] = 1;
$query_params['context']['default'] = 'view';

$query_params['category'] = array(
'description' => __( 'Limit results to those matching a category ID.', 'gutenberg' ),
'type' => 'integer',
'minimum' => 1,
);

/**
* Filter collection parameters for the pattern directory controller.
*
* @since 5.5.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_pattern_directory_collection_params', $query_params );
}
}
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ function gutenberg_is_experiment_enabled( $name ) {
if ( ! class_exists( 'WP_REST_Widgets_Controller' ) ) {
require_once __DIR__ . '/class-wp-rest-widgets-controller.php';
}
if ( ! class_exists( 'WP_REST_Pattern_Directory_Controller' ) ) {
require dirname( __FILE__ ) . '/class-wp-rest-pattern-directory-controller.php';
}
if ( ! class_exists( 'WP_REST_Menus_Controller' ) ) {
require_once __DIR__ . '/class-wp-rest-menus-controller.php';
}
Expand Down
9 changes: 9 additions & 0 deletions lib/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
die( 'Silence is golden.' );
}

/**
* Registers the block pattern directory.
*/
function gutenberg_register_rest_pattern_directory() {
$block_directory_controller = new WP_REST_Pattern_Directory_Controller();
$block_directory_controller->register_routes();
}
add_filter( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' );

/**
* Registers the menu locations area REST API routes.
*/
Expand Down
Loading

0 comments on commit 3ff214f

Please sign in to comment.