class-wp-rest-pattern-directory-controller.php (10440B)
1 <?php 2 /** 3 * Block Pattern Directory REST API: WP_REST_Pattern_Directory_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.8.0 8 */ 9 10 /** 11 * Controller which provides REST endpoint for block patterns. 12 * 13 * This simply proxies the endpoint at http://api.wordpress.org/patterns/1.0/. That isn't necessary for 14 * functionality, but is desired for privacy. It prevents api.wordpress.org from knowing the user's IP address. 15 * 16 * @since 5.8.0 17 * 18 * @see WP_REST_Controller 19 */ 20 class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller { 21 22 /** 23 * Constructs the controller. 24 * 25 * @since 5.8.0 26 */ 27 public function __construct() { 28 $this->namespace = 'wp/v2'; 29 $this->rest_base = 'pattern-directory'; 30 } 31 32 /** 33 * Registers the necessary REST API routes. 34 * 35 * @since 5.8.0 36 */ 37 public function register_routes() { 38 register_rest_route( 39 $this->namespace, 40 '/' . $this->rest_base . '/patterns', 41 array( 42 array( 43 'methods' => WP_REST_Server::READABLE, 44 'callback' => array( $this, 'get_items' ), 45 'permission_callback' => array( $this, 'get_items_permissions_check' ), 46 'args' => $this->get_collection_params(), 47 ), 48 'schema' => array( $this, 'get_public_item_schema' ), 49 ) 50 ); 51 } 52 53 /** 54 * Checks whether a given request has permission to view the local pattern directory. 55 * 56 * @since 5.8.0 57 * 58 * @param WP_REST_Request $request Full details about the request. 59 * @return true|WP_Error True if the request has permission, WP_Error object otherwise. 60 */ 61 public function get_items_permissions_check( $request ) { 62 if ( current_user_can( 'edit_posts' ) ) { 63 return true; 64 } 65 66 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 67 if ( current_user_can( $post_type->cap->edit_posts ) ) { 68 return true; 69 } 70 } 71 72 return new WP_Error( 73 'rest_pattern_directory_cannot_view', 74 __( 'Sorry, you are not allowed to browse the local block pattern directory.' ), 75 array( 'status' => rest_authorization_required_code() ) 76 ); 77 } 78 79 /** 80 * Search and retrieve block patterns metadata 81 * 82 * @since 5.8.0 83 * 84 * @param WP_REST_Request $request Full details about the request. 85 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 86 */ 87 public function get_items( $request ) { 88 /* 89 * Include an unmodified `$wp_version`, so the API can craft a response that's tailored to 90 * it. Some plugins modify the version in a misguided attempt to improve security by 91 * obscuring the version, which can cause invalid requests. 92 */ 93 require ABSPATH . WPINC . '/version.php'; 94 95 $query_args = array( 96 'locale' => get_user_locale(), 97 'wp-version' => $wp_version, 98 ); 99 100 $category_id = $request['category']; 101 $keyword_id = $request['keyword']; 102 $search_term = $request['search']; 103 104 if ( $category_id ) { 105 $query_args['pattern-categories'] = $category_id; 106 } 107 108 if ( $keyword_id ) { 109 $query_args['pattern-keywords'] = $keyword_id; 110 } 111 112 if ( $search_term ) { 113 $query_args['search'] = $search_term; 114 } 115 116 /* 117 * Include a hash of the query args, so that different requests are stored in 118 * separate caches. 119 * 120 * MD5 is chosen for its speed, low-collision rate, universal availability, and to stay 121 * under the character limit for `_site_transient_timeout_{...}` keys. 122 * 123 * @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses 124 */ 125 $transient_key = 'wp_remote_block_patterns_' . md5( implode( '-', $query_args ) ); 126 127 /* 128 * Use network-wide transient to improve performance. The locale is the only site 129 * configuration that affects the response, and it's included in the transient key. 130 */ 131 $raw_patterns = get_site_transient( $transient_key ); 132 133 if ( ! $raw_patterns ) { 134 $api_url = add_query_arg( 135 array_map( 'rawurlencode', $query_args ), 136 'http://api.wordpress.org/patterns/1.0/' 137 ); 138 139 if ( wp_http_supports( array( 'ssl' ) ) ) { 140 $api_url = set_url_scheme( $api_url, 'https' ); 141 } 142 143 /* 144 * Default to a short TTL, to mitigate cache stampedes on high-traffic sites. 145 * This assumes that most errors will be short-lived, e.g., packet loss that causes the 146 * first request to fail, but a follow-up one will succeed. The value should be high 147 * enough to avoid stampedes, but low enough to not interfere with users manually 148 * re-trying a failed request. 149 */ 150 $cache_ttl = 5; 151 $wporg_response = wp_remote_get( $api_url ); 152 $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) ); 153 154 if ( is_wp_error( $wporg_response ) ) { 155 $raw_patterns = $wporg_response; 156 157 } elseif ( ! is_array( $raw_patterns ) ) { 158 // HTTP request succeeded, but response data is invalid. 159 $raw_patterns = new WP_Error( 160 'pattern_api_failed', 161 sprintf( 162 /* translators: %s: Support forums URL. */ 163 __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ), 164 __( 'https://wordpress.org/support/forums/' ) 165 ), 166 array( 167 'response' => wp_remote_retrieve_body( $wporg_response ), 168 ) 169 ); 170 171 } else { 172 // Response has valid data. 173 $cache_ttl = HOUR_IN_SECONDS; 174 } 175 176 set_site_transient( $transient_key, $raw_patterns, $cache_ttl ); 177 } 178 179 if ( is_wp_error( $raw_patterns ) ) { 180 $raw_patterns->add_data( array( 'status' => 500 ) ); 181 182 return $raw_patterns; 183 } 184 185 $response = array(); 186 187 if ( $raw_patterns ) { 188 foreach ( $raw_patterns as $pattern ) { 189 $response[] = $this->prepare_response_for_collection( 190 $this->prepare_item_for_response( $pattern, $request ) 191 ); 192 } 193 } 194 195 return new WP_REST_Response( $response ); 196 } 197 198 /** 199 * Prepare a raw pattern before it's output in an API response. 200 * 201 * @since 5.8.0 202 * 203 * @param object $raw_pattern A pattern from api.wordpress.org, before any changes. 204 * @param WP_REST_Request $request Request object. 205 * @return WP_REST_Response 206 */ 207 public function prepare_item_for_response( $raw_pattern, $request ) { 208 $prepared_pattern = array( 209 'id' => absint( $raw_pattern->id ), 210 'title' => sanitize_text_field( $raw_pattern->title->rendered ), 211 'content' => wp_kses_post( $raw_pattern->pattern_content ), 212 'categories' => array_map( 'sanitize_title', $raw_pattern->category_slugs ), 213 'keywords' => array_map( 'sanitize_title', $raw_pattern->keyword_slugs ), 214 'description' => sanitize_text_field( $raw_pattern->meta->wpop_description ), 215 'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ), 216 ); 217 218 $prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request ); 219 220 $response = new WP_REST_Response( $prepared_pattern ); 221 222 /** 223 * Filters the REST API response for a pattern. 224 * 225 * @since 5.8.0 226 * 227 * @param WP_REST_Response $response The response object. 228 * @param object $raw_pattern The unprepared pattern. 229 * @param WP_REST_Request $request The request object. 230 */ 231 return apply_filters( 'rest_prepare_block_pattern', $response, $raw_pattern, $request ); 232 } 233 234 /** 235 * Retrieves the pattern's schema, conforming to JSON Schema. 236 * 237 * @since 5.8.0 238 * 239 * @return array Item schema data. 240 */ 241 public function get_item_schema() { 242 if ( $this->schema ) { 243 return $this->add_additional_fields_schema( $this->schema ); 244 } 245 246 $this->schema = array( 247 '$schema' => 'http://json-schema.org/draft-04/schema#', 248 'title' => 'pattern-directory-item', 249 'type' => 'object', 250 'properties' => array( 251 'id' => array( 252 'description' => __( 'The pattern ID.' ), 253 'type' => 'integer', 254 'minimum' => 1, 255 'context' => array( 'view', 'embed' ), 256 ), 257 258 'title' => array( 259 'description' => __( 'The pattern title, in human readable format.' ), 260 'type' => 'string', 261 'minLength' => 1, 262 'context' => array( 'view', 'embed' ), 263 ), 264 265 'content' => array( 266 'description' => __( 'The pattern content.' ), 267 'type' => 'string', 268 'minLength' => 1, 269 'context' => array( 'view', 'embed' ), 270 ), 271 272 'categories' => array( 273 'description' => __( "The pattern's category slugs." ), 274 'type' => 'array', 275 'uniqueItems' => true, 276 'items' => array( 'type' => 'string' ), 277 'context' => array( 'view', 'embed' ), 278 ), 279 280 'keywords' => array( 281 'description' => __( "The pattern's keyword slugs." ), 282 'type' => 'array', 283 'uniqueItems' => true, 284 'items' => array( 'type' => 'string' ), 285 'context' => array( 'view', 'embed' ), 286 ), 287 288 'description' => array( 289 'description' => __( 'A description of the pattern.' ), 290 'type' => 'string', 291 'minLength' => 1, 292 'context' => array( 'view', 'embed' ), 293 ), 294 295 'viewport_width' => array( 296 'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.' ), 297 'type' => 'integer', 298 'context' => array( 'view', 'embed' ), 299 ), 300 ), 301 ); 302 303 return $this->add_additional_fields_schema( $this->schema ); 304 } 305 306 /** 307 * Retrieves the search params for the patterns collection. 308 * 309 * @since 5.8.0 310 * 311 * @return array Collection parameters. 312 */ 313 public function get_collection_params() { 314 $query_params = parent::get_collection_params(); 315 316 // Pagination is not supported. 317 unset( $query_params['page'] ); 318 unset( $query_params['per_page'] ); 319 320 $query_params['search']['minLength'] = 1; 321 $query_params['context']['default'] = 'view'; 322 323 $query_params['category'] = array( 324 'description' => __( 'Limit results to those matching a category ID.' ), 325 'type' => 'integer', 326 'minimum' => 1, 327 ); 328 329 $query_params['keyword'] = array( 330 'description' => __( 'Limit results to those matching a keyword ID.' ), 331 'type' => 'integer', 332 'minimum' => 1, 333 ); 334 335 /** 336 * Filter collection parameters for the pattern directory controller. 337 * 338 * @since 5.8.0 339 * 340 * @param array $query_params JSON Schema-formatted collection parameters. 341 */ 342 return apply_filters( 'rest_pattern_directory_collection_params', $query_params ); 343 } 344 }