Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c8072d4
Font Library: add wp_font_face post type and scaffold font face REST …
creativecoder Jan 9, 2024
61da35c
Font Library: create font faces through the REST API (#57702)
creativecoder Jan 11, 2024
42565a3
Refactor Font Family Controller (#57785)
creativecoder Jan 12, 2024
afacdf2
Font Family and Font Face REST API endpoints: better data handling an…
creativecoder Jan 15, 2024
7c82cb9
Font Families REST API endpoint: ensure unique font family slugs (#57…
creativecoder Jan 16, 2024
f84cc6d
Font Library: delete child font faces and font assets when deleting p…
creativecoder Jan 16, 2024
492a3ee
Font Library: refactor client side install functions to work with rev…
jffng Jan 17, 2024
9ebc25f
Cleanup/font library view error handling (#57926)
pbking Jan 17, 2024
92ff955
Fix unique key prop warning when opening modal
mikachan Jan 17, 2024
5b57f3e
Add key props to FontsGrid children
mikachan Jan 17, 2024
8a5efd4
Font Faces endpoint: prevent creating font faces with duplicate setti…
creativecoder Jan 17, 2024
3a739bc
Font Library: Update uninstall/delete on client side (#57932)
mikachan Jan 18, 2024
22253e5
Add slug/id back to FontCollection
mikachan Jan 18, 2024
c646e3d
Change tabsFromCollections inline with Font Collections PR
mikachan Jan 18, 2024
2d3abce
Use child.key for key prop in FontsGrid
mikachan Jan 18, 2024
c6e0fbb
Update packages/edit-site/src/components/global-styles/font-library-m…
mikachan Jan 18, 2024
74be330
Merge branch 'trunk' into try/font-library-refactor
mikachan Jan 18, 2024
a666bb5
Font Library: address JS feedback in #57688 (#57961)
mikachan Jan 18, 2024
32689e1
Font Library REST API endpoints: address initial feedback from featur…
creativecoder Jan 19, 2024
2aa9c64
Font Library: font collection refactor to use the new schema (#57884)
matiasbenedetto Jan 19, 2024
fe2c0ae
Merge branch 'try/font-library-refactor' into font-library/fix-react-…
mikachan Jan 19, 2024
ab889a7
Merge branch 'try/font-library-refactor' into font-library/fix-react-…
mikachan Jan 23, 2024
75c27e5
Remove old WP_REST_Autosave_Fonts_Controller class
mikachan Jan 23, 2024
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
Prev Previous commit
Next Next commit
Font Family and Font Face REST API endpoints: better data handling an…
…d errors (#57843)
  • Loading branch information
creativecoder authored Jan 15, 2024
commit afacdf2d4e683871956a3d3963738213cd4f82bb
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