Skip to content

Commit

Permalink
Font Library: sanitize font collection data (#58636)
Browse files Browse the repository at this point in the history
Co-authored-by: matiasbenedetto <mmaattiiaass@git.wordpress.org>
Co-authored-by: creativecoder <grantmkin@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
  • Loading branch information
4 people committed Feb 5, 2024
1 parent 2337198 commit b942f10
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 43 deletions.
104 changes: 76 additions & 28 deletions lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php
Expand Up @@ -31,7 +31,7 @@ final class WP_Font_Collection {
*
* @since 6.5.0
*
* @var array
* @var array|WP_Error|null
*/
private $data;

Expand All @@ -40,7 +40,7 @@ final class WP_Font_Collection {
*
* @since 6.5.0
*
* @var string
* @var string|null
*/
private $src;

Expand All @@ -62,10 +62,10 @@ final class WP_Font_Collection {
public function __construct( $slug, $data_or_file ) {
$this->slug = sanitize_title( $slug );

// Data or json are lazy loaded and validated in get_data().
if ( is_array( $data_or_file ) ) {
$this->data = $data_or_file;
$this->data = $this->sanitize_and_validate_data( $data_or_file );
} else {
// JSON data is lazy loaded by ::get_data().
$this->src = $data_or_file;
}

Expand All @@ -87,30 +87,21 @@ public function __construct( $slug, $data_or_file ) {
* @return array|WP_Error An array containing the font collection data, or a WP_Error on failure.
*/
public function get_data() {
// If we have a JSON config, load it and cache the data if it's valid.
// If the collection uses JSON data, load it and cache the data/error.
if ( $this->src && empty( $this->data ) ) {
$data = $this->load_from_json( $this->src );
if ( is_wp_error( $data ) ) {
return $data;
}

$this->data = $data;
$this->data = $this->load_from_json( $this->src );
}

// Validate required properties are not empty.
$data = $this->validate_data( $this->data );
if ( is_wp_error( $data ) ) {
return $data;
if ( is_wp_error( $this->data ) ) {
return $this->data;
}

// Set defaults for optional properties.
return wp_parse_args(
$data,
array(
'description' => '',
'categories' => array(),
)
$defaults = array(
'description' => '',
'categories' => array(),
);
return wp_parse_args( $this->data, $defaults );
}

/**
Expand Down Expand Up @@ -151,7 +142,7 @@ private function load_from_file( $file ) {
return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.', 'gutenberg' ) );
}

return $data;
return $this->sanitize_and_validate_data( $data );
}

/**
Expand Down Expand Up @@ -180,8 +171,8 @@ private function load_from_url( $url ) {
return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the REST response JSON.', 'gutenberg' ) );
}

// Make sure the data is valid before caching it.
$data = $this->validate_data( $data );
// Make sure the data is valid before storing it in a transient.
$data = $this->sanitize_and_validate_data( $data );
if ( is_wp_error( $data ) ) {
return $data;
}
Expand All @@ -193,14 +184,17 @@ private function load_from_url( $url ) {
}

/**
* Validates the font collection configuration.
* Sanitizes and validates the font collection data.
*
* @since 6.5.0
*
* @param array $data Font collection configuration to validate.
* @return array|WP_Error Array of data if valid, otherwise a WP_Error instance.
* @param array $data Font collection data to sanitize and validate.
* @return array|WP_Error Sanitized data if valid, otherwise a WP_Error instance.
*/
private function validate_data( $data ) {
private function sanitize_and_validate_data( $data ) {
$schema = self::get_sanitization_schema();
$data = WP_Font_Utils::sanitize_from_schema( $data, $schema );

$required_properties = array( 'name', 'font_families' );
foreach ( $required_properties as $property ) {
if ( empty( $data[ $property ] ) ) {
Expand All @@ -217,5 +211,59 @@ private function validate_data( $data ) {

return $data;
}

/**
* Retrieves the font collection sanitization schema.
*
* @since 6.5.0
*
* @return array Font collection sanitization schema.
*/
private static function get_sanitization_schema() {
return array(
'name' => 'sanitize_text_field',
'description' => 'sanitize_text_field',
'font_families' => array(
array(
'font_family_settings' => array(
'name' => 'sanitize_text_field',
'slug' => 'sanitize_title',
'fontFamily' => 'sanitize_text_field',
'preview' => 'sanitize_url',
'fontFace' => array(
array(
'fontFamily' => 'sanitize_text_field',
'fontStyle' => 'sanitize_text_field',
'fontWeight' => 'sanitize_text_field',
'src' => function ( $value ) {
return is_array( $value )
? array_map( 'sanitize_text_field', $value )
: sanitize_text_field( $value );
},
'preview' => 'sanitize_url',
'fontDisplay' => 'sanitize_text_field',
'fontStretch' => 'sanitize_text_field',
'ascentOverride' => 'sanitize_text_field',
'descentOverride' => 'sanitize_text_field',
'fontVariant' => 'sanitize_text_field',
'fontFeatureSettings' => 'sanitize_text_field',
'fontVariationSettings' => 'sanitize_text_field',
'lineGapOverride' => 'sanitize_text_field',
'sizeAdjust' => 'sanitize_text_field',
'unicodeRange' => 'sanitize_text_field',
),
),
),
'categories' => array( 'sanitize_title' ),
),
),
'categories' => array(
array(
'name' => 'sanitize_text_field',
'slug' => 'sanitize_title',
),
),
);
}
}
}
3 changes: 2 additions & 1 deletion lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php
Expand Up @@ -158,7 +158,7 @@ public static function sanitize_from_schema( $tree, $schema ) {
}

$is_value_array = is_array( $value );
$is_schema_array = is_array( $schema[ $key ] );
$is_schema_array = is_array( $schema[ $key ] ) && ! is_callable( $schema[ $key ] );

if ( $is_value_array && $is_schema_array ) {
if ( wp_is_numeric_array( $value ) ) {
Expand Down Expand Up @@ -193,6 +193,7 @@ public static function sanitize_from_schema( $tree, $schema ) {
* Apply the sanitizer to the value.
*
* @since 6.5.0
*
* @param mixed $value The value to sanitize.
* @param mixed $sanitizer The sanitizer to apply.
*
Expand Down
Expand Up @@ -14,8 +14,12 @@ class Tests_Fonts_WpFontCollection_Construct extends WP_UnitTestCase {

public function test_should_do_it_wrong_with_invalid_slug() {
$this->setExpectedIncorrectUsage( 'WP_Font_Collection::__construct' );
$mock_collection_data = array(
'name' => 'Test Collection',
'font_families' => array( 'mock ' ),
);

$collection = new WP_Font_Collection( 'slug with spaces', array() );
$collection = new WP_Font_Collection( 'slug with spaces', $mock_collection_data );

$this->assertSame( 'slug-with-spaces', $collection->slug, 'Slug is not sanitized.' );
}
Expand Down
95 changes: 88 additions & 7 deletions phpunit/tests/fonts/font-library/wpFontCollection/getData.php
Expand Up @@ -79,13 +79,13 @@ public function data_create_font_collection() {
'slug' => 'my-collection',
'config' => array(
'name' => 'My Collection',
'font_families' => array( 'mock' ),
'font_families' => array( array() ),
),
'expected_data' => array(
'description' => '',
'categories' => array(),
'name' => 'My Collection',
'font_families' => array( 'mock' ),
'font_families' => array( array() ),
),
),

Expand All @@ -94,14 +94,95 @@ public function data_create_font_collection() {
'config' => array(
'name' => 'My Collection',
'description' => 'My collection description',
'font_families' => array( 'mock' ),
'categories' => array( 'mock' ),
'font_families' => array( array() ),
'categories' => array(),
),
'expected_data' => array(
'description' => 'My collection description',
'categories' => array( 'mock' ),
'categories' => array(),
'name' => 'My Collection',
'font_families' => array( 'mock' ),
'font_families' => array( array() ),
),
),

'font collection with risky data' => array(
'slug' => 'my-collection',
'config' => array(
'name' => 'My Collection<script>alert("xss")</script>',
'description' => 'My collection description<script>alert("xss")</script>',
'font_families' => array(
array(
'font_family_settings' => array(
'fontFamily' => 'Open Sans, sans-serif<script>alert("xss")</script>',
'slug' => 'open-sans',
'name' => 'Open Sans<script>alert("xss")</script>',
'fontFace' => array(
array(
'fontFamily' => 'Open Sans',
'fontStyle' => 'normal',
'fontWeight' => '400',
'src' => 'https://example.com/src-as-string.ttf?a=<script>alert("xss")</script>',
),
array(
'fontFamily' => 'Open Sans',
'fontStyle' => 'normal',
'fontWeight' => '400',
'src' => array(
'https://example.com/src-as-array.woff2?a=<script>alert("xss")</script>',
'https://example.com/src-as-array.ttf',
),
),
),
'unwanted_property' => 'potentially evil value',
),
'categories' => array( 'sans-serif<script>alert("xss")</script>' ),
),
),
'categories' => array(
array(
'name' => 'Mock col<script>alert("xss")</script>',
'slug' => 'mock-col<script>alert("xss")</script>',
'unwanted_property' => 'potentially evil value',
),
),
'unwanted_property' => 'potentially evil value',
),
'expected_data' => array(
'description' => 'My collection description',
'categories' => array(
array(
'name' => 'Mock col',
'slug' => 'mock-colalertxss',
),
),
'name' => 'My Collection',
'font_families' => array(
array(
'font_family_settings' => array(
'fontFamily' => 'Open Sans, sans-serif',
'slug' => 'open-sans',
'name' => 'Open Sans',
'fontFace' => array(
array(
'fontFamily' => 'Open Sans',
'fontStyle' => 'normal',
'fontWeight' => '400',
'src' => 'https://example.com/src-as-string.ttf?a=',
),
array(
'fontFamily' => 'Open Sans',
'fontStyle' => 'normal',
'fontWeight' => '400',
'src' => array(
'https://example.com/src-as-array.woff2?a=',
'https://example.com/src-as-array.ttf',
),
),
),
),
'categories' => array( 'sans-serifalertxss' ),
),
),
),
),

Expand All @@ -114,7 +195,7 @@ public function data_create_font_collection() {
* @param array $config Font collection config.
*/
public function test_should_error_when_missing_properties( $config ) {
$this->setExpectedIncorrectUsage( 'WP_Font_Collection::validate_data' );
$this->setExpectedIncorrectUsage( 'WP_Font_Collection::sanitize_and_validate_data' );

$collection = new WP_Font_Collection( 'my-collection', $config );
$data = $collection->get_data();
Expand Down
Expand Up @@ -13,7 +13,12 @@
class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTestCase {

public function test_should_get_font_collection() {
wp_register_font_collection( 'my-font-collection', array( 'font_families' => array( 'mock' ) ) );
$mock_collection_data = array(
'name' => 'Test Collection',
'font_families' => array( 'mock' ),
);

wp_register_font_collection( 'my-font-collection', $mock_collection_data );
$font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' );
$this->assertInstanceOf( 'WP_Font_Collection', $font_collection );
}
Expand Down
Expand Up @@ -13,6 +13,7 @@
class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_UnitTestCase {
public function test_should_register_font_collection() {
$config = array(
'name' => 'My Collection',
'font_families' => array( 'mock' ),
);

Expand All @@ -21,14 +22,19 @@ public function test_should_register_font_collection() {
}

public function test_should_return_error_if_slug_is_repeated() {
$mock_collection_data = array(
'name' => 'Test Collection',
'font_families' => array( 'mock' ),
);

// Register first collection.
$collection1 = WP_Font_Library::register_font_collection( 'my-collection-1', array( 'font_families' => array( 'mock' ) ) );
$collection1 = WP_Font_Library::register_font_collection( 'my-collection-1', $mock_collection_data );
$this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' );

// Expects a _doing_it_wrong notice.
$this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' );

// Try to register a second collection with same slug.
WP_Font_Library::register_font_collection( 'my-collection-1', array( 'font_families' => array( 'mock' ) ) );
WP_Font_Library::register_font_collection( 'my-collection-1', $mock_collection_data );
}
}
Expand Up @@ -13,9 +13,14 @@
class Tests_Fonts_WpFontLibrary_UnregisterFontCollection extends WP_Font_Library_UnitTestCase {

public function test_should_unregister_font_collection() {
$mock_collection_data = array(
'name' => 'Test Collection',
'font_families' => array( 'mock' ),
);

// Registers two mock font collections.
WP_Font_Library::register_font_collection( 'mock-font-collection-1', array( 'font_families' => array( 'mock' ) ) );
WP_Font_Library::register_font_collection( 'mock-font-collection-2', array( 'font_families' => array( 'mock' ) ) );
WP_Font_Library::register_font_collection( 'mock-font-collection-1', $mock_collection_data );
WP_Font_Library::register_font_collection( 'mock-font-collection-2', $mock_collection_data );

// Unregister mock font collection.
WP_Font_Library::unregister_font_collection( 'mock-font-collection-1' );
Expand All @@ -36,6 +41,6 @@ public function unregister_non_existing_collection() {
// Unregisters non-existing font collection.
WP_Font_Library::unregister_font_collection( 'non-existing-collection' );
$collections = WP_Font_Library::get_font_collections();
$this->assertEmpty( $collections, 'Should not be registered collections.' );
$this->assertEmpty( $collections, 'No collections should be registered.' );
}
}

0 comments on commit b942f10

Please sign in to comment.