|
1 <?php |
|
2 /** |
|
3 * REST API: WP_REST_Font_Faces_Controller class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage REST_API |
|
7 * @since 6.5.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Class to access font faces through the REST API. |
|
12 */ |
|
13 class WP_REST_Font_Faces_Controller extends WP_REST_Posts_Controller { |
|
14 |
|
15 /** |
|
16 * The latest version of theme.json schema supported by the controller. |
|
17 * |
|
18 * @since 6.5.0 |
|
19 * @var int |
|
20 */ |
|
21 const LATEST_THEME_JSON_VERSION_SUPPORTED = 3; |
|
22 |
|
23 /** |
|
24 * Whether the controller supports batching. |
|
25 * |
|
26 * @since 6.5.0 |
|
27 * @var false |
|
28 */ |
|
29 protected $allow_batch = false; |
|
30 |
|
31 /** |
|
32 * Registers the routes for posts. |
|
33 * |
|
34 * @since 6.5.0 |
|
35 * |
|
36 * @see register_rest_route() |
|
37 */ |
|
38 public function register_routes() { |
|
39 register_rest_route( |
|
40 $this->namespace, |
|
41 '/' . $this->rest_base, |
|
42 array( |
|
43 'args' => array( |
|
44 'font_family_id' => array( |
|
45 'description' => __( 'The ID for the parent font family of the font face.' ), |
|
46 'type' => 'integer', |
|
47 'required' => true, |
|
48 ), |
|
49 ), |
|
50 array( |
|
51 'methods' => WP_REST_Server::READABLE, |
|
52 'callback' => array( $this, 'get_items' ), |
|
53 'permission_callback' => array( $this, 'get_items_permissions_check' ), |
|
54 'args' => $this->get_collection_params(), |
|
55 ), |
|
56 array( |
|
57 'methods' => WP_REST_Server::CREATABLE, |
|
58 'callback' => array( $this, 'create_item' ), |
|
59 'permission_callback' => array( $this, 'create_item_permissions_check' ), |
|
60 'args' => $this->get_create_params(), |
|
61 ), |
|
62 'schema' => array( $this, 'get_public_item_schema' ), |
|
63 ) |
|
64 ); |
|
65 |
|
66 register_rest_route( |
|
67 $this->namespace, |
|
68 '/' . $this->rest_base . '/(?P<id>[\d]+)', |
|
69 array( |
|
70 'args' => array( |
|
71 'font_family_id' => array( |
|
72 'description' => __( 'The ID for the parent font family of the font face.' ), |
|
73 'type' => 'integer', |
|
74 'required' => true, |
|
75 ), |
|
76 'id' => array( |
|
77 'description' => __( 'Unique identifier for the font face.' ), |
|
78 'type' => 'integer', |
|
79 'required' => true, |
|
80 ), |
|
81 ), |
|
82 array( |
|
83 'methods' => WP_REST_Server::READABLE, |
|
84 'callback' => array( $this, 'get_item' ), |
|
85 'permission_callback' => array( $this, 'get_item_permissions_check' ), |
|
86 'args' => array( |
|
87 'context' => $this->get_context_param( array( 'default' => 'view' ) ), |
|
88 ), |
|
89 ), |
|
90 array( |
|
91 'methods' => WP_REST_Server::DELETABLE, |
|
92 'callback' => array( $this, 'delete_item' ), |
|
93 'permission_callback' => array( $this, 'delete_item_permissions_check' ), |
|
94 'args' => array( |
|
95 'force' => array( |
|
96 'type' => 'boolean', |
|
97 'default' => false, |
|
98 'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ), |
|
99 ), |
|
100 ), |
|
101 ), |
|
102 'schema' => array( $this, 'get_public_item_schema' ), |
|
103 ) |
|
104 ); |
|
105 } |
|
106 |
|
107 /** |
|
108 * Checks if a given request has access to font faces. |
|
109 * |
|
110 * @since 6.5.0 |
|
111 * |
|
112 * @param WP_REST_Request $request Full details about the request. |
|
113 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. |
|
114 */ |
|
115 public function get_items_permissions_check( $request ) { |
|
116 $post_type = get_post_type_object( $this->post_type ); |
|
117 |
|
118 if ( ! current_user_can( $post_type->cap->read ) ) { |
|
119 return new WP_Error( |
|
120 'rest_cannot_read', |
|
121 __( 'Sorry, you are not allowed to access font faces.' ), |
|
122 array( 'status' => rest_authorization_required_code() ) |
|
123 ); |
|
124 } |
|
125 |
|
126 return true; |
|
127 } |
|
128 |
|
129 /** |
|
130 * Checks if a given request has access to a font face. |
|
131 * |
|
132 * @since 6.5.0 |
|
133 * |
|
134 * @param WP_REST_Request $request Full details about the request. |
|
135 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. |
|
136 */ |
|
137 public function get_item_permissions_check( $request ) { |
|
138 $post = $this->get_post( $request['id'] ); |
|
139 if ( is_wp_error( $post ) ) { |
|
140 return $post; |
|
141 } |
|
142 |
|
143 if ( ! current_user_can( 'read_post', $post->ID ) ) { |
|
144 return new WP_Error( |
|
145 'rest_cannot_read', |
|
146 __( 'Sorry, you are not allowed to access this font face.' ), |
|
147 array( 'status' => rest_authorization_required_code() ) |
|
148 ); |
|
149 } |
|
150 |
|
151 return true; |
|
152 } |
|
153 |
|
154 /** |
|
155 * Validates settings when creating a font face. |
|
156 * |
|
157 * @since 6.5.0 |
|
158 * |
|
159 * @param string $value Encoded JSON string of font face settings. |
|
160 * @param WP_REST_Request $request Request object. |
|
161 * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. |
|
162 */ |
|
163 public function validate_create_font_face_settings( $value, $request ) { |
|
164 $settings = json_decode( $value, true ); |
|
165 |
|
166 // Check settings string is valid JSON. |
|
167 if ( null === $settings ) { |
|
168 return new WP_Error( |
|
169 'rest_invalid_param', |
|
170 __( 'font_face_settings parameter must be a valid JSON string.' ), |
|
171 array( 'status' => 400 ) |
|
172 ); |
|
173 } |
|
174 |
|
175 // Check that the font face settings match the theme.json schema. |
|
176 $schema = $this->get_item_schema()['properties']['font_face_settings']; |
|
177 $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); |
|
178 |
|
179 if ( is_wp_error( $has_valid_settings ) ) { |
|
180 $has_valid_settings->add_data( array( 'status' => 400 ) ); |
|
181 return $has_valid_settings; |
|
182 } |
|
183 |
|
184 // Check that none of the required settings are empty values. |
|
185 $required = $schema['required']; |
|
186 foreach ( $required as $key ) { |
|
187 if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { |
|
188 return new WP_Error( |
|
189 'rest_invalid_param', |
|
190 /* translators: %s: Name of the missing font face settings parameter, e.g. "font_face_settings[src]". */ |
|
191 sprintf( __( '%s cannot be empty.' ), "font_face_setting[ $key ]" ), |
|
192 array( 'status' => 400 ) |
|
193 ); |
|
194 } |
|
195 } |
|
196 |
|
197 $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); |
|
198 $files = $request->get_file_params(); |
|
199 |
|
200 foreach ( $srcs as $src ) { |
|
201 // Check that each src is a non-empty string. |
|
202 $src = ltrim( $src ); |
|
203 if ( empty( $src ) ) { |
|
204 return new WP_Error( |
|
205 'rest_invalid_param', |
|
206 /* translators: %s: Font face source parameter name: "font_face_settings[src]". */ |
|
207 sprintf( __( '%s values must be non-empty strings.' ), 'font_face_settings[src]' ), |
|
208 array( 'status' => 400 ) |
|
209 ); |
|
210 } |
|
211 |
|
212 // Check that srcs are valid URLs or file references. |
|
213 if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) { |
|
214 return new WP_Error( |
|
215 'rest_invalid_param', |
|
216 /* translators: 1: Font face source parameter name: "font_face_settings[src]", 2: The invalid src value. */ |
|
217 sprintf( __( '%1$s value "%2$s" must be a valid URL or file reference.' ), 'font_face_settings[src]', $src ), |
|
218 array( 'status' => 400 ) |
|
219 ); |
|
220 } |
|
221 } |
|
222 |
|
223 // Check that each file in the request references a src in the settings. |
|
224 foreach ( array_keys( $files ) as $file ) { |
|
225 if ( ! in_array( $file, $srcs, true ) ) { |
|
226 return new WP_Error( |
|
227 'rest_invalid_param', |
|
228 /* translators: 1: File key (e.g. "file-0") in the request data, 2: Font face source parameter name: "font_face_settings[src]". */ |
|
229 sprintf( __( 'File %1$s must be used in %2$s.' ), $file, 'font_face_settings[src]' ), |
|
230 array( 'status' => 400 ) |
|
231 ); |
|
232 } |
|
233 } |
|
234 |
|
235 return true; |
|
236 } |
|
237 |
|
238 /** |
|
239 * Sanitizes the font face settings when creating a font face. |
|
240 * |
|
241 * @since 6.5.0 |
|
242 * |
|
243 * @param string $value Encoded JSON string of font face settings. |
|
244 * @return array Decoded and sanitized array of font face settings. |
|
245 */ |
|
246 public function sanitize_font_face_settings( $value ) { |
|
247 // Settings arrive as stringified JSON, since this is a multipart/form-data request. |
|
248 $settings = json_decode( $value, true ); |
|
249 $schema = $this->get_item_schema()['properties']['font_face_settings']['properties']; |
|
250 |
|
251 // Sanitize settings based on callbacks in the schema. |
|
252 foreach ( $settings as $key => $value ) { |
|
253 $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; |
|
254 $settings[ $key ] = call_user_func( $sanitize_callback, $value ); |
|
255 } |
|
256 |
|
257 return $settings; |
|
258 } |
|
259 |
|
260 /** |
|
261 * Retrieves a collection of font faces within the parent font family. |
|
262 * |
|
263 * @since 6.5.0 |
|
264 * |
|
265 * @param WP_REST_Request $request Full details about the request. |
|
266 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
267 */ |
|
268 public function get_items( $request ) { |
|
269 $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); |
|
270 if ( is_wp_error( $font_family ) ) { |
|
271 return $font_family; |
|
272 } |
|
273 |
|
274 return parent::get_items( $request ); |
|
275 } |
|
276 |
|
277 /** |
|
278 * Retrieves a single font face within the parent font family. |
|
279 * |
|
280 * @since 6.5.0 |
|
281 * |
|
282 * @param WP_REST_Request $request Full details about the request. |
|
283 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
284 */ |
|
285 public function get_item( $request ) { |
|
286 $post = $this->get_post( $request['id'] ); |
|
287 if ( is_wp_error( $post ) ) { |
|
288 return $post; |
|
289 } |
|
290 |
|
291 // Check that the font face has a valid parent font family. |
|
292 $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); |
|
293 if ( is_wp_error( $font_family ) ) { |
|
294 return $font_family; |
|
295 } |
|
296 |
|
297 if ( (int) $font_family->ID !== (int) $post->post_parent ) { |
|
298 return new WP_Error( |
|
299 'rest_font_face_parent_id_mismatch', |
|
300 /* translators: %d: A post id. */ |
|
301 sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ), |
|
302 array( 'status' => 404 ) |
|
303 ); |
|
304 } |
|
305 |
|
306 return parent::get_item( $request ); |
|
307 } |
|
308 |
|
309 /** |
|
310 * Creates a font face for the parent font family. |
|
311 * |
|
312 * @since 6.5.0 |
|
313 * |
|
314 * @param WP_REST_Request $request Full details about the request. |
|
315 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
316 */ |
|
317 public function create_item( $request ) { |
|
318 $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); |
|
319 if ( is_wp_error( $font_family ) ) { |
|
320 return $font_family; |
|
321 } |
|
322 |
|
323 // Settings have already been decoded by ::sanitize_font_face_settings(). |
|
324 $settings = $request->get_param( 'font_face_settings' ); |
|
325 $file_params = $request->get_file_params(); |
|
326 |
|
327 // Check that the necessary font face properties are unique. |
|
328 $query = new WP_Query( |
|
329 array( |
|
330 'post_type' => $this->post_type, |
|
331 'posts_per_page' => 1, |
|
332 'title' => WP_Font_Utils::get_font_face_slug( $settings ), |
|
333 'update_post_meta_cache' => false, |
|
334 'update_post_term_cache' => false, |
|
335 ) |
|
336 ); |
|
337 if ( ! empty( $query->posts ) ) { |
|
338 return new WP_Error( |
|
339 'rest_duplicate_font_face', |
|
340 __( 'A font face matching those settings already exists.' ), |
|
341 array( 'status' => 400 ) |
|
342 ); |
|
343 } |
|
344 |
|
345 // Move the uploaded font asset from the temp folder to the fonts directory. |
|
346 if ( ! function_exists( 'wp_handle_upload' ) ) { |
|
347 require_once ABSPATH . 'wp-admin/includes/file.php'; |
|
348 } |
|
349 |
|
350 $srcs = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src']; |
|
351 $processed_srcs = array(); |
|
352 $font_file_meta = array(); |
|
353 |
|
354 foreach ( $srcs as $src ) { |
|
355 // If src not a file reference, use it as is. |
|
356 if ( ! isset( $file_params[ $src ] ) ) { |
|
357 $processed_srcs[] = $src; |
|
358 continue; |
|
359 } |
|
360 |
|
361 $file = $file_params[ $src ]; |
|
362 $font_file = $this->handle_font_file_upload( $file ); |
|
363 if ( is_wp_error( $font_file ) ) { |
|
364 return $font_file; |
|
365 } |
|
366 |
|
367 $processed_srcs[] = $font_file['url']; |
|
368 $font_file_meta[] = $this->relative_fonts_path( $font_file['file'] ); |
|
369 } |
|
370 |
|
371 // Store the updated settings for prepare_item_for_database to use. |
|
372 $settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs; |
|
373 $request->set_param( 'font_face_settings', $settings ); |
|
374 |
|
375 // Ensure that $settings data is slashed, so values with quotes are escaped. |
|
376 // WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content. |
|
377 $font_face_post = parent::create_item( $request ); |
|
378 |
|
379 if ( is_wp_error( $font_face_post ) ) { |
|
380 return $font_face_post; |
|
381 } |
|
382 |
|
383 $font_face_id = $font_face_post->data['id']; |
|
384 |
|
385 foreach ( $font_file_meta as $font_file_path ) { |
|
386 add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path ); |
|
387 } |
|
388 |
|
389 return $font_face_post; |
|
390 } |
|
391 |
|
392 /** |
|
393 * Deletes a single font face. |
|
394 * |
|
395 * @since 6.5.0 |
|
396 * |
|
397 * @param WP_REST_Request $request Full details about the request. |
|
398 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
|
399 */ |
|
400 public function delete_item( $request ) { |
|
401 $post = $this->get_post( $request['id'] ); |
|
402 if ( is_wp_error( $post ) ) { |
|
403 return $post; |
|
404 } |
|
405 |
|
406 $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); |
|
407 if ( is_wp_error( $font_family ) ) { |
|
408 return $font_family; |
|
409 } |
|
410 |
|
411 if ( (int) $font_family->ID !== (int) $post->post_parent ) { |
|
412 return new WP_Error( |
|
413 'rest_font_face_parent_id_mismatch', |
|
414 /* translators: %d: A post id. */ |
|
415 sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ), |
|
416 array( 'status' => 404 ) |
|
417 ); |
|
418 } |
|
419 |
|
420 $force = isset( $request['force'] ) ? (bool) $request['force'] : false; |
|
421 |
|
422 // We don't support trashing for font faces. |
|
423 if ( ! $force ) { |
|
424 return new WP_Error( |
|
425 'rest_trash_not_supported', |
|
426 /* translators: %s: force=true */ |
|
427 sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ), |
|
428 array( 'status' => 501 ) |
|
429 ); |
|
430 } |
|
431 |
|
432 return parent::delete_item( $request ); |
|
433 } |
|
434 |
|
435 /** |
|
436 * Prepares a single font face output for response. |
|
437 * |
|
438 * @since 6.5.0 |
|
439 * |
|
440 * @param WP_Post $item Post object. |
|
441 * @param WP_REST_Request $request Request object. |
|
442 * @return WP_REST_Response Response object. |
|
443 */ |
|
444 public function prepare_item_for_response( $item, $request ) { |
|
445 $fields = $this->get_fields_for_response( $request ); |
|
446 $data = array(); |
|
447 |
|
448 if ( rest_is_field_included( 'id', $fields ) ) { |
|
449 $data['id'] = $item->ID; |
|
450 } |
|
451 if ( rest_is_field_included( 'theme_json_version', $fields ) ) { |
|
452 $data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED; |
|
453 } |
|
454 |
|
455 if ( rest_is_field_included( 'parent', $fields ) ) { |
|
456 $data['parent'] = $item->post_parent; |
|
457 } |
|
458 |
|
459 if ( rest_is_field_included( 'font_face_settings', $fields ) ) { |
|
460 $data['font_face_settings'] = $this->get_settings_from_post( $item ); |
|
461 } |
|
462 |
|
463 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; |
|
464 $data = $this->add_additional_fields_to_object( $data, $request ); |
|
465 $data = $this->filter_response_by_context( $data, $context ); |
|
466 |
|
467 $response = rest_ensure_response( $data ); |
|
468 |
|
469 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { |
|
470 $links = $this->prepare_links( $item ); |
|
471 $response->add_links( $links ); |
|
472 } |
|
473 |
|
474 /** |
|
475 * Filters the font face data for a REST API response. |
|
476 * |
|
477 * @since 6.5.0 |
|
478 * |
|
479 * @param WP_REST_Response $response The response object. |
|
480 * @param WP_Post $post Font face post object. |
|
481 * @param WP_REST_Request $request Request object. |
|
482 */ |
|
483 return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request ); |
|
484 } |
|
485 |
|
486 /** |
|
487 * Retrieves the post's schema, conforming to JSON Schema. |
|
488 * |
|
489 * @since 6.5.0 |
|
490 * |
|
491 * @return array Item schema data. |
|
492 */ |
|
493 public function get_item_schema() { |
|
494 if ( $this->schema ) { |
|
495 return $this->add_additional_fields_schema( $this->schema ); |
|
496 } |
|
497 |
|
498 $schema = array( |
|
499 '$schema' => 'http://json-schema.org/draft-04/schema#', |
|
500 'title' => $this->post_type, |
|
501 'type' => 'object', |
|
502 // Base properties for every Post. |
|
503 'properties' => array( |
|
504 'id' => array( |
|
505 'description' => __( 'Unique identifier for the post.', 'default' ), |
|
506 'type' => 'integer', |
|
507 'context' => array( 'view', 'edit', 'embed' ), |
|
508 'readonly' => true, |
|
509 ), |
|
510 'theme_json_version' => array( |
|
511 'description' => __( 'Version of the theme.json schema used for the typography settings.' ), |
|
512 'type' => 'integer', |
|
513 'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, |
|
514 'minimum' => 2, |
|
515 'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, |
|
516 'context' => array( 'view', 'edit', 'embed' ), |
|
517 ), |
|
518 'parent' => array( |
|
519 'description' => __( 'The ID for the parent font family of the font face.' ), |
|
520 'type' => 'integer', |
|
521 'context' => array( 'view', 'edit', 'embed' ), |
|
522 ), |
|
523 // Font face settings come directly from theme.json schema |
|
524 // See https://schemas.wp.org/trunk/theme.json |
|
525 'font_face_settings' => array( |
|
526 'description' => __( 'font-face declaration in theme.json format.' ), |
|
527 'type' => 'object', |
|
528 'context' => array( 'view', 'edit', 'embed' ), |
|
529 'properties' => array( |
|
530 'fontFamily' => array( |
|
531 'description' => __( 'CSS font-family value.' ), |
|
532 'type' => 'string', |
|
533 'default' => '', |
|
534 'arg_options' => array( |
|
535 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), |
|
536 ), |
|
537 ), |
|
538 'fontStyle' => array( |
|
539 'description' => __( 'CSS font-style value.' ), |
|
540 'type' => 'string', |
|
541 'default' => 'normal', |
|
542 'arg_options' => array( |
|
543 'sanitize_callback' => 'sanitize_text_field', |
|
544 ), |
|
545 ), |
|
546 'fontWeight' => array( |
|
547 'description' => __( 'List of available font weights, separated by a space.' ), |
|
548 'default' => '400', |
|
549 // Changed from `oneOf` to avoid errors from loose type checking. |
|
550 // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check. |
|
551 'type' => array( 'string', 'integer' ), |
|
552 'arg_options' => array( |
|
553 'sanitize_callback' => 'sanitize_text_field', |
|
554 ), |
|
555 ), |
|
556 'fontDisplay' => array( |
|
557 'description' => __( 'CSS font-display value.' ), |
|
558 'type' => 'string', |
|
559 'default' => 'fallback', |
|
560 'enum' => array( |
|
561 'auto', |
|
562 'block', |
|
563 'fallback', |
|
564 'swap', |
|
565 'optional', |
|
566 ), |
|
567 'arg_options' => array( |
|
568 'sanitize_callback' => 'sanitize_text_field', |
|
569 ), |
|
570 ), |
|
571 'src' => array( |
|
572 'description' => __( 'Paths or URLs to the font files.' ), |
|
573 // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array, |
|
574 // and causing a "matches more than one of the expected formats" error. |
|
575 'anyOf' => array( |
|
576 array( |
|
577 'type' => 'string', |
|
578 ), |
|
579 array( |
|
580 'type' => 'array', |
|
581 'items' => array( |
|
582 'type' => 'string', |
|
583 ), |
|
584 ), |
|
585 ), |
|
586 'default' => array(), |
|
587 'arg_options' => array( |
|
588 'sanitize_callback' => function ( $value ) { |
|
589 return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value ); |
|
590 }, |
|
591 ), |
|
592 ), |
|
593 'fontStretch' => array( |
|
594 'description' => __( 'CSS font-stretch value.' ), |
|
595 'type' => 'string', |
|
596 'arg_options' => array( |
|
597 'sanitize_callback' => 'sanitize_text_field', |
|
598 ), |
|
599 ), |
|
600 'ascentOverride' => array( |
|
601 'description' => __( 'CSS ascent-override value.' ), |
|
602 'type' => 'string', |
|
603 'arg_options' => array( |
|
604 'sanitize_callback' => 'sanitize_text_field', |
|
605 ), |
|
606 ), |
|
607 'descentOverride' => array( |
|
608 'description' => __( 'CSS descent-override value.' ), |
|
609 'type' => 'string', |
|
610 'arg_options' => array( |
|
611 'sanitize_callback' => 'sanitize_text_field', |
|
612 ), |
|
613 ), |
|
614 'fontVariant' => array( |
|
615 'description' => __( 'CSS font-variant value.' ), |
|
616 'type' => 'string', |
|
617 'arg_options' => array( |
|
618 'sanitize_callback' => 'sanitize_text_field', |
|
619 ), |
|
620 ), |
|
621 'fontFeatureSettings' => array( |
|
622 'description' => __( 'CSS font-feature-settings value.' ), |
|
623 'type' => 'string', |
|
624 'arg_options' => array( |
|
625 'sanitize_callback' => 'sanitize_text_field', |
|
626 ), |
|
627 ), |
|
628 'fontVariationSettings' => array( |
|
629 'description' => __( 'CSS font-variation-settings value.' ), |
|
630 'type' => 'string', |
|
631 'arg_options' => array( |
|
632 'sanitize_callback' => 'sanitize_text_field', |
|
633 ), |
|
634 ), |
|
635 'lineGapOverride' => array( |
|
636 'description' => __( 'CSS line-gap-override value.' ), |
|
637 'type' => 'string', |
|
638 'arg_options' => array( |
|
639 'sanitize_callback' => 'sanitize_text_field', |
|
640 ), |
|
641 ), |
|
642 'sizeAdjust' => array( |
|
643 'description' => __( 'CSS size-adjust value.' ), |
|
644 'type' => 'string', |
|
645 'arg_options' => array( |
|
646 'sanitize_callback' => 'sanitize_text_field', |
|
647 ), |
|
648 ), |
|
649 'unicodeRange' => array( |
|
650 'description' => __( 'CSS unicode-range value.' ), |
|
651 'type' => 'string', |
|
652 'arg_options' => array( |
|
653 'sanitize_callback' => 'sanitize_text_field', |
|
654 ), |
|
655 ), |
|
656 'preview' => array( |
|
657 'description' => __( 'URL to a preview image of the font face.' ), |
|
658 'type' => 'string', |
|
659 'format' => 'uri', |
|
660 'default' => '', |
|
661 'arg_options' => array( |
|
662 'sanitize_callback' => 'sanitize_url', |
|
663 ), |
|
664 ), |
|
665 ), |
|
666 'required' => array( 'fontFamily', 'src' ), |
|
667 'additionalProperties' => false, |
|
668 ), |
|
669 ), |
|
670 ); |
|
671 |
|
672 $this->schema = $schema; |
|
673 |
|
674 return $this->add_additional_fields_schema( $this->schema ); |
|
675 } |
|
676 |
|
677 /** |
|
678 * Retrieves the item's schema for display / public consumption purposes. |
|
679 * |
|
680 * @since 6.5.0 |
|
681 * |
|
682 * @return array Public item schema data. |
|
683 */ |
|
684 public function get_public_item_schema() { |
|
685 |
|
686 $schema = parent::get_public_item_schema(); |
|
687 |
|
688 // Also remove `arg_options' from child font_family_settings properties, since the parent |
|
689 // controller only handles the top level properties. |
|
690 foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) { |
|
691 unset( $property['arg_options'] ); |
|
692 } |
|
693 |
|
694 return $schema; |
|
695 } |
|
696 |
|
697 /** |
|
698 * Retrieves the query params for the font face collection. |
|
699 * |
|
700 * @since 6.5.0 |
|
701 * |
|
702 * @return array Collection parameters. |
|
703 */ |
|
704 public function get_collection_params() { |
|
705 $query_params = parent::get_collection_params(); |
|
706 |
|
707 // Remove unneeded params. |
|
708 unset( |
|
709 $query_params['after'], |
|
710 $query_params['modified_after'], |
|
711 $query_params['before'], |
|
712 $query_params['modified_before'], |
|
713 $query_params['search'], |
|
714 $query_params['search_columns'], |
|
715 $query_params['slug'], |
|
716 $query_params['status'] |
|
717 ); |
|
718 |
|
719 $query_params['orderby']['default'] = 'id'; |
|
720 $query_params['orderby']['enum'] = array( 'id', 'include' ); |
|
721 |
|
722 /** |
|
723 * Filters collection parameters for the font face controller. |
|
724 * |
|
725 * @since 6.5.0 |
|
726 * |
|
727 * @param array $query_params JSON Schema-formatted collection parameters. |
|
728 */ |
|
729 return apply_filters( 'rest_wp_font_face_collection_params', $query_params ); |
|
730 } |
|
731 |
|
732 /** |
|
733 * Get the params used when creating a new font face. |
|
734 * |
|
735 * @since 6.5.0 |
|
736 * |
|
737 * @return array Font face create arguments. |
|
738 */ |
|
739 public function get_create_params() { |
|
740 $properties = $this->get_item_schema()['properties']; |
|
741 return array( |
|
742 'theme_json_version' => $properties['theme_json_version'], |
|
743 // When creating, font_face_settings is stringified JSON, to work with multipart/form-data used |
|
744 // when uploading font files. |
|
745 'font_face_settings' => array( |
|
746 'description' => __( 'font-face declaration in theme.json format, encoded as a string.' ), |
|
747 'type' => 'string', |
|
748 'required' => true, |
|
749 'validate_callback' => array( $this, 'validate_create_font_face_settings' ), |
|
750 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ), |
|
751 ), |
|
752 ); |
|
753 } |
|
754 |
|
755 /** |
|
756 * Get the parent font family, if the ID is valid. |
|
757 * |
|
758 * @since 6.5.0 |
|
759 * |
|
760 * @param int $font_family_id Supplied ID. |
|
761 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. |
|
762 */ |
|
763 protected function get_parent_font_family_post( $font_family_id ) { |
|
764 $error = new WP_Error( |
|
765 'rest_post_invalid_parent', |
|
766 __( 'Invalid post parent ID.', 'default' ), |
|
767 array( 'status' => 404 ) |
|
768 ); |
|
769 |
|
770 if ( (int) $font_family_id <= 0 ) { |
|
771 return $error; |
|
772 } |
|
773 |
|
774 $font_family_post = get_post( (int) $font_family_id ); |
|
775 |
|
776 if ( empty( $font_family_post ) || empty( $font_family_post->ID ) |
|
777 || 'wp_font_family' !== $font_family_post->post_type |
|
778 ) { |
|
779 return $error; |
|
780 } |
|
781 |
|
782 return $font_family_post; |
|
783 } |
|
784 |
|
785 /** |
|
786 * Prepares links for the request. |
|
787 * |
|
788 * @since 6.5.0 |
|
789 * |
|
790 * @param WP_Post $post Post object. |
|
791 * @return array Links for the given post. |
|
792 */ |
|
793 protected function prepare_links( $post ) { |
|
794 // Entity meta. |
|
795 return array( |
|
796 'self' => array( |
|
797 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), |
|
798 ), |
|
799 'collection' => array( |
|
800 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ), |
|
801 ), |
|
802 'parent' => array( |
|
803 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ), |
|
804 ), |
|
805 ); |
|
806 } |
|
807 |
|
808 /** |
|
809 * Prepares a single font face post for creation. |
|
810 * |
|
811 * @since 6.5.0 |
|
812 * |
|
813 * @param WP_REST_Request $request Request object. |
|
814 * @return stdClass Post object. |
|
815 */ |
|
816 protected function prepare_item_for_database( $request ) { |
|
817 $prepared_post = new stdClass(); |
|
818 |
|
819 // Settings have already been decoded by ::sanitize_font_face_settings(). |
|
820 $settings = $request->get_param( 'font_face_settings' ); |
|
821 |
|
822 // Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting, |
|
823 // which may contain multibyte characters. |
|
824 $title = WP_Font_Utils::get_font_face_slug( $settings ); |
|
825 |
|
826 $prepared_post->post_type = $this->post_type; |
|
827 $prepared_post->post_parent = $request['font_family_id']; |
|
828 $prepared_post->post_status = 'publish'; |
|
829 $prepared_post->post_title = $title; |
|
830 $prepared_post->post_name = sanitize_title( $title ); |
|
831 $prepared_post->post_content = wp_json_encode( $settings ); |
|
832 |
|
833 return $prepared_post; |
|
834 } |
|
835 |
|
836 /** |
|
837 * Sanitizes a single src value for a font face. |
|
838 * |
|
839 * @since 6.5.0 |
|
840 * |
|
841 * @param string $value Font face src that is a URL or the key for a $_FILES array item. |
|
842 * @return string Sanitized value. |
|
843 */ |
|
844 protected function sanitize_src( $value ) { |
|
845 $value = ltrim( $value ); |
|
846 return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value ); |
|
847 } |
|
848 |
|
849 /** |
|
850 * Handles the upload of a font file using wp_handle_upload(). |
|
851 * |
|
852 * @since 6.5.0 |
|
853 * |
|
854 * @param array $file Single file item from $_FILES. |
|
855 * @return array|WP_Error Array containing uploaded file attributes on success, or WP_Error object on failure. |
|
856 */ |
|
857 protected function handle_font_file_upload( $file ) { |
|
858 add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); |
|
859 // Filter the upload directory to return the fonts directory. |
|
860 add_filter( 'upload_dir', '_wp_filter_font_directory' ); |
|
861 |
|
862 $overrides = array( |
|
863 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ), |
|
864 // Not testing a form submission. |
|
865 'test_form' => false, |
|
866 // Only allow uploading font files for this request. |
|
867 'mimes' => WP_Font_Utils::get_allowed_font_mime_types(), |
|
868 ); |
|
869 |
|
870 // Bypasses is_uploaded_file() when running unit tests. |
|
871 if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { |
|
872 $overrides['action'] = 'wp_handle_mock_upload'; |
|
873 } |
|
874 |
|
875 $uploaded_file = wp_handle_upload( $file, $overrides ); |
|
876 |
|
877 remove_filter( 'upload_dir', '_wp_filter_font_directory' ); |
|
878 remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); |
|
879 |
|
880 return $uploaded_file; |
|
881 } |
|
882 |
|
883 /** |
|
884 * Handles file upload error. |
|
885 * |
|
886 * @since 6.5.0 |
|
887 * |
|
888 * @param array $file File upload data. |
|
889 * @param string $message Error message from wp_handle_upload(). |
|
890 * @return WP_Error WP_Error object. |
|
891 */ |
|
892 public function handle_font_file_upload_error( $file, $message ) { |
|
893 $status = 500; |
|
894 $code = 'rest_font_upload_unknown_error'; |
|
895 |
|
896 if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) { |
|
897 $status = 400; |
|
898 $code = 'rest_font_upload_invalid_file_type'; |
|
899 } |
|
900 |
|
901 return new WP_Error( $code, $message, array( 'status' => $status ) ); |
|
902 } |
|
903 |
|
904 /** |
|
905 * Returns relative path to an uploaded font file. |
|
906 * |
|
907 * The path is relative to the current fonts directory. |
|
908 * |
|
909 * @since 6.5.0 |
|
910 * @access private |
|
911 * |
|
912 * @param string $path Full path to the file. |
|
913 * @return string Relative path on success, unchanged path on failure. |
|
914 */ |
|
915 protected function relative_fonts_path( $path ) { |
|
916 $new_path = $path; |
|
917 |
|
918 $fonts_dir = wp_get_font_dir(); |
|
919 if ( str_starts_with( $new_path, $fonts_dir['basedir'] ) ) { |
|
920 $new_path = str_replace( $fonts_dir['basedir'], '', $new_path ); |
|
921 $new_path = ltrim( $new_path, '/' ); |
|
922 } |
|
923 |
|
924 return $new_path; |
|
925 } |
|
926 |
|
927 /** |
|
928 * Gets the font face's settings from the post. |
|
929 * |
|
930 * @since 6.5.0 |
|
931 * |
|
932 * @param WP_Post $post Font face post object. |
|
933 * @return array Font face settings array. |
|
934 */ |
|
935 protected function get_settings_from_post( $post ) { |
|
936 $settings = json_decode( $post->post_content, true ); |
|
937 $properties = $this->get_item_schema()['properties']['font_face_settings']['properties']; |
|
938 |
|
939 // Provide required, empty settings if needed. |
|
940 if ( null === $settings ) { |
|
941 $settings = array( |
|
942 'fontFamily' => '', |
|
943 'src' => array(), |
|
944 ); |
|
945 } |
|
946 |
|
947 // Only return the properties defined in the schema. |
|
948 return array_intersect_key( $settings, $properties ); |
|
949 } |
|
950 } |