Skip to content

Commit

Permalink
Font Faces endpoint: prevent creating font faces with duplicate setti…
Browse files Browse the repository at this point in the history
…ngs (#57903)
  • Loading branch information
creativecoder committed Jan 22, 2024
1 parent 13b5640 commit 3e37968
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static function format_font_family( $font_family ) {
function ( $family ) {
$trimmed = trim( $family );
if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) {
return "'" . $trimmed . "'";
return '"' . $trimmed . '"';
}
return $trimmed;
},
Expand All @@ -107,4 +107,84 @@ function ( $family ) {

return $font_family;
}

/**
* Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF`
*
* Used for comparison with other font faces in the same family, to prevent duplicates
* that would both match according the CSS font matching spec. Uses only simple case-insensitive
* matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or
* unicode ranges.
*
* @since 6.5.0
*
* @link https://drafts.csswg.org/css-fonts/#font-style-matching
*
* @param array $settings {
* Font face settings.
*
* @type string $fontFamily Font family name.
* @type string $fontStyle Optional font style, defaults to 'normal'.
* @type string $fontWeight Optional font weight, defaults to 400.
* @type string $fontStretch Optional font stretch, defaults to '100%'.
* @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'.
* }
* @return string Font face slug.
*/
public static function get_font_face_slug( $settings ) {
$settings = wp_parse_args(
$settings,
array(
'fontFamily' => '',
'fontStyle' => 'normal',
'fontWeight' => '400',
'fontStretch' => '100%',
'unicodeRange' => 'U+0-10FFFF',
)
);

// Convert all values to lowercase for comparison.
// Font family names may use multibyte characters.
$font_family = mb_strtolower( $settings['fontFamily'] );
$font_style = strtolower( $settings['fontStyle'] );
$font_weight = strtolower( $settings['fontWeight'] );
$font_stretch = strtolower( $settings['fontStretch'] );
$unicode_range = strtoupper( $settings['unicodeRange'] );

// Convert weight keywords to numeric strings.
$font_weight = str_replace( 'normal', '400', $font_weight );
$font_weight = str_replace( 'bold', '700', $font_weight );

// Convert stretch keywords to numeric strings.
$font_stretch_map = array(
'ultra-condensed' => '50%',
'extra-condensed' => '62.5%',
'condensed' => '75%',
'semi-condensed' => '87.5%',
'normal' => '100%',
'semi-expanded' => '112.5%',
'expanded' => '125%',
'extra-expanded' => '150%',
'untra-expanded' => '200%',
);
$font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch );

$slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range );

$slug_elements = array_map(
function ( $elem ) {
// Remove quotes to normalize font-family names, and ';' to use as a separator.
$elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) );

// Normalize comma separated lists by removing whitespace in between items,
// but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts).
// CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE,
// which by default are all matched by \s in PHP.
return preg_replace( '/,\s+/', ',', $elem );
},
$slug_elements
);

return join( ';', $slug_elements );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,22 @@ public function create_item( $request ) {
$settings = $request->get_param( 'font_face_settings' );
$file_params = $request->get_file_params();

// Check that the necessary font face properties are unique.
$existing_font_face = get_posts(
array(
'post_type' => $this->post_type,
'posts_per_page' => 1,
'title' => WP_Font_Family_Utils::get_font_face_slug( $settings ),
)
);
if ( ! empty( $existing_font_face ) ) {
return new WP_Error(
'rest_duplicate_font_face',
__( 'A font face matching those settings already exists.', 'gutenberg' ),
array( 'status' => 400 )
);
}

// Move the uploaded font asset from the temp folder to the fonts directory.
if ( ! function_exists( 'wp_handle_upload' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
Expand Down Expand Up @@ -648,11 +664,15 @@ protected function prepare_item_for_database( $request ) {
// Settings have already been decoded by ::sanitize_font_face_settings().
$settings = $request->get_param( 'font_face_settings' );

// Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting,
// which may contain multibyte characters.
$title = WP_Font_Family_Utils::get_font_face_slug( $settings );

$prepared_post->post_type = $this->post_type;
$prepared_post->post_parent = $request['font_family_id'];
$prepared_post->post_status = 'publish';
$prepared_post->post_title = $settings['fontFamily'];
$prepared_post->post_name = sanitize_title( $settings['fontFamily'] );
$prepared_post->post_title = $title;
$prepared_post->post_name = sanitize_title( $title );
$prepared_post->post_content = wp_json_encode( $settings );

return $prepared_post;
Expand Down Expand Up @@ -751,10 +771,10 @@ protected function get_settings_from_post( $post ) {
// Provide required, empty settings if needed.
if ( null === $settings ) {
$settings = array(
'src' => array(),
'fontFamily' => '',
'src' => array(),
);
}
$settings['fontFamily'] = $post->post_title ?? '';

// Only return the properties defined in the schema.
return array_intersect_key( $settings, $properties );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@ public function data_should_format_font_family() {
return array(
'data_families_with_spaces_and_numbers' => array(
'font_family' => 'Rock 3D , Open Sans,serif',
'expected' => "'Rock 3D', 'Open Sans', serif",
'expected' => '"Rock 3D", "Open Sans", serif',
),
'data_single_font_family' => array(
'font_family' => 'Rock 3D',
'expected' => "'Rock 3D'",
'expected' => '"Rock 3D"',
),
'data_no_spaces' => array(
'font_family' => 'Rock3D',
'expected' => 'Rock3D',
),
'data_many_spaces_and_existing_quotes' => array(
'font_family' => 'Rock 3D serif, serif,sans-serif, "Open Sans"',
'expected' => "'Rock 3D serif', serif, sans-serif, \"Open Sans\"",
'expected' => '"Rock 3D serif", serif, sans-serif, "Open Sans"',
),
'data_empty_family' => array(
'font_family' => ' ',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
/**
* Test WP_Font_Family_Utils::get_font_face_slug().
*
* @package WordPress
* @subpackage Font Library
* *
* @covers WP_Font_Family_Utils::get_font_face_slug
*/
class Tests_Fonts_WpFontsFamilyUtils_GetFontFamilySlug extends WP_UnitTestCase {
/**
* @dataProvider data_get_font_face_slug_normalizes_values
*/
public function test_get_font_face_slug_normalizes_values( $settings, $expected_slug ) {
$slug = WP_Font_Family_Utils::get_font_face_slug( $settings );

$this->assertSame( $expected_slug, $slug );
}

public function data_get_font_face_slug_normalizes_values() {
return array(
'Sets defaults' => array(
'settings' => array(
'fontFamily' => 'Open Sans',
),
'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
),
'Converts normal weight to 400' => array(
'settings' => array(
'fontFamily' => 'Open Sans',
'fontWeight' => 'normal',
),
'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
),
'Converts bold weight to 700' => array(
'settings' => array(
'fontFamily' => 'Open Sans',
'fontWeight' => 'bold',
),
'expected_slug' => 'open sans;normal;700;100%;U+0-10FFFF',
),
'Converts normal font-stretch to 100%' => array(
'settings' => array(
'fontFamily' => 'Open Sans',
'fontStretch' => 'normal',
),
'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
),
'Removes double quotes from fontFamilies' => array(
'settings' => array(
'fontFamily' => '"Open Sans"',
),
'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
),
'Removes single quotes from fontFamilies' => array(
'settings' => array(
'fontFamily' => "'Open Sans'",
),
'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
),
'Removes spaces between comma separated font families' => array(
'settings' => array(
'fontFamily' => 'Open Sans, serif',
),
'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
),
'Removes tabs between comma separated font families' => array(
'settings' => array(
'fontFamily' => "Open Sans,\tserif",
),
'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
),
'Removes new lines between comma separated font families' => array(
'settings' => array(
'fontFamily' => "Open Sans,\nserif",
),
'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
),
);
}
}
Loading

0 comments on commit 3e37968

Please sign in to comment.