|
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 } |