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