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 Library REST API: sanitize font family and font face settings #58590

Merged
merged 9 commits into from Feb 5, 2024
52 changes: 29 additions & 23 deletions lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php
Expand Up @@ -21,35 +21,41 @@
*/
class WP_Font_Utils {
/**
* Format font family names.
* Sanitizes and formats font family names.
*
* Adds surrounding quotes to font family names containing spaces and not already quoted.
* - Applies `sanitize_text_field`
* - Adds surrounding quotes to names that contain spaces and are not already quoted
*
* @since 6.5.0
* @access private
*
* @see sanitize_text_field()
*
* @param string $font_family Font family name(s), comma-separated.
* @return string Formatted font family name(s).
* @return string Sanitized and formatted font family name(s).
*/
public static function format_font_family( $font_family ) {
if ( $font_family ) {
$font_families = explode( ',', $font_family );
$wrapped_font_families = array_map(
function ( $family ) {
$trimmed = trim( $family );
if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) {
return '"' . $trimmed . '"';
}
return $trimmed;
},
$font_families
);

if ( count( $wrapped_font_families ) === 1 ) {
$font_family = $wrapped_font_families[0];
} else {
$font_family = implode( ', ', $wrapped_font_families );
}
public static function sanitize_font_family( $font_family ) {
if ( ! $font_family ) {
return '';
}

$font_family = sanitize_text_field( $font_family );
$font_families = explode( ',', $font_family );
$wrapped_font_families = array_map(
function ( $family ) {
$trimmed = trim( $family );
if ( ! empty( $trimmed ) && false !== strpos( $trimmed, ' ' ) && false === strpos( $trimmed, "'" ) && false === strpos( $trimmed, '"' ) ) {
return '"' . $trimmed . '"';
}
return $trimmed;
},
$font_families
);

if ( count( $wrapped_font_families ) === 1 ) {
$font_family = $wrapped_font_families[0];
} else {
$font_family = implode( ', ', $wrapped_font_families );
}

return $font_family;
Expand Down Expand Up @@ -128,7 +134,7 @@ function ( $elem ) {
$slug_elements
);

return join( ';', $slug_elements );
return sanitize_text_field( join( ';', $slug_elements ) );
}

/**
Expand Down
Expand Up @@ -187,20 +187,32 @@ public function validate_create_font_face_settings( $value, $request ) {
}
}

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

// 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 )
);
foreach ( $srcs as $src ) {
// Check that each src is a non-empty string.
$src = ltrim( $src );
if ( empty( $src ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'font_face_settings[src] values must be non-empty strings.', 'gutenberg' ),
array( 'status' => 400 )
);
}

// Check that srcs are valid URLs or file references.
if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: src value in the font face settings. */
sprintf( __( 'font_face_settings[src] value "%s" must be a valid URL or file reference.', 'gutenberg' ), $src ),
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(
Expand All @@ -227,9 +239,12 @@ public function validate_create_font_face_settings( $value, $request ) {
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 );
$schema = $this->get_item_schema()['properties']['font_face_settings']['properties'];

if ( isset( $settings['fontFamily'] ) ) {
$settings['fontFamily'] = WP_Font_Utils::format_font_family( $settings['fontFamily'] );
// Sanitize settings based on callbacks in the schema.
foreach ( $settings as $key => $value ) {
$sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
$settings[ $key ] = call_user_func( $sanitize_callback, $value );
}

return $settings;
Expand Down Expand Up @@ -509,18 +524,27 @@ public function get_item_schema() {
'description' => __( 'CSS font-family value.', 'gutenberg' ),
'type' => 'string',
'default' => '',
'arg_options' => array(
'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
),
),
'fontStyle' => array(
'description' => __( 'CSS font-style value.', 'gutenberg' ),
'type' => 'string',
'default' => 'normal',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontWeight' => array(
'description' => __( 'List of available font weights, separated by a space.', 'gutenberg' ),
'default' => '400',
// 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' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontDisplay' => array(
'description' => __( 'CSS font-display value.', 'gutenberg' ),
Expand All @@ -533,10 +557,14 @@ public function get_item_schema() {
'swap',
'optional',
),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'src' => array(
'description' => __( 'Paths or URLs to the font files.', 'gutenberg' ),
// Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array.
// Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array,
// and causing a "matches more than one of the expected formats" error.
'anyOf' => array(
array(
'type' => 'string',
Expand All @@ -549,46 +577,83 @@ public function get_item_schema() {
),
),
'default' => array(),
'arg_options' => array(
'sanitize_callback' => function ( $value ) {
return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value );
},
),
),
'fontStretch' => array(
'description' => __( 'CSS font-stretch value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'ascentOverride' => array(
'description' => __( 'CSS ascent-override value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'descentOverride' => array(
'description' => __( 'CSS descent-override value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontVariant' => array(
'description' => __( 'CSS font-variant value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontFeatureSettings' => array(
'description' => __( 'CSS font-feature-settings value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontVariationSettings' => array(
'description' => __( 'CSS font-variation-settings value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'lineGapOverride' => array(
'description' => __( 'CSS line-gap-override value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'sizeAdjust' => array(
'description' => __( 'CSS size-adjust value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'unicodeRange' => array(
'description' => __( 'CSS unicode-range value.', 'gutenberg' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'preview' => array(
'description' => __( 'URL to a preview image of the font face.', 'gutenberg' ),
'type' => 'string',
'format' => 'uri',
'default' => '',
'arg_options' => array(
'sanitize_callback' => 'sanitize_url',
),
),
),
'required' => array( 'fontFamily', 'src' ),
Expand All @@ -602,6 +667,26 @@ public function get_item_schema() {
return $this->add_additional_fields_schema( $this->schema );
}

/**
* Retrieves the item's schema for display / public consumption purposes.
*
* @since 6.5.0
*
* @return array Public item schema data.
*/
public function get_public_item_schema() {

$schema = parent::get_public_item_schema();

// Also remove `arg_options' from child font_family_settings properties, since the parent
// controller only handles the top level properties.
foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) {
unset( $property['arg_options'] );
}

return $schema;
}

/**
* Retrieves the query params for the font face collection.
*
Expand Down Expand Up @@ -739,6 +824,20 @@ protected function prepare_item_for_database( $request ) {
return $prepared_post;
}

/**
* Sanitizes a single src value for a font face.
*
* @since 6.5.0
*
* @param string $value Font face src that is a URL or the key for a $_FILES array item.
*
* @return string Sanitized value.
*/
protected function sanitize_src( $value ) {
$value = ltrim( $value );
return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value );
}

/**
* Handles the upload of a font file using wp_handle_upload().
*
Expand Down