[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
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 block 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 * @since 6.0.0 Added 'slug' to request. 84 * 85 * @param WP_REST_Request $request Full details about the request. 86 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 87 */ 88 public function get_items( $request ) { 89 /* 90 * Include an unmodified `$wp_version`, so the API can craft a response that's tailored to 91 * it. Some plugins modify the version in a misguided attempt to improve security by 92 * obscuring the version, which can cause invalid requests. 93 */ 94 require ABSPATH . WPINC . '/version.php'; 95 96 $query_args = array( 97 'locale' => get_user_locale(), 98 'wp-version' => $wp_version, 99 ); 100 101 $category_id = $request['category']; 102 $keyword_id = $request['keyword']; 103 $search_term = $request['search']; 104 $slug = $request['slug']; 105 106 if ( $category_id ) { 107 $query_args['pattern-categories'] = $category_id; 108 } 109 110 if ( $keyword_id ) { 111 $query_args['pattern-keywords'] = $keyword_id; 112 } 113 114 if ( $search_term ) { 115 $query_args['search'] = $search_term; 116 } 117 118 if ( $slug ) { 119 $query_args['slug'] = $slug; 120 } 121 122 $transient_key = $this->get_transient_key( $query_args ); 123 124 /* 125 * Use network-wide transient to improve performance. The locale is the only site 126 * configuration that affects the response, and it's included in the transient key. 127 */ 128 $raw_patterns = get_site_transient( $transient_key ); 129 130 if ( ! $raw_patterns ) { 131 $api_url = 'http://api.wordpress.org/patterns/1.0/?' . build_query( $query_args ); 132 if ( wp_http_supports( array( 'ssl' ) ) ) { 133 $api_url = set_url_scheme( $api_url, 'https' ); 134 } 135 136 /* 137 * Default to a short TTL, to mitigate cache stampedes on high-traffic sites. 138 * This assumes that most errors will be short-lived, e.g., packet loss that causes the 139 * first request to fail, but a follow-up one will succeed. The value should be high 140 * enough to avoid stampedes, but low enough to not interfere with users manually 141 * re-trying a failed request. 142 */ 143 $cache_ttl = 5; 144 $wporg_response = wp_remote_get( $api_url ); 145 $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) ); 146 147 if ( is_wp_error( $wporg_response ) ) { 148 $raw_patterns = $wporg_response; 149 150 } elseif ( ! is_array( $raw_patterns ) ) { 151 // HTTP request succeeded, but response data is invalid. 152 $raw_patterns = new WP_Error( 153 'pattern_api_failed', 154 sprintf( 155 /* translators: %s: Support forums URL. */ 156 __( '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>.' ), 157 __( 'https://wordpress.org/support/forums/' ) 158 ), 159 array( 160 'response' => wp_remote_retrieve_body( $wporg_response ), 161 ) 162 ); 163 164 } else { 165 // Response has valid data. 166 $cache_ttl = HOUR_IN_SECONDS; 167 } 168 169 set_site_transient( $transient_key, $raw_patterns, $cache_ttl ); 170 } 171 172 if ( is_wp_error( $raw_patterns ) ) { 173 $raw_patterns->add_data( array( 'status' => 500 ) ); 174 175 return $raw_patterns; 176 } 177 178 $response = array(); 179 180 if ( $raw_patterns ) { 181 foreach ( $raw_patterns as $pattern ) { 182 $response[] = $this->prepare_response_for_collection( 183 $this->prepare_item_for_response( $pattern, $request ) 184 ); 185 } 186 } 187 188 return new WP_REST_Response( $response ); 189 } 190 191 /** 192 * Prepare a raw block pattern before it gets output in a REST API response. 193 * 194 * @since 5.8.0 195 * @since 5.9.0 Renamed `$raw_pattern` to `$item` to match parent class for PHP 8 named parameter support. 196 * 197 * @param object $item Raw pattern from api.wordpress.org, before any changes. 198 * @param WP_REST_Request $request Request object. 199 * @return WP_REST_Response 200 */ 201 public function prepare_item_for_response( $item, $request ) { 202 // Restores the more descriptive, specific name for use within this method. 203 $raw_pattern = $item; 204 $prepared_pattern = array( 205 'id' => absint( $raw_pattern->id ), 206 'title' => sanitize_text_field( $raw_pattern->title->rendered ), 207 'content' => wp_kses_post( $raw_pattern->pattern_content ), 208 'categories' => array_map( 'sanitize_title', $raw_pattern->category_slugs ), 209 'keywords' => array_map( 'sanitize_title', $raw_pattern->keyword_slugs ), 210 'description' => sanitize_text_field( $raw_pattern->meta->wpop_description ), 211 'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ), 212 ); 213 214 $prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request ); 215 216 $response = new WP_REST_Response( $prepared_pattern ); 217 218 /** 219 * Filters the REST API response for a block pattern. 220 * 221 * @since 5.8.0 222 * 223 * @param WP_REST_Response $response The response object. 224 * @param object $raw_pattern The unprepared block pattern. 225 * @param WP_REST_Request $request The request object. 226 */ 227 return apply_filters( 'rest_prepare_block_pattern', $response, $raw_pattern, $request ); 228 } 229 230 /** 231 * Retrieves the block pattern's schema, conforming to JSON Schema. 232 * 233 * @since 5.8.0 234 * 235 * @return array Item schema data. 236 */ 237 public function get_item_schema() { 238 if ( $this->schema ) { 239 return $this->add_additional_fields_schema( $this->schema ); 240 } 241 242 $this->schema = array( 243 '$schema' => 'http://json-schema.org/draft-04/schema#', 244 'title' => 'pattern-directory-item', 245 'type' => 'object', 246 'properties' => array( 247 'id' => array( 248 'description' => __( 'The pattern ID.' ), 249 'type' => 'integer', 250 'minimum' => 1, 251 'context' => array( 'view', 'edit', 'embed' ), 252 ), 253 254 'title' => array( 255 'description' => __( 'The pattern title, in human readable format.' ), 256 'type' => 'string', 257 'minLength' => 1, 258 'context' => array( 'view', 'edit', 'embed' ), 259 ), 260 261 'content' => array( 262 'description' => __( 'The pattern content.' ), 263 'type' => 'string', 264 'minLength' => 1, 265 'context' => array( 'view', 'edit', 'embed' ), 266 ), 267 268 'categories' => array( 269 'description' => __( "The pattern's category slugs." ), 270 'type' => 'array', 271 'uniqueItems' => true, 272 'items' => array( 'type' => 'string' ), 273 'context' => array( 'view', 'edit', 'embed' ), 274 ), 275 276 'keywords' => array( 277 'description' => __( "The pattern's keyword slugs." ), 278 'type' => 'array', 279 'uniqueItems' => true, 280 'items' => array( 'type' => 'string' ), 281 'context' => array( 'view', 'edit', 'embed' ), 282 ), 283 284 'description' => array( 285 'description' => __( 'A description of the pattern.' ), 286 'type' => 'string', 287 'minLength' => 1, 288 'context' => array( 'view', 'edit', 'embed' ), 289 ), 290 291 'viewport_width' => array( 292 'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.' ), 293 'type' => 'integer', 294 'context' => array( 'view', 'edit', 'embed' ), 295 ), 296 ), 297 ); 298 299 return $this->add_additional_fields_schema( $this->schema ); 300 } 301 302 /** 303 * Retrieves the search parameters for the block pattern's collection. 304 * 305 * @since 5.8.0 306 * 307 * @return array Collection parameters. 308 */ 309 public function get_collection_params() { 310 $query_params = parent::get_collection_params(); 311 312 // Pagination is not supported. 313 unset( $query_params['page'] ); 314 unset( $query_params['per_page'] ); 315 316 $query_params['search']['minLength'] = 1; 317 $query_params['context']['default'] = 'view'; 318 319 $query_params['category'] = array( 320 'description' => __( 'Limit results to those matching a category ID.' ), 321 'type' => 'integer', 322 'minimum' => 1, 323 ); 324 325 $query_params['keyword'] = array( 326 'description' => __( 'Limit results to those matching a keyword ID.' ), 327 'type' => 'integer', 328 'minimum' => 1, 329 ); 330 331 $query_params['slug'] = array( 332 'description' => __( 'Limit results to those matching a pattern (slug).' ), 333 'type' => 'array', 334 ); 335 336 /** 337 * Filter collection parameters for the block pattern directory controller. 338 * 339 * @since 5.8.0 340 * 341 * @param array $query_params JSON Schema-formatted collection parameters. 342 */ 343 return apply_filters( 'rest_pattern_directory_collection_params', $query_params ); 344 } 345 346 /* 347 * Include a hash of the query args, so that different requests are stored in 348 * separate caches. 349 * 350 * MD5 is chosen for its speed, low-collision rate, universal availability, and to stay 351 * under the character limit for `_site_transient_timeout_{...}` keys. 352 * 353 * @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses 354 * 355 * @since 6.0.0 356 * 357 * @param array $query_args Query arguments to generate a transient key from. 358 * @return string Transient key. 359 */ 360 protected function get_transient_key( $query_args ) { 361 362 if ( isset( $query_args['slug'] ) ) { 363 // This is an additional precaution because the "sort" function expects an array. 364 $query_args['slug'] = wp_parse_list( $query_args['slug'] ); 365 366 // Empty arrays should not affect the transient key. 367 if ( empty( $query_args['slug'] ) ) { 368 unset( $query_args['slug'] ); 369 } else { 370 // Sort the array so that the transient key doesn't depend on the order of slugs. 371 sort( $query_args['slug'] ); 372 } 373 } 374 375 return 'wp_remote_block_patterns_' . md5( serialize( $query_args ) ); 376 } 377 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Dec 6 01:00:02 2024 | Cross-referenced by PHPXref 0.7.1 |