Skip to content

Commit

Permalink
WP_Theme_JSON_Gutenberg: Add nested indexed array schema sanitization (
Browse files Browse the repository at this point in the history
…#56447)

* Add GB specific resolver

* changing unset function

* adding nested validation

* rename function

* Revert "Add GB specific resolver"

This reverts commit 27c0f6f.

* removing not needed check

* removing not needed check

* Remove keys from tree that are not arrays when they are defined as arrays in schema

* remove key if it is empty

* adding tests

* adding function docs

* php format

* add comment to function

---------

Co-authored-by: hellofromtonya <[email protected]>
  • Loading branch information
2 people authored and derekblank committed Dec 7, 2023
1 parent b5a07af commit 1bfd62b
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 12 deletions.
131 changes: 119 additions & 12 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,31 @@ class WP_Theme_JSON_Gutenberg {
),
);

const FONT_FAMILY_SCHEMA = array(
array(
'fontFamily' => null,
'name' => null,
'slug' => null,
'fontFace' => array(
array(
'ascentOverride' => null,
'descentOverride' => null,
'fontDisplay' => null,
'fontFamily' => null,
'fontFeatureSettings' => null,
'fontStyle' => null,
'fontStretch' => null,
'fontVariationSettings' => null,
'fontWeight' => null,
'lineGapOverride' => null,
'sizeAdjust' => null,
'src' => null,
'unicodeRange' => null,
),
),
),
);

/**
* The valid properties under the styles key.
*
Expand Down Expand Up @@ -550,6 +575,52 @@ class WP_Theme_JSON_Gutenberg {
'typography' => 'typography',
);

/**
* Return the input schema at the root and per origin.
*
* @since 6.5.0
*
* @param array $schema The base schema.
* @return array The schema at the root and per origin.
*
* Example:
* schema_in_root_and_per_origin(
* array(
* 'fontFamily' => null,
* 'slug' => null,
* )
* )
*
* Returns:
* array(
* 'fontFamily' => null,
* 'slug' => null,
* 'default' => array(
* 'fontFamily' => null,
* 'slug' => null,
* ),
* 'blocks' => array(
* 'fontFamily' => null,
* 'slug' => null,
* ),
* 'theme' => array(
* 'fontFamily' => null,
* 'slug' => null,
* ),
* 'custom' => array(
* 'fontFamily' => null,
* 'slug' => null,
* ),
* )
*/
protected static function schema_in_root_and_per_origin( $schema ) {
$schema_in_root_and_per_origin = $schema;
foreach ( static::VALID_ORIGINS as $origin ) {
$schema_in_root_and_per_origin[ $origin ] = $schema;
}
return $schema_in_root_and_per_origin;
}

/**
* Returns a class name by an element name.
*
Expand Down Expand Up @@ -791,11 +862,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n
$schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
}

$schema['styles'] = static::VALID_STYLES;
$schema['styles']['blocks'] = $schema_styles_blocks;
$schema['styles']['elements'] = $schema_styles_elements;
$schema['settings'] = static::VALID_SETTINGS;
$schema['settings']['blocks'] = $schema_settings_blocks;
$schema['styles'] = static::VALID_STYLES;
$schema['styles']['blocks'] = $schema_styles_blocks;
$schema['styles']['elements'] = $schema_styles_elements;
$schema['settings'] = static::VALID_SETTINGS;
$schema['settings']['blocks'] = $schema_settings_blocks;
$schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );

// Remove anything that's not present in the schema.
foreach ( array( 'styles', 'settings' ) as $subtree ) {
Expand Down Expand Up @@ -967,18 +1039,39 @@ protected static function get_blocks_metadata() {
* @return array The modified $tree.
*/
protected static function remove_keys_not_in_schema( $tree, $schema ) {
$tree = array_intersect_key( $tree, $schema );
if ( ! is_array( $tree ) ) {
return $tree;
}

foreach ( $schema as $key => $data ) {
if ( ! isset( $tree[ $key ] ) ) {
foreach ( $tree as $key => $value ) {
// Remove keys not in the schema or with null/empty values.
if ( ! array_key_exists( $key, $schema ) ) {
unset( $tree[ $key ] );
continue;
}

if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
$tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
// Check if the value is an array and requires further processing.
if ( is_array( $value ) && is_array( $schema[ $key ] ) ) {
// Determine if it is an associative or indexed array.
$schema_is_assoc = self::is_assoc( $value );

if ( $schema_is_assoc ) {
// If associative, process as a single object.
$tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );

if ( empty( $tree[ $key ] ) ) {
unset( $tree[ $key ] );
if ( empty( $tree[ $key ] ) ) {
unset( $tree[ $key ] );
}
} else {
// If indexed, process each item in the array.
foreach ( $value as $item_key => $item_value ) {
if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
$tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
} else {
// If the schema does not define a further structure, keep the value as is.
$tree[ $key ][ $item_key ] = $item_value;
}
}
}
} elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
unset( $tree[ $key ] );
Expand All @@ -988,6 +1081,20 @@ protected static function remove_keys_not_in_schema( $tree, $schema ) {
return $tree;
}

/**
* Checks if the given array is associative.
*
* @since 6.5.0
* @param array $data The array to check.
* @return bool True if the array is associative, false otherwise.
*/
protected static function is_assoc( $data ) {
if ( array() === $data ) {
return false;
}
return array_keys( $data ) !== range( 0, count( $data ) - 1 );
}

/**
* Returns the existing settings for each block.
*
Expand Down
130 changes: 130 additions & 0 deletions phpunit/class-wp-theme-json-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,136 @@ public function data_sanitize_for_block_with_style_variations() {
);
}

public function test_sanitize_indexed_arrays() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => '2',
'badKey2' => 'I am Evil!!!!',
'settings' => array(
'badKey3' => 'I am Evil!!!!',
'typography' => array(
'badKey4' => 'I am Evil!!!!',
'fontFamilies' => array(
'custom' => array(
array(
'badKey4' => 'I am Evil!!!!',
'name' => 'Arial',
'slug' => 'arial',
'fontFamily' => 'Arial, sans-serif',
),
),
'theme' => array(
array(
'badKey5' => 'I am Evil!!!!',
'name' => 'Piazzolla',
'slug' => 'piazzolla',
'fontFamily' => 'Piazzolla',
'fontFace' => array(
array(
'badKey6' => 'I am Evil!!!!',
'fontFamily' => 'Piazzolla',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
array(
'badKey7' => 'I am Evil!!!!',
'fontFamily' => 'Piazzolla',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
),
),
array(
'badKey8' => 'I am Evil!!!!',
'name' => 'Inter',
'slug' => 'Inter',
'fontFamily' => 'Inter',
'fontFace' => array(
array(
'badKey9' => 'I am Evil!!!!',
'fontFamily' => 'Inter',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
array(
'badKey10' => 'I am Evil!!!!',
'fontFamily' => 'Inter',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
),
),
),
),
),
),
)
);

$expected_sanitized = array(
'version' => '2',
'settings' => array(
'typography' => array(
'fontFamilies' => array(
'custom' => array(
array(
'name' => 'Arial',
'slug' => 'arial',
'fontFamily' => 'Arial, sans-serif',
),
),
'theme' => array(
array(
'name' => 'Piazzolla',
'slug' => 'piazzolla',
'fontFamily' => 'Piazzolla',
'fontFace' => array(
array(
'fontFamily' => 'Piazzolla',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
array(
'fontFamily' => 'Piazzolla',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
),
),
array(
'name' => 'Inter',
'slug' => 'Inter',
'fontFamily' => 'Inter',
'fontFace' => array(
array(
'fontFamily' => 'Inter',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
array(
'fontFamily' => 'Inter',
'fontStyle' => 'italic',
'fontWeight' => '400',
'src' => 'https://example.com/font.ttf',
),
),
),
),
),
),
),
);
$sanitized_theme_json = $theme_json->get_raw_data();
$this->assertSameSetsWithIndex( $expected_sanitized, $sanitized_theme_json, 'Sanitized theme.json does not match' );
}

/**
* @dataProvider data_sanitize_with_invalid_style_variation
*
Expand Down

0 comments on commit 1bfd62b

Please sign in to comment.