Skip to content

Commit

Permalink
Font Family and Font Face REST API endpoints: better data handling an…
Browse files Browse the repository at this point in the history
…d errors (#57843)
  • Loading branch information
creativecoder committed Jan 22, 2024
1 parent 998f084 commit 90b5717
Show file tree
Hide file tree
Showing 5 changed files with 484 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public function register_routes() {
}

/**
* Checks if a given request has access to read posts.
* Checks if a given request has access to font faces.
*
* @since 6.5.0
*
Expand All @@ -140,41 +140,69 @@ public function get_font_faces_permissions_check() {
return true;
}

/**
* Validates settings when creating a font face.
*
* @since 6.5.0
*
* @param string $value Encoded JSON string of font face settings.
* @param WP_REST_Request $request Request object.
* @return false|WP_Error True if the settings are valid, otherwise a WP_Error object.
*/
public function validate_create_font_face_settings( $value, $request ) {
$settings = json_decode( $value, true );
$schema = $this->get_item_schema()['properties']['font_face_settings'];

// Check settings string is valid JSON.
if ( null === $settings ) {
return new WP_Error(
'rest_invalid_param',
__( 'font_face_settings parameter must be a valid JSON string.', 'gutenberg' ),
array( 'status' => 400 )
);
}

// Check that the font face settings match the theme.json schema.
$valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' );
$schema = $this->get_item_schema()['properties']['font_face_settings'];
$has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' );

// Some properties trigger a multiple "oneOf" types error that we ignore, because they are still valid.
// e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric type checking.
if ( is_wp_error( $valid_settings ) && $valid_settings->get_error_code() !== 'rest_one_of_multiple_matches' ) {
$valid_settings->add_data( array( 'status' => 400 ) );
return $valid_settings;
if ( is_wp_error( $has_valid_settings ) ) {
$has_valid_settings->add_data( array( 'status' => 400 ) );
return $has_valid_settings;
}

$srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] );
$files = $request->get_file_params();

// Check that each file in the request references a src in the settings.
foreach ( array_keys( $files ) as $file ) {
if ( ! in_array( $file, $srcs, true ) ) {
// Check that none of the required settings are empty values.
$required = $schema['required'];
foreach ( $required as $key ) {
if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: A URL. */
__( 'Every file uploaded must be used as a font face src.', 'gutenberg' ),
/* translators: %s: Font family setting key. */
sprintf( __( 'font_face_setting[%s] cannot be empty.', 'gutenberg' ), $key ),
array( 'status' => 400 )
);
}
}

// Check that src strings are non-empty.
foreach ( $srcs as $src ) {
if ( ! $src ) {
$srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] );

// Check that srcs are non-empty strings.
$filtered_src = array_filter( array_filter( $srcs, 'is_string' ) );
if ( empty( $filtered_src ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ),
array( 'status' => 400 )
);
}

// Check that each file in the request references a src in the settings.
$files = $request->get_file_params();
foreach ( array_keys( $files ) as $file ) {
if ( ! in_array( $file, $srcs, true ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'Font face src values must be non-empty strings.', 'gutenberg' ),
// translators: %s: File key (e.g. `file-0`) in the request data.
sprintf( __( 'File %1$s must be used in font_face_settings[src].', 'gutenberg' ), $file ),
array( 'status' => 400 )
);
}
Expand All @@ -183,6 +211,26 @@ public function validate_create_font_face_settings( $value, $request ) {
return true;
}

/**
* Sanitizes the font face settings when creating a font face.
*
* @since 6.5.0
*
* @param string $value Encoded JSON string of font face settings.
* @param WP_REST_Request $request Request object.
* @return array Decoded array of font face settings.
*/
public function sanitize_font_face_settings( $value ) {
// Settings arrive as stringified JSON, since this is a multipart/form-data request.
$settings = json_decode( $value, true );

if ( isset( $settings['fontFamily'] ) ) {
$settings['fontFamily'] = WP_Font_Family_Utils::format_font_family( $settings['fontFamily'] );
}

return $settings;
}

/**
* Retrieves a collection of font faces within the parent font family.
*
Expand All @@ -201,7 +249,7 @@ public function get_items( $request ) {
}

/**
* Retrieves a single font face for within parent font family.
* Retrieves a single font face within the parent font family.
*
* @since 6.5.0
*
Expand All @@ -214,6 +262,7 @@ public function get_item( $request ) {
return $post;
}

// Check that the font face has a valid parent font family.
$font_family = $this->get_font_family_post( $request['font_family_id'] );
if ( is_wp_error( $font_family ) ) {
return $font_family;
Expand Down Expand Up @@ -242,8 +291,8 @@ public function get_item( $request ) {
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
// Settings arrive as stringified JSON, since this is a multipart/form-data request.
$settings = json_decode( $request->get_param( 'font_face_settings' ), true );
// Settings have already been decoded by ::sanitize_font_face_settings().
$settings = $request->get_param( 'font_face_settings' );
$file_params = $request->get_file_params();

// Move the uploaded font asset from the temp folder to the fonts directory.
Expand All @@ -264,12 +313,8 @@ public function create_item( $request ) {

$file = $file_params[ $src ];
$font_file = $this->handle_font_file_upload( $file );
if ( isset( $font_file['error'] ) ) {
return new WP_Error(
'rest_font_upload_unknown_error',
$font_file['error'],
array( 'status' => 500 )
);
if ( is_wp_error( $font_file ) ) {
return $font_file;
}

$processed_srcs[] = $font_file['url'];
Expand Down Expand Up @@ -336,7 +381,20 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V
$data['id'] = $item->ID;
$data['theme_json_version'] = 2;
$data['parent'] = $item->post_parent;
$data['font_face_settings'] = json_decode( $item->post_content, true );

$settings = json_decode( $item->post_content, true );
$properties = $this->get_item_schema()['properties']['font_face_settings']['properties'];

// Provide required, empty settings if the post_content is not valid JSON.
if ( null === $settings ) {
$settings = array(
'fontFamily' => '',
'src' => array(),
);
}

// Only return the properties defined in the schema.
$data['font_face_settings'] = array_intersect_key( $settings, $properties );

$response = rest_ensure_response( $data );
$links = $this->prepare_links( $item );
Expand Down Expand Up @@ -369,7 +427,7 @@ public function get_item_schema() {
'readonly' => true,
),
'theme_json_version' => array(
'description' => __( 'Version of the theme.json schema used for the font face typography settings.', 'gutenberg' ),
'description' => __( 'Version of the theme.json schema used for the typography settings.', 'gutenberg' ),
'type' => 'integer',
'default' => 2,
'minimum' => 2,
Expand Down Expand Up @@ -398,14 +456,9 @@ public function get_item_schema() {
'fontWeight' => array(
'description' => 'List of available font weights, separated by a space.',
'default' => '400',
'oneOf' => array(
array(
'type' => 'string',
),
array(
'type' => 'integer',
),
),
// Changed from `oneOf` to avoid errors from loose type checking.
// e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check.
'type' => array( 'string', 'integer' ),
),
'fontDisplay' => array(
'description' => 'CSS font-display value.',
Expand All @@ -421,7 +474,8 @@ public function get_item_schema() {
),
'src' => array(
'description' => 'Paths or URLs to the font files.',
'oneOf' => array(
// Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array.
'anyOf' => array(
array(
'type' => 'string',
),
Expand Down Expand Up @@ -494,30 +548,12 @@ public function get_item_schema() {
* @return array Collection parameters.
*/
public function get_collection_params() {
$params = parent::get_collection_params();

return array(
'page' => array(
'description' => __( 'Current page of the collection.', 'default' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.', 'default' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'search' => array(
'description' => __( 'Limit results to those matching a string.', 'default' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
'page' => $params['page'],
'per_page' => $params['per_page'],
'search' => $params['search'],
);
}

Expand All @@ -539,6 +575,7 @@ public function get_create_params() {
'type' => 'string',
'required' => true,
'validate_callback' => array( $this, 'validate_create_font_face_settings' ),
'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ),
),
);
}
Expand Down Expand Up @@ -610,10 +647,18 @@ protected function prepare_links( $post ) {
return $links;
}

/**
* Prepares a single font face post for creation.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = new stdClass();

// Settings have already been decoded and processed by create_item().
// Settings have already been decoded by ::sanitize_font_face_settings().
$settings = $request->get_param( 'font_face_settings' );

$prepared_post->post_type = $this->post_type;
Expand All @@ -626,19 +671,30 @@ protected function prepare_item_for_database( $request ) {
return $prepared_post;
}

/**
* Handles the upload of a font file using wp_handle_upload().
*
* @since 6.5.0
*
* @param array $file Single file item from $_FILES.
* @return array Array containing uploaded file attributes on success, or error on failure.
*/
protected function handle_font_file_upload( $file ) {
add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) );
add_filter( 'upload_dir', 'wp_get_font_dir' );

$overrides = array(
'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ),
// Arbitrary string to avoid the is_uploaded_file() check applied
// when using 'wp_handle_upload'.
'action' => 'wp_handle_font_upload',
'action' => 'wp_handle_font_upload',
// Not testing a form submission.
'test_form' => false,
'test_form' => false,
// Seems mime type for files that are not images cannot be tested.
// See wp_check_filetype_and_ext().
'test_type' => true,
'test_type' => true,
// Only allow uploading font files for this request.
'mimes' => WP_Font_Library::get_expected_font_mime_types_per_php_version(),
);

$uploaded_file = wp_handle_upload( $file, $overrides );
Expand All @@ -649,6 +705,27 @@ protected function handle_font_file_upload( $file ) {
return $uploaded_file;
}

/**
* Handles file upload error.
*
* @since 6.5.0
*
* @param array $file File upload data.
* @param string $message Error message from wp_handle_upload().
* @return WP_Error WP_Error object.
*/
public function handle_font_file_upload_error( $file, $message ) {
$status = 500;
$code = 'rest_font_upload_unknown_error';

if ( 'Sorry, you are not allowed to upload this file type.' === $message ) {
$status = 400;
$code = 'rest_font_upload_invalid_file_type';
}

return new WP_Error( $code, $message, array( 'status' => $status ) );
}

/**
* Returns relative path to an uploaded font file.
*
Expand Down
Loading

0 comments on commit 90b5717

Please sign in to comment.