Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Font Family and Font Face REST API endpoints: better data handling and errors #57843

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading