diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 646c7abc59eafe..9311001f2edd14 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -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. * @@ -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. * @@ -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 ) { @@ -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 ] ); @@ -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. * diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 07bec961553c9a..89900d45893d91 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -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 *