PluginProbe
Gutenberg / 12.1.0
Gutenberg v12.1.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / class-wp-rest-pattern-directory-controller.php

class-wp-rest-pattern-directory-controller.php in Gutenberg 12.1.0, at lib/class-wp-rest-pattern-directory-controller.php

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