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