From 0b6246fc76afd68ca50b55e38bcd45bb62f2a931 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 11 Oct 2026 21:35:02 +1100 Subject: [PATCH 1/4] Editor: Ignore theme.json values of unexpected types. Prevents `WP_Theme_JSON` from throwing fatal `TypeError`s when theme.json or global styles data contains values of the wrong type. Sanitization keeps schema leaves such as `css` and `spacingScale` whatever their type, so, for example, an array `css` value reached `explode()` in `process_blocks_custom_css()`. Values of unexpected types are now ignored: non-string `css` and `ref` values, a non-array `spacingScale` or non-string `spacingScale.unit`, shared block style variations that are not arrays, and `blockTypes` items that are not strings. Backports https://github.com/WordPress/gutenberg/pull/84523. See #66286. --- src/wp-includes/class-wp-theme-json.php | 14 +++-- tests/phpunit/tests/theme/wpThemeJson.php | 73 +++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index c17f2b1b07de2..47a7a3ff9571f 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1140,7 +1140,7 @@ public function __construct( $theme_json = array( 'version' => self::LATEST_SCHE // In addition to presets, spacingScale (which generates presets) is also keyed by origin. $scale_path = array( 'settings', 'spacing', 'spacingScale' ); $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null ); - if ( null !== $spacing_scale ) { + if ( is_array( $spacing_scale ) ) { // If the spacingScale is not already keyed by origin. if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) { _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) ); @@ -1214,7 +1214,10 @@ private static function unwrap_shared_block_style_variations( $theme_json, $vali foreach ( $registered_variations as $variation_name ) { $block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array(); $top_level_data = $variations[ $variation_name ] ?? array(); - $merged_data = array_replace_recursive( $top_level_data, $block_level_data ); + if ( ! is_array( $block_level_data ) || ! is_array( $top_level_data ) ) { + continue; + } + $merged_data = array_replace_recursive( $top_level_data, $block_level_data ); if ( ! empty( $merged_data ) ) { _wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data ); } @@ -1493,7 +1496,7 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n if ( ! empty( $input['blockTypes'] ) && is_array( $input['blockTypes'] ) ) { $variation_pseudo_selectors = array(); foreach ( $input['blockTypes'] as $variation_block_type ) { - if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ] ) ) { + if ( is_string( $variation_block_type ) && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ] ) ) { $variation_pseudo_selectors = array_merge( $variation_pseudo_selectors, static::VALID_BLOCK_PSEUDO_SELECTORS[ $variation_block_type ] @@ -2121,7 +2124,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' public static function process_blocks_custom_css( $css, $selector ) { $processed_css = ''; - if ( empty( $css ) ) { + if ( empty( $css ) || ! is_string( $css ) ) { return $processed_css; } @@ -3241,7 +3244,7 @@ protected static function get_property_value( $styles, $path, $theme_json = null * In the case of backgroundImage, if both a ref and a URL are present in the value, * the URL takes precedence and the ref is ignored. */ - if ( is_array( $value ) && isset( $value['ref'] ) ) { + if ( is_array( $value ) && isset( $value['ref'] ) && is_string( $value['ref'] ) ) { $value_path = explode( '.', $value['ref'] ); $ref_value = _wp_array_get( $theme_json, $value_path ); // Background Image refs can refer to a string or an array containing a URL string. @@ -5569,6 +5572,7 @@ private static function compute_spacing_sizes( $spacing_scale ) { ! isset( $spacing_scale['mediumStep'] ) || ! is_numeric( $spacing_scale['mediumStep'] ) || ! isset( $spacing_scale['unit'] ) || + ! is_string( $spacing_scale['unit'] ) || ! isset( $spacing_scale['operator'] ) || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) || ! isset( $spacing_scale['increment'] ) || diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 1fbdabb84d7e8..79f056b60ee3a 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -5896,6 +5896,64 @@ public static function data_sanitize_with_invalid_style_variation() { ); } + /** + * @ticket 66286 + * + * @dataProvider data_get_stylesheet_ignores_values_of_unexpected_types + * + * @param array $theme_json_data The theme.json data, without the version. + */ + public function test_get_stylesheet_ignores_values_of_unexpected_types( $theme_json_data ) { + $theme_json = new WP_Theme_JSON( + array_merge( array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ), $theme_json_data ) + ); + + $this->assertIsString( $theme_json->get_stylesheet() ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_get_stylesheet_ignores_values_of_unexpected_types() { + return 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( + array( 'styles' => array( 'blocks' => array( 'core/paragraph' => array( 'css' => array( 'color: red;' ) ) ) ) ), + ), + 'ref as an array' => array( + array( 'styles' => array( 'color' => array( 'text' => array( 'ref' => array( 'styles', 'color' ) ) ) ) ), + ), + 'spacing scale as a string' => array( + array( 'settings' => array( 'spacing' => array( 'spacingScale' => '1.5' ) ) ), + ), + 'spacing scale unit as an array' => array( + array( + 'settings' => array( + 'spacing' => array( + 'spacingScale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 7, + 'mediumStep' => 1.5, + 'unit' => array( 'rem' ), + ), + ), + ), + ), + ), + 'shared style variation as a string' => array( + array( 'styles' => array( 'variations' => array( 'plain' => 'red' ) ) ), + ), + 'block types item as an array' => array( + array( 'blockTypes' => array( array( 'core/button' ) ) ), + ), + ); + } + /** * @ticket 57583 * @ticket 61165 @@ -7411,6 +7469,7 @@ public static function data_custom_css_for_user_caps() { /** * @ticket 61165 * @ticket 61769 + * @ticket 66286 * * @dataProvider data_process_blocks_custom_css * @@ -7447,6 +7506,20 @@ public static function data_process_blocks_custom_css() { ), 'expected' => '', ), + 'css as an object' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => array( 'color' => 'red' ), + ), + 'expected' => '', + ), + 'css as a list of strings' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => array( 'color: red;' ), + ), + 'expected' => '', + ), 'no nested selectors' => array( 'input' => array( 'selector' => '.foo', From e632387a3c2e08c705c3739a085f38afd3f773ca Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 12 Oct 2026 13:31:51 +1100 Subject: [PATCH 2/4] Editor: Only output root custom CSS when it is a string. Backports https://github.com/WordPress/gutenberg/commit/ba8f807c5f from https://github.com/WordPress/gutenberg/pull/84523. See #66286. --- src/wp-includes/class-wp-theme-json.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 47a7a3ff9571f..688b6f1efe238 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2104,7 +2104,10 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' // Load the custom CSS last so it has the highest specificity. if ( in_array( 'custom-css', $types, true ) ) { // Add the global styles root CSS. - $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); + $root_css = $this->theme_json['styles']['css'] ?? ''; + if ( is_string( $root_css ) ) { + $stylesheet .= $root_css; + } } return $stylesheet; From 531540945ec6b116537055982ccc91ea0c6c1099 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 12 Oct 2026 14:12:46 +1100 Subject: [PATCH 3/4] Editor: Ignore per-origin spacing scales that are not arrays when merging. `WP_Theme_JSON::merge()` passed per-origin `spacingScale` values straight to `array_replace()`, so a scale such as `{ "theme": "1.5" }` caused a fatal `TypeError`. Base and incoming per-origin spacing scales that are not arrays are now skipped. Backports https://github.com/WordPress/gutenberg/pull/84529. See #66286. --- src/wp-includes/class-wp-theme-json.php | 8 ++-- tests/phpunit/tests/theme/wpThemeJson.php | 55 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 688b6f1efe238..2f80a256b8936 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -4437,11 +4437,13 @@ public function merge( $incoming ) { $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin ); // Apply the base spacing scale to the current layer. - $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() ); - $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale ); + $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() ); + if ( is_array( $base_spacing_scale ) ) { + $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale ); + } $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null ); - if ( ! isset( $spacing_scale ) ) { + if ( ! is_array( $spacing_scale ) ) { continue; } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 79f056b60ee3a..e90a045cc02f9 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -5954,6 +5954,61 @@ public static function data_get_stylesheet_ignores_values_of_unexpected_types() ); } + /** + * @ticket 66286 + */ + public function test_merge_does_not_generate_spacing_sizes_from_a_non_array_per_origin_spacing_scale() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'spacing' => array( + 'spacingScale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 3, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + ), + ), + ), + 'default' + ); + $theme_json->merge( + new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( 'spacing' => array( 'spacingScale' => array( 'theme' => '1.5' ) ) ), + ) + ) + ); + $spacing_sizes = $theme_json->get_settings()['spacing']['spacingSizes']; + + $this->assertSame( array(), $spacing_sizes['theme'], 'The theme layer with a non-array spacing scale should generate no sizes.' ); + $this->assertSame( + array( + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '1rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '2.25rem', + ), + ), + $spacing_sizes['default'], + 'The default layer sizes should be unaffected.' + ); + } + /** * @ticket 57583 * @ticket 61165 From f73b2daf1a6ce9ae2fa857fc5afa2b9397445f60 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 12 Oct 2026 17:33:42 +1100 Subject: [PATCH 4/4] Editor: Test that non-string root custom CSS is ignored. Adds a root `styles.css` case to the unexpected types data provider and requests the `custom-css` type from `get_stylesheet()`, which the root custom CSS check only runs for. See #66286. --- tests/phpunit/tests/theme/wpThemeJson.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index e90a045cc02f9..72087f8bc8c18 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -5908,7 +5908,7 @@ public function test_get_stylesheet_ignores_values_of_unexpected_types( $theme_j array_merge( array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ), $theme_json_data ) ); - $this->assertIsString( $theme_json->get_stylesheet() ); + $this->assertIsString( $theme_json->get_stylesheet( array( 'variables', 'styles', 'presets', 'custom-css' ) ) ); } /** @@ -5918,6 +5918,9 @@ 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( + array( 'styles' => array( 'css' => array( 'color' => 'red' ) ) ), + ), 'element custom css as an object' => array( array( 'styles' => array( 'elements' => array( 'link' => array( 'css' => array( 'color' => 'red' ) ) ) ) ), ),