|
1 <?php |
|
2 /** |
|
3 * REST API: WP_REST_Font_Families_Controller class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage REST_API |
|
7 * @since 6.5.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Font Families Controller class. |
|
12 * |
|
13 * @since 6.5.0 |
|
14 */ |
|
15 class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller { |
|
16 |
|
17 /** |
|
18 * The latest version of theme.json schema supported by the controller. |
|
19 * |
|
20 * @since 6.5.0 |
|
21 * @var int |
|
22 */ |
|
23 const LATEST_THEME_JSON_VERSION_SUPPORTED = 3; |
|
24 |
|
25 /** |
|
26 * Whether the controller supports batching. |
|
27 * |
|
28 * @since 6.5.0 |
|
29 * @var false |
|
30 */ |
|
31 protected $allow_batch = false; |
|
32 |
|
33 /** |
|
34 * Checks if a given request has access to font families. |
|
35 * |
|
36 * @since 6.5.0 |
|
37 * |
|
38 * @param WP_REST_Request $request Full details about the request. |
|
39 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. |
|
40 */ |
|
41 public function get_items_permissions_check( $request ) { |
|
42 $post_type = get_post_type_object( $this->post_type ); |
|
43 |
|
44 if ( ! current_user_can( $post_type->cap->read ) ) { |
|
45 return new WP_Error( |
|
46 'rest_cannot_read', |
|
47 __( 'Sorry, you are not allowed to access font families.' ), |
|
48 array( 'status' => rest_authorization_required_code() ) |
|
49 ); |
|
50 } |
|
51 |
|
52 return true; |
|
53 } |
|
54 |
|
55 /** |
|
56 * Checks if a given request has access to a font family. |
|
57 * |
|
58 * @since 6.5.0 |
|
59 * |
|
60 * @param WP_REST_Request $request Full details about the request. |
|
61 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. |
|
62 */ |
|
63 public function get_item_permissions_check( $request ) { |
|
64 $post = $this->get_post( $request['id'] ); |
|
65 if ( is_wp_error( $post ) ) { |
|
66 return $post; |
|
67 } |
|
68 |
|
69 if ( ! current_user_can( 'read_post', $post->ID ) ) { |
|
70 return new WP_Error( |
|
71 'rest_cannot_read', |
|
72 __( 'Sorry, you are not allowed to access this font family.' ), |
|
73 array( 'status' => rest_authorization_required_code() ) |
|
74 ); |
|
75 } |
|
76 |
|
77 return true; |
|
78 } |
|
79 |
|
80 /** |
|
81 * Validates settings when creating or updating a font family. |
|
82 * |
|
83 * @since 6.5.0 |
|
84 * |
|
85 * @param string $value Encoded JSON string of font family settings. |
|
86 * @param WP_REST_Request $request Request object. |
|
87 * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. |
|
88 */ |
|
89 public function validate_font_family_settings( $value, $request ) { |
|
90 $settings = json_decode( $value, true ); |
|
91 |
|
92 // Check settings string is valid JSON. |
|
93 if ( null === $settings ) { |
|
94 return new WP_Error( |
|
95 'rest_invalid_param', |
|
96 /* translators: %s: Parameter name: "font_family_settings". */ |
|
97 sprintf( __( '%s parameter must be a valid JSON string.' ), 'font_family_settings' ), |
|
98 array( 'status' => 400 ) |
|
99 ); |
|
100 } |
|
101 |
|
102 $schema = $this->get_item_schema()['properties']['font_family_settings']; |
|
103 $required = $schema['required']; |
|
104 |
|
105 if ( isset( $request['id'] ) ) { |
|
106 // Allow sending individual properties if we are updating an existing font family. |
|
107 unset( $schema['required'] ); |
|
108 |
|
109 // But don't allow updating the slug, since it is used as a unique identifier. |
|
110 if ( isset( $settings['slug'] ) ) { |
|
111 return new WP_Error( |
|
112 'rest_invalid_param', |
|
113 /* translators: %s: Name of parameter being updated: font_family_settings[slug]". */ |
|
114 sprintf( __( '%s cannot be updated.' ), 'font_family_settings[slug]' ), |
|
115 array( 'status' => 400 ) |
|
116 ); |
|
117 } |
|
118 } |
|
119 |
|
120 // Check that the font face settings match the theme.json schema. |
|
121 $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); |
|
122 |
|
123 if ( is_wp_error( $has_valid_settings ) ) { |
|
124 $has_valid_settings->add_data( array( 'status' => 400 ) ); |
|
125 return $has_valid_settings; |
|
126 } |
|
127 |
|
128 // Check that none of the required settings are empty values. |
|
129 foreach ( $required as $key ) { |
|
130 if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { |
|
131 return new WP_Error( |
|
132 'rest_invalid_param', |
|
133 /* translators: %s: Name of the empty font family setting parameter, e.g. "font_family_settings[slug]". */ |
|
134 sprintf( __( '%s cannot be empty.' ), "font_family_settings[ $key ]" ), |
|
135 array( 'status' => 400 ) |
|
136 ); |
|
137 } |
|
138 } |
|
139 |
|
140 return true; |
|
141 } |
|
142 |
|
143 /** |
|
144 * Sanitizes the font family settings when creating or updating a font family. |
|
145 * |
|
146 * @since 6.5.0 |
|
147 * |
|
148 * @param string $value Encoded JSON string of font family settings. |
|
149 * @return array Decoded array of font family settings. |
|
150 */ |
|
151 public function sanitize_font_family_settings( $value ) { |
|
152 // Settings arrive as stringified JSON, since this is a multipart/form-data request. |
|
153 $settings = json_decode( $value, true ); |
|
154 $schema = $this->get_item_schema()['properties']['font_family_settings']['properties']; |
|
155 |
|
156 // Sanitize settings based on callbacks in the schema. |
|
157 foreach ( $settings as $key => $value ) { |
|
158 $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; |
|
159 $settings[ $key ] = call_user_func( $sanitize_callback, $value ); |
|
160 } |
|
161 |
|
162 return $settings; |
|
163 } |
|
164 |
|
165 /** |
|
166 * Creates a single font family. |
|
167 * |
|
168 * @since 6.5.0 |
|
169 * |
|
170 * @param WP_REST_Request $request Full details about the request. |
|
171 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
172 */ |
|
173 public function create_item( $request ) { |
|
174 $settings = $request->get_param( 'font_family_settings' ); |
|
175 |
|
176 // Check that the font family slug is unique. |
|
177 $query = new WP_Query( |
|
178 array( |
|
179 'post_type' => $this->post_type, |
|
180 'posts_per_page' => 1, |
|
181 'name' => $settings['slug'], |
|
182 'update_post_meta_cache' => false, |
|
183 'update_post_term_cache' => false, |
|
184 ) |
|
185 ); |
|
186 if ( ! empty( $query->posts ) ) { |
|
187 return new WP_Error( |
|
188 'rest_duplicate_font_family', |
|
189 /* translators: %s: Font family slug. */ |
|
190 sprintf( __( 'A font family with slug "%s" already exists.' ), $settings['slug'] ), |
|
191 array( 'status' => 400 ) |
|
192 ); |
|
193 } |
|
194 |
|
195 return parent::create_item( $request ); |
|
196 } |
|
197 |
|
198 /** |
|
199 * Deletes a single font family. |
|
200 * |
|
201 * @since 6.5.0 |
|
202 * |
|
203 * @param WP_REST_Request $request Full details about the request. |
|
204 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
205 */ |
|
206 public function delete_item( $request ) { |
|
207 $force = isset( $request['force'] ) ? (bool) $request['force'] : false; |
|
208 |
|
209 // We don't support trashing for font families. |
|
210 if ( ! $force ) { |
|
211 return new WP_Error( |
|
212 'rest_trash_not_supported', |
|
213 /* translators: %s: force=true */ |
|
214 sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ), |
|
215 array( 'status' => 501 ) |
|
216 ); |
|
217 } |
|
218 |
|
219 return parent::delete_item( $request ); |
|
220 } |
|
221 |
|
222 /** |
|
223 * Prepares a single font family output for response. |
|
224 * |
|
225 * @since 6.5.0 |
|
226 * |
|
227 * @param WP_Post $item Post object. |
|
228 * @param WP_REST_Request $request Request object. |
|
229 * @return WP_REST_Response Response object. |
|
230 */ |
|
231 public function prepare_item_for_response( $item, $request ) { |
|
232 $fields = $this->get_fields_for_response( $request ); |
|
233 $data = array(); |
|
234 |
|
235 if ( rest_is_field_included( 'id', $fields ) ) { |
|
236 $data['id'] = $item->ID; |
|
237 } |
|
238 |
|
239 if ( rest_is_field_included( 'theme_json_version', $fields ) ) { |
|
240 $data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED; |
|
241 } |
|
242 |
|
243 if ( rest_is_field_included( 'font_faces', $fields ) ) { |
|
244 $data['font_faces'] = $this->get_font_face_ids( $item->ID ); |
|
245 } |
|
246 |
|
247 if ( rest_is_field_included( 'font_family_settings', $fields ) ) { |
|
248 $data['font_family_settings'] = $this->get_settings_from_post( $item ); |
|
249 } |
|
250 |
|
251 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; |
|
252 $data = $this->add_additional_fields_to_object( $data, $request ); |
|
253 $data = $this->filter_response_by_context( $data, $context ); |
|
254 |
|
255 $response = rest_ensure_response( $data ); |
|
256 |
|
257 if ( rest_is_field_included( '_links', $fields ) ) { |
|
258 $links = $this->prepare_links( $item ); |
|
259 $response->add_links( $links ); |
|
260 } |
|
261 |
|
262 /** |
|
263 * Filters the font family data for a REST API response. |
|
264 * |
|
265 * @since 6.5.0 |
|
266 * |
|
267 * @param WP_REST_Response $response The response object. |
|
268 * @param WP_Post $post Font family post object. |
|
269 * @param WP_REST_Request $request Request object. |
|
270 */ |
|
271 return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request ); |
|
272 } |
|
273 |
|
274 /** |
|
275 * Retrieves the post's schema, conforming to JSON Schema. |
|
276 * |
|
277 * @since 6.5.0 |
|
278 * |
|
279 * @return array Item schema data. |
|
280 */ |
|
281 public function get_item_schema() { |
|
282 if ( $this->schema ) { |
|
283 return $this->add_additional_fields_schema( $this->schema ); |
|
284 } |
|
285 |
|
286 $schema = array( |
|
287 '$schema' => 'http://json-schema.org/draft-04/schema#', |
|
288 'title' => $this->post_type, |
|
289 'type' => 'object', |
|
290 // Base properties for every Post. |
|
291 'properties' => array( |
|
292 'id' => array( |
|
293 'description' => __( 'Unique identifier for the post.', 'default' ), |
|
294 'type' => 'integer', |
|
295 'context' => array( 'view', 'edit', 'embed' ), |
|
296 'readonly' => true, |
|
297 ), |
|
298 'theme_json_version' => array( |
|
299 'description' => __( 'Version of the theme.json schema used for the typography settings.' ), |
|
300 'type' => 'integer', |
|
301 'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, |
|
302 'minimum' => 2, |
|
303 'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, |
|
304 'context' => array( 'view', 'edit', 'embed' ), |
|
305 ), |
|
306 'font_faces' => array( |
|
307 'description' => __( 'The IDs of the child font faces in the font family.' ), |
|
308 'type' => 'array', |
|
309 'context' => array( 'view', 'edit', 'embed' ), |
|
310 'items' => array( |
|
311 'type' => 'integer', |
|
312 ), |
|
313 ), |
|
314 // Font family settings come directly from theme.json schema |
|
315 // See https://schemas.wp.org/trunk/theme.json |
|
316 'font_family_settings' => array( |
|
317 'description' => __( 'font-face definition in theme.json format.' ), |
|
318 'type' => 'object', |
|
319 'context' => array( 'view', 'edit', 'embed' ), |
|
320 'properties' => array( |
|
321 'name' => array( |
|
322 'description' => __( 'Name of the font family preset, translatable.' ), |
|
323 'type' => 'string', |
|
324 'arg_options' => array( |
|
325 'sanitize_callback' => 'sanitize_text_field', |
|
326 ), |
|
327 ), |
|
328 'slug' => array( |
|
329 'description' => __( 'Kebab-case unique identifier for the font family preset.' ), |
|
330 'type' => 'string', |
|
331 'arg_options' => array( |
|
332 'sanitize_callback' => 'sanitize_title', |
|
333 ), |
|
334 ), |
|
335 'fontFamily' => array( |
|
336 'description' => __( 'CSS font-family value.' ), |
|
337 'type' => 'string', |
|
338 'arg_options' => array( |
|
339 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), |
|
340 ), |
|
341 ), |
|
342 'preview' => array( |
|
343 'description' => __( 'URL to a preview image of the font family.' ), |
|
344 'type' => 'string', |
|
345 'format' => 'uri', |
|
346 'default' => '', |
|
347 'arg_options' => array( |
|
348 'sanitize_callback' => 'sanitize_url', |
|
349 ), |
|
350 ), |
|
351 ), |
|
352 'required' => array( 'name', 'slug', 'fontFamily' ), |
|
353 'additionalProperties' => false, |
|
354 ), |
|
355 ), |
|
356 ); |
|
357 |
|
358 $this->schema = $schema; |
|
359 |
|
360 return $this->add_additional_fields_schema( $this->schema ); |
|
361 } |
|
362 |
|
363 /** |
|
364 * Retrieves the item's schema for display / public consumption purposes. |
|
365 * |
|
366 * @since 6.5.0 |
|
367 * |
|
368 * @return array Public item schema data. |
|
369 */ |
|
370 public function get_public_item_schema() { |
|
371 |
|
372 $schema = parent::get_public_item_schema(); |
|
373 |
|
374 // Also remove `arg_options' from child font_family_settings properties, since the parent |
|
375 // controller only handles the top level properties. |
|
376 foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) { |
|
377 unset( $property['arg_options'] ); |
|
378 } |
|
379 |
|
380 return $schema; |
|
381 } |
|
382 |
|
383 /** |
|
384 * Retrieves the query params for the font family collection. |
|
385 * |
|
386 * @since 6.5.0 |
|
387 * |
|
388 * @return array Collection parameters. |
|
389 */ |
|
390 public function get_collection_params() { |
|
391 $query_params = parent::get_collection_params(); |
|
392 |
|
393 // Remove unneeded params. |
|
394 unset( |
|
395 $query_params['after'], |
|
396 $query_params['modified_after'], |
|
397 $query_params['before'], |
|
398 $query_params['modified_before'], |
|
399 $query_params['search'], |
|
400 $query_params['search_columns'], |
|
401 $query_params['status'] |
|
402 ); |
|
403 |
|
404 $query_params['orderby']['default'] = 'id'; |
|
405 $query_params['orderby']['enum'] = array( 'id', 'include' ); |
|
406 |
|
407 /** |
|
408 * Filters collection parameters for the font family controller. |
|
409 * |
|
410 * @since 6.5.0 |
|
411 * |
|
412 * @param array $query_params JSON Schema-formatted collection parameters. |
|
413 */ |
|
414 return apply_filters( 'rest_wp_font_family_collection_params', $query_params ); |
|
415 } |
|
416 |
|
417 /** |
|
418 * Get the arguments used when creating or updating a font family. |
|
419 * |
|
420 * @since 6.5.0 |
|
421 * |
|
422 * @return array Font family create/edit arguments. |
|
423 */ |
|
424 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { |
|
425 if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) { |
|
426 $properties = $this->get_item_schema()['properties']; |
|
427 return array( |
|
428 'theme_json_version' => $properties['theme_json_version'], |
|
429 // When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data. |
|
430 // Font families don't currently support file uploads, but may accept preview files in the future. |
|
431 'font_family_settings' => array( |
|
432 'description' => __( 'font-family declaration in theme.json format, encoded as a string.' ), |
|
433 'type' => 'string', |
|
434 'required' => true, |
|
435 'validate_callback' => array( $this, 'validate_font_family_settings' ), |
|
436 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ), |
|
437 ), |
|
438 ); |
|
439 } |
|
440 |
|
441 return parent::get_endpoint_args_for_item_schema( $method ); |
|
442 } |
|
443 |
|
444 /** |
|
445 * Get the child font face post IDs. |
|
446 * |
|
447 * @since 6.5.0 |
|
448 * |
|
449 * @param int $font_family_id Font family post ID. |
|
450 * @return int[] Array of child font face post IDs. |
|
451 */ |
|
452 protected function get_font_face_ids( $font_family_id ) { |
|
453 $query = new WP_Query( |
|
454 array( |
|
455 'fields' => 'ids', |
|
456 'post_parent' => $font_family_id, |
|
457 'post_type' => 'wp_font_face', |
|
458 'posts_per_page' => 99, |
|
459 'order' => 'ASC', |
|
460 'orderby' => 'id', |
|
461 'update_post_meta_cache' => false, |
|
462 'update_post_term_cache' => false, |
|
463 ) |
|
464 ); |
|
465 |
|
466 return $query->posts; |
|
467 } |
|
468 |
|
469 /** |
|
470 * Prepares font family links for the request. |
|
471 * |
|
472 * @since 6.5.0 |
|
473 * |
|
474 * @param WP_Post $post Post object. |
|
475 * @return array Links for the given post. |
|
476 */ |
|
477 protected function prepare_links( $post ) { |
|
478 // Entity meta. |
|
479 $links = parent::prepare_links( $post ); |
|
480 |
|
481 return array( |
|
482 'self' => $links['self'], |
|
483 'collection' => $links['collection'], |
|
484 'font_faces' => $this->prepare_font_face_links( $post->ID ), |
|
485 ); |
|
486 } |
|
487 |
|
488 /** |
|
489 * Prepares child font face links for the request. |
|
490 * |
|
491 * @param int $font_family_id Font family post ID. |
|
492 * @return array Links for the child font face posts. |
|
493 */ |
|
494 protected function prepare_font_face_links( $font_family_id ) { |
|
495 $font_face_ids = $this->get_font_face_ids( $font_family_id ); |
|
496 $links = array(); |
|
497 foreach ( $font_face_ids as $font_face_id ) { |
|
498 $links[] = array( |
|
499 'embeddable' => true, |
|
500 'href' => rest_url( sprintf( '%s/%s/%s/font-faces/%s', $this->namespace, $this->rest_base, $font_family_id, $font_face_id ) ), |
|
501 ); |
|
502 } |
|
503 return $links; |
|
504 } |
|
505 |
|
506 /** |
|
507 * Prepares a single font family post for create or update. |
|
508 * |
|
509 * @since 6.5.0 |
|
510 * |
|
511 * @param WP_REST_Request $request Request object. |
|
512 * @return stdClass|WP_Error Post object or WP_Error. |
|
513 */ |
|
514 protected function prepare_item_for_database( $request ) { |
|
515 $prepared_post = new stdClass(); |
|
516 // Settings have already been decoded by ::sanitize_font_family_settings(). |
|
517 $settings = $request->get_param( 'font_family_settings' ); |
|
518 |
|
519 // This is an update and we merge with the existing font family. |
|
520 if ( isset( $request['id'] ) ) { |
|
521 $existing_post = $this->get_post( $request['id'] ); |
|
522 if ( is_wp_error( $existing_post ) ) { |
|
523 return $existing_post; |
|
524 } |
|
525 |
|
526 $prepared_post->ID = $existing_post->ID; |
|
527 $existing_settings = $this->get_settings_from_post( $existing_post ); |
|
528 $settings = array_merge( $existing_settings, $settings ); |
|
529 } |
|
530 |
|
531 $prepared_post->post_type = $this->post_type; |
|
532 $prepared_post->post_status = 'publish'; |
|
533 $prepared_post->post_title = $settings['name']; |
|
534 $prepared_post->post_name = sanitize_title( $settings['slug'] ); |
|
535 |
|
536 // Remove duplicate information from settings. |
|
537 unset( $settings['name'] ); |
|
538 unset( $settings['slug'] ); |
|
539 |
|
540 $prepared_post->post_content = wp_json_encode( $settings ); |
|
541 |
|
542 return $prepared_post; |
|
543 } |
|
544 |
|
545 /** |
|
546 * Gets the font family's settings from the post. |
|
547 * |
|
548 * @since 6.5.0 |
|
549 * |
|
550 * @param WP_Post $post Font family post object. |
|
551 * @return array Font family settings array. |
|
552 */ |
|
553 protected function get_settings_from_post( $post ) { |
|
554 $settings_json = json_decode( $post->post_content, true ); |
|
555 |
|
556 // Default to empty strings if the settings are missing. |
|
557 return array( |
|
558 'name' => isset( $post->post_title ) && $post->post_title ? $post->post_title : '', |
|
559 'slug' => isset( $post->post_name ) && $post->post_name ? $post->post_name : '', |
|
560 'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '', |
|
561 'preview' => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '', |
|
562 ); |
|
563 } |
|
564 } |