From 556b753ea79d919d088257412011482cba840c31 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 14 Oct 2026 14:31:06 +1100 Subject: [PATCH] Editor: Remove theme.json preset items of unexpected types. Preset leaves are untyped in `VALID_SETTINGS`, so a preset item that isn't an array, or whose `slug` isn't a scalar, reached code that reads `$preset['slug']` as a string and threw a fatal `TypeError`. `WP_Theme_JSON::sanitize()` now removes those items from plain preset lists and from lists keyed by origin, at the top level and per block. Filtering in `sanitize()` also covers `remove_insecure_properties()`, which the kses filter uses when global styles are saved. Scalar slugs are kept, so an integer `slug` still works. Backports https://github.com/WordPress/gutenberg/pull/84534. See #66304. --- src/wp-includes/class-wp-theme-json.php | 26 +++++ tests/phpunit/tests/theme/wpThemeJson.php | 114 ++++++++++++++++++++-- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index f5cdc1b43c6a1..116a8ad951983 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1536,6 +1536,32 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $result['viewport'] = self::sanitize_viewport_settings( $input[ $subtree ]['viewport'] ); } + if ( 'settings' === $subtree ) { + // Preset leaves are untyped in VALID_SETTINGS, but consumers read `$preset['slug']` as a string. + $is_valid_preset = static function ( $preset ) { + return is_array( $preset ) && ( ! isset( $preset['slug'] ) || is_scalar( $preset['slug'] ) ); + }; + foreach ( static::get_setting_nodes( array( 'settings' => $result ) ) as $node ) { + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $path = array_merge( array_slice( $node['path'], 1 ), $preset_metadata['path'] ); + $presets = _wp_array_get( $result, $path, null ); + if ( ! is_array( $presets ) ) { + continue; + } + if ( isset( $presets[0] ) ) { + $presets = array_values( array_filter( $presets, $is_valid_preset ) ); + } else { + foreach ( static::VALID_ORIGINS as $origin ) { + if ( isset( $presets[ $origin ] ) && is_array( $presets[ $origin ] ) ) { + $presets[ $origin ] = array_values( array_filter( $presets[ $origin ], $is_valid_preset ) ); + } + } + } + _wp_array_set( $result, $path, $presets ); + } + } + } + if ( empty( $result ) ) { unset( $output[ $subtree ] ); } else { diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 0de5a5cfe88c2..0b75ece403145 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -5898,6 +5898,7 @@ public static function data_sanitize_with_invalid_style_variation() { /** * @ticket 66286 + * @ticket 66304 * * @dataProvider data_get_stylesheet_ignores_values_of_unexpected_types * @@ -5918,22 +5919,57 @@ public function test_get_stylesheet_ignores_values_of_unexpected_types( $theme_j */ public static function data_get_stylesheet_ignores_values_of_unexpected_types() { return array( - 'root custom css as an object' => array( + 'root custom css as an object' => array( array( 'styles' => array( 'css' => array( 'color' => 'red' ) ) ), ), - 'element custom css as an object' => array( + 'element custom css as an object' => array( array( 'styles' => array( 'elements' => array( 'link' => array( 'css' => array( 'color' => 'red' ) ) ) ) ), ), - 'block custom css as a list' => array( + 'block custom css as a list' => array( array( 'styles' => array( 'blocks' => array( 'core/paragraph' => array( 'css' => array( 'color: red;' ) ) ) ) ), ), - 'ref as an array' => array( + 'ref as an array' => array( array( 'styles' => array( 'color' => array( 'text' => array( 'ref' => array( 'styles', 'color' ) ) ) ) ), ), - 'spacing scale as a string' => array( + 'palette as a list of strings' => array( + array( 'settings' => array( 'color' => array( 'palette' => array( '#000', '#fff' ) ) ) ), + ), + 'palette keyed by origin with a string' => array( + array( 'settings' => array( 'color' => array( 'palette' => array( 'custom' => array( '#000' ) ) ) ) ), + ), + 'palette mixing a list and an origin' => array( + array( + 'settings' => array( + 'color' => array( + 'palette' => array( + 0 => '#000', + 'theme' => '#fff', + ), + ), + ), + ), + ), + 'preset slug as an array' => array( + array( + 'settings' => array( + 'typography' => array( + 'fontSizes' => array( + array( + 'slug' => array( 'small' ), + 'size' => '1rem', + ), + ), + ), + ), + ), + ), + 'font families as a list of strings' => array( + array( 'settings' => array( 'typography' => array( 'fontFamilies' => array( 'Inter' ) ) ) ), + ), + 'spacing scale as a string' => array( array( 'settings' => array( 'spacing' => array( 'spacingScale' => '1.5' ) ) ), ), - 'spacing scale unit as an array' => array( + 'spacing scale unit as an array' => array( array( 'settings' => array( 'spacing' => array( @@ -5948,15 +5984,77 @@ public static function data_get_stylesheet_ignores_values_of_unexpected_types() ), ), ), - 'shared style variation as a string' => array( + 'shared style variation as a string' => array( array( 'styles' => array( 'variations' => array( 'plain' => 'red' ) ) ), ), - 'block types item as an array' => array( + 'block types item as an array' => array( array( 'blockTypes' => array( array( 'core/button' ) ) ), ), ); } + /** + * @ticket 66304 + */ + public function test_sanitize_removes_preset_items_of_unexpected_types() { + $valid_color = array( + 'slug' => 'black', + 'color' => '#000', + 'name' => 'Black', + ); + $int_slug = array( + 'slug' => 50, + 'color' => '#888', + 'name' => 'Grey', + ); + $invalid = array( + '#fff', + array( + 'slug' => array( 'white' ), + 'color' => '#fff', + ), + ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'color' => array( 'palette' => array_merge( $invalid, array( $valid_color, $int_slug ) ) ), + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( 'palette' => array( 'custom' => array_merge( $invalid, array( $valid_color, $int_slug ) ) ) ), + ), + ), + ), + ) + ); + $settings = $theme_json->get_settings(); + + $this->assertSame( array( $valid_color, $int_slug ), $settings['color']['palette']['theme'], 'Top-level preset list' ); + $this->assertSame( array( $valid_color, $int_slug ), $settings['blocks']['core/paragraph']['color']['palette']['custom'], 'Block-level presets keyed by origin' ); + } + + /** + * @ticket 66304 + */ + public function test_remove_insecure_properties_removes_preset_items_of_unexpected_types() { + $valid_color = array( + 'slug' => 'black', + 'color' => '#000', + 'name' => 'Black', + ); + + $sanitized = WP_Theme_JSON::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( 'color' => array( 'palette' => array( 'custom' => array( '#fff', $valid_color ) ) ) ), + ), + 'custom' + ); + + $this->assertSame( array( $valid_color ), $sanitized['settings']['color']['palette']['custom'] ); + } + /** * @ticket 66286 */