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


Generated : Fri Oct 9 08:20:33 2026 Cross-referenced by PHPXref