[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-font-faces-controller.php (source)

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


Generated : Thu Jun 18 08:20:10 2026 Cross-referenced by PHPXref