diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index e48bdfa1541dd..c1bc6209f306d 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -395,6 +395,7 @@ class WP_Theme_JSON { * @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios', * 'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'. * @since 6.9.0 Added support for `border.radiusSizes`. + * @since 7.0.0 Added type markers to the schema for boolean values. * @var array */ const VALID_SETTINGS = array( @@ -442,8 +443,8 @@ class WP_Theme_JSON { 'allowCustomContentAndWideSize' => null, ), 'lightbox' => array( - 'enabled' => null, - 'allowEditing' => null, + 'enabled' => true, + 'allowEditing' => true, ), 'position' => array( 'fixed' => null, @@ -1246,6 +1247,7 @@ protected static function get_blocks_metadata() { * It is recursive and modifies the input in-place. * * @since 5.8.0 + * @since 7.0.0 Added type validation for boolean values. * * @param array $tree Input to process. * @param array $schema Schema to adhere to. @@ -1263,6 +1265,17 @@ protected static function remove_keys_not_in_schema( $tree, $schema ) { continue; } + // Validate type if schema specifies a boolean marker. + if ( is_bool( $schema[ $key ] ) ) { + // Schema expects a boolean value - validate the input matches. + if ( ! is_bool( $value ) ) { + unset( $tree[ $key ] ); + continue; + } + // Type matches, keep the value and continue to next key. + continue; + } + if ( is_array( $schema[ $key ] ) ) { if ( ! is_array( $value ) ) { unset( $tree[ $key ] ); @@ -3673,6 +3686,35 @@ protected static function remove_insecure_inner_block_styles( $blocks ) { return $sanitized; } + /** + * Preserves valid typed settings from input to output based on type markers in schema. + * + * Recursively iterates through the schema and validates/preserves settings + * that have type markers (e.g., boolean) in VALID_SETTINGS. + * + * @since 7.0.0 + * + * @param array $input Input settings to process. + * @param array $output Output settings array (passed by reference). + * @param array $schema Schema to validate against (typically VALID_SETTINGS). + * @param array $path Current path in the schema (for recursive calls). + */ + private static function preserve_valid_typed_settings( $input, &$output, $schema, $path = array() ) { + foreach ( $schema as $key => $schema_value ) { + $current_path = array_merge( $path, array( $key ) ); + + // Validate boolean type markers. + if ( is_bool( $schema_value ) ) { + $value = _wp_array_get( $input, $current_path, null ); + if ( is_bool( $value ) ) { + _wp_array_set( $output, $current_path, $value ); // Preserve boolean value. + } + } elseif ( is_array( $schema_value ) ) { + self::preserve_valid_typed_settings( $input, $output, $schema_value, $current_path ); // Recurse into nested structure. + } + } + } + /** * Processes a setting node and returns the same node * without the insecure settings. @@ -3732,6 +3774,9 @@ protected static function remove_insecure_settings( $input ) { // Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed. static::remove_indirect_properties( $input, $output ); + // Preserve all valid settings that have type markers in VALID_SETTINGS. + self::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS ); + return $output; } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 2bf0e7d84f266..aeb89f323e77a 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -5999,8 +5999,8 @@ public function test_internal_syntax_is_converted_to_css_variables() { * @ticket 58588 * @ticket 60613 * - * @covers WP_Theme_JSON_Gutenberg::resolve_variables - * @covers WP_Theme_JSON_Gutenberg::convert_variables_to_value + * @covers WP_Theme_JSON::resolve_variables + * @covers WP_Theme_JSON::convert_variables_to_value */ public function test_resolve_variables() { $primary_color = '#9DFF20'; @@ -6623,4 +6623,132 @@ public function test_merge_incoming_data_unique_slugs_always_preserved() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_preserves_boolean_values_when_schema_expects_boolean() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'lightbox' => array( + 'enabled' => true, + 'allowEditing' => false, + ), + ), + ) + ); + + $settings = $theme_json->get_settings(); + $this->assertTrue( $settings['lightbox']['enabled'], 'Enabled should be true' ); + $this->assertFalse( $settings['lightbox']['allowEditing'], 'Allow editing should be false' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_removes_non_boolean_values_when_schema_expects_boolean() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'lightbox' => array( + 'enabled' => 'not-a-boolean', + 'allowEditing' => 123, + ), + ), + ) + ); + + $settings = $theme_json->get_settings(); + $this->assertArrayNotHasKey( 'enabled', $settings['lightbox'] ?? array(), 'Enabled should be removed' ); + $this->assertArrayNotHasKey( 'allowEditing', $settings['lightbox'] ?? array(), 'Allow editing should be removed' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_preserves_boolean_values_in_block_settings() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/image' => array( + 'lightbox' => array( + 'enabled' => true, + 'allowEditing' => false, + ), + ), + ), + ), + ) + ); + + $settings = $theme_json->get_settings(); + $this->assertTrue( $settings['blocks']['core/image']['lightbox']['enabled'], 'Enabled should be true' ); + $this->assertFalse( $settings['blocks']['core/image']['lightbox']['allowEditing'], 'Allow editing should be false' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_removes_non_boolean_values_in_block_settings() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/image' => array( + 'lightbox' => array( + 'enabled' => 'string-value', + 'allowEditing' => array( 'not', 'a', 'boolean' ), + ), + ), + ), + ), + ) + ); + + $settings = $theme_json->get_settings(); + $lightbox = $settings['blocks']['core/image']['lightbox'] ?? array(); + $this->assertArrayNotHasKey( 'enabled', $lightbox, 'Enabled should be removed' ); + $this->assertArrayNotHasKey( 'allowEditing', $lightbox, 'Allow editing should be removed' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_preserves_null_schema_behavior() { + // Test that settings with null in schema (no type validation) still accept any type. + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'appearanceTools' => 'string-value', // null in schema, should accept any type. + 'custom' => array( 'nested' => 'value' ), // null in schema, should accept any type. + ), + ) + ); + + $settings = $theme_json->get_settings(); + $this->assertSame( 'string-value', $settings['appearanceTools'], 'Appearance tools should be string value' ); + $this->assertSame( array( 'nested' => 'value' ), $settings['custom'], 'Custom should be array value' ); + } }