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