Skip to content

Commit 62f13ff

Browse files
committed
Enhance WP_Theme_JSON to include type validation for boolean values in settings. Introduce a new method to preserve valid typed settings based on schema markers. Update unit tests to ensure correct behavior for boolean values and their validation against the schema.
1 parent 7bff6ae commit 62f13ff

File tree

2 files changed

+135
-88
lines changed

2 files changed

+135
-88
lines changed

src/wp-includes/class-wp-theme-json.php

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ class WP_Theme_JSON {
395395
* @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios',
396396
* 'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'.
397397
* @since 6.9.0 Added support for `border.radiusSizes`.
398+
* @since 7.0.0 Added type markers to the schema for boolean values.
398399
* @var array
399400
*/
400401
const VALID_SETTINGS = array(
@@ -442,8 +443,8 @@ class WP_Theme_JSON {
442443
'allowCustomContentAndWideSize' => null,
443444
),
444445
'lightbox' => array(
445-
'enabled' => null,
446-
'allowEditing' => null,
446+
'enabled' => true,
447+
'allowEditing' => true,
447448
),
448449
'position' => array(
449450
'fixed' => null,
@@ -482,28 +483,6 @@ class WP_Theme_JSON {
482483
),
483484
);
484485

485-
/**
486-
* Safe settings that should be preserved by ::remove_insecure_settings().
487-
*
488-
* These are non-preset, non-CSS settings that control behavior rather than styling.
489-
* Each entry defines the setting path and its expected type for validation.
490-
*
491-
* The constant is deliberately private to prevent external usage by plugins.
492-
* Like the class itself, it is intended for internal core usage.
493-
*
494-
* @since 7.0.0
495-
*/
496-
private const SAFE_SETTINGS = array(
497-
array(
498-
'path' => array( 'lightbox', 'allowEditing' ),
499-
'type' => 'boolean',
500-
),
501-
array(
502-
'path' => array( 'lightbox', 'enabled' ),
503-
'type' => 'boolean',
504-
),
505-
);
506-
507486
/**
508487
* The valid properties for fontFamilies under settings key.
509488
*
@@ -1268,6 +1247,7 @@ protected static function get_blocks_metadata() {
12681247
* It is recursive and modifies the input in-place.
12691248
*
12701249
* @since 5.8.0
1250+
* @since 7.0.0 Added type validation for boolean values.
12711251
*
12721252
* @param array $tree Input to process.
12731253
* @param array $schema Schema to adhere to.
@@ -1285,6 +1265,17 @@ protected static function remove_keys_not_in_schema( $tree, $schema ) {
12851265
continue;
12861266
}
12871267

1268+
// Validate type if schema specifies a boolean marker.
1269+
if ( is_bool( $schema[ $key ] ) ) {
1270+
// Schema expects a boolean value - validate the input matches.
1271+
if ( ! is_bool( $value ) ) {
1272+
unset( $tree[ $key ] );
1273+
continue;
1274+
}
1275+
// Type matches, keep the value and continue to next key.
1276+
continue;
1277+
}
1278+
12881279
if ( is_array( $schema[ $key ] ) ) {
12891280
if ( ! is_array( $value ) ) {
12901281
unset( $tree[ $key ] );
@@ -3695,6 +3686,35 @@ protected static function remove_insecure_inner_block_styles( $blocks ) {
36953686
return $sanitized;
36963687
}
36973688

3689+
/**
3690+
* Preserves valid typed settings from input to output based on type markers in schema.
3691+
*
3692+
* Recursively iterates through the schema and validates/preserves settings
3693+
* that have type markers (e.g., boolean) in VALID_SETTINGS.
3694+
*
3695+
* @since 7.0.0
3696+
*
3697+
* @param array $input Input settings to process.
3698+
* @param array $output Output settings array (passed by reference).
3699+
* @param array $schema Schema to validate against (typically VALID_SETTINGS).
3700+
* @param array $path Current path in the schema (for recursive calls).
3701+
*/
3702+
private static function preserve_valid_typed_settings( $input, &$output, $schema, $path = array() ) {
3703+
foreach ( $schema as $key => $schema_value ) {
3704+
$current_path = array_merge( $path, array( $key ) );
3705+
3706+
// Validate boolean type markers.
3707+
if ( is_bool( $schema_value ) ) {
3708+
$value = _wp_array_get( $input, $current_path, null );
3709+
if ( null !== $value && is_bool( $value ) ) {
3710+
_wp_array_set( $output, $current_path, $value ); // Preserve boolean value.
3711+
}
3712+
} elseif ( is_array( $schema_value ) ) {
3713+
static::preserve_valid_typed_settings( $input, $output, $schema_value, $current_path ); // Recurse into nested structure.
3714+
}
3715+
}
3716+
}
3717+
36983718
/**
36993719
* Processes a setting node and returns the same node
37003720
* without the insecure settings.
@@ -3754,32 +3774,8 @@ protected static function remove_insecure_settings( $input ) {
37543774
// Ensure indirect properties not included in any `PRESETS_METADATA` value are allowed.
37553775
static::remove_indirect_properties( $input, $output );
37563776

3757-
// Preserve all valid settings that aren't presets or indirect properties.
3758-
foreach ( static::SAFE_SETTINGS as $safe_setting ) {
3759-
$path = $safe_setting['path'];
3760-
$type = $safe_setting['type'];
3761-
3762-
// Extract the value from input using the path.
3763-
$value = _wp_array_get( $input, $path, null );
3764-
3765-
// Skip if the setting is not present in the input.
3766-
if ( null === $value ) {
3767-
continue;
3768-
}
3769-
3770-
// Validate the type.
3771-
$is_valid_type = false;
3772-
switch ( $type ) {
3773-
case 'boolean':
3774-
$is_valid_type = is_bool( $value );
3775-
break;
3776-
}
3777-
3778-
// If the type is valid, set it in the output using the path.
3779-
if ( $is_valid_type ) {
3780-
_wp_array_set( $output, $path, $value );
3781-
}
3782-
}
3777+
// Preserve all valid settings that have type markers in VALID_SETTINGS.
3778+
static::preserve_valid_typed_settings( $input, $output, static::VALID_SETTINGS );
37833779

37843780
return $output;
37853781
}

tests/phpunit/tests/theme/wpThemeJson.php

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5999,8 +5999,8 @@ public function test_internal_syntax_is_converted_to_css_variables() {
59995999
* @ticket 58588
60006000
* @ticket 60613
60016001
*
6002-
* @covers WP_Theme_JSON_Gutenberg::resolve_variables
6003-
* @covers WP_Theme_JSON_Gutenberg::convert_variables_to_value
6002+
* @covers WP_Theme_JSON::resolve_variables
6003+
* @covers WP_Theme_JSON::convert_variables_to_value
60046004
*/
60056005
public function test_resolve_variables() {
60066006
$primary_color = '#9DFF20';
@@ -6625,79 +6625,130 @@ public function test_merge_incoming_data_unique_slugs_always_preserved() {
66256625
}
66266626

66276627
/**
6628-
* @covers WP_Theme_JSON::remove_insecure_properties
6628+
* @covers WP_Theme_JSON::sanitize
6629+
* @covers WP_Theme_JSON::remove_keys_not_in_schema
66296630
*
66306631
* @ticket 64280
66316632
*/
6632-
public function test_remove_insecure_properties_should_allow_safe_settings() {
6633-
$actual = WP_Theme_JSON::remove_insecure_properties(
6633+
public function test_sanitize_preserves_boolean_values_when_schema_expects_boolean() {
6634+
$theme_json = new WP_Theme_JSON(
66346635
array(
66356636
'version' => WP_Theme_JSON::LATEST_SCHEMA,
66366637
'settings' => array(
6637-
'blocks' => array(
6638-
'core/image' => array(
6639-
'lightbox' => array(
6640-
'enabled' => false,
6641-
'allowEditing' => true,
6642-
),
6643-
'unsupported' => 'value',
6644-
),
6638+
'lightbox' => array(
6639+
'enabled' => true,
6640+
'allowEditing' => false,
66456641
),
66466642
),
66476643
)
66486644
);
66496645

6650-
$expected = array(
6651-
'version' => WP_Theme_JSON::LATEST_SCHEMA,
6652-
'settings' => array(
6653-
'blocks' => array(
6654-
'core/image' => array(
6655-
'lightbox' => array(
6656-
'enabled' => false,
6657-
'allowEditing' => true,
6658-
),
6646+
$settings = $theme_json->get_settings();
6647+
$this->assertTrue( $settings['lightbox']['enabled'] );
6648+
$this->assertFalse( $settings['lightbox']['allowEditing'] );
6649+
}
6650+
6651+
/**
6652+
* @covers WP_Theme_JSON::sanitize
6653+
* @covers WP_Theme_JSON::remove_keys_not_in_schema
6654+
*
6655+
* @ticket 64280
6656+
*/
6657+
public function test_sanitize_removes_non_boolean_values_when_schema_expects_boolean() {
6658+
$theme_json = new WP_Theme_JSON(
6659+
array(
6660+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
6661+
'settings' => array(
6662+
'lightbox' => array(
6663+
'enabled' => 'not-a-boolean',
6664+
'allowEditing' => 123,
66596665
),
66606666
),
6661-
),
6667+
)
66626668
);
66636669

6664-
$this->assertEqualSetsWithIndex( $expected, $actual );
6670+
$settings = $theme_json->get_settings();
6671+
$this->assertArrayNotHasKey( 'enabled', $settings['lightbox'] ?? array() );
6672+
$this->assertArrayNotHasKey( 'allowEditing', $settings['lightbox'] ?? array() );
66656673
}
6674+
66666675
/**
6667-
* @covers WP_Theme_JSON::remove_insecure_properties
6676+
* @covers WP_Theme_JSON::sanitize
6677+
* @covers WP_Theme_JSON::remove_keys_not_in_schema
66686678
*
66696679
* @ticket 64280
66706680
*/
6671-
public function test_remove_insecure_properties_should_not_allow_unsafe_settings() {
6672-
$actual = WP_Theme_JSON::remove_insecure_properties(
6681+
public function test_sanitize_preserves_boolean_values_in_block_settings() {
6682+
$theme_json = new WP_Theme_JSON(
66736683
array(
66746684
'version' => WP_Theme_JSON::LATEST_SCHEMA,
66756685
'settings' => array(
66766686
'blocks' => array(
66776687
'core/image' => array(
66786688
'lightbox' => array(
6679-
'enabled' => 'false',
6680-
'allowEditing' => true,
6689+
'enabled' => true,
6690+
'allowEditing' => false,
66816691
),
66826692
),
66836693
),
66846694
),
66856695
)
66866696
);
66876697

6688-
$expected = array(
6689-
'version' => WP_Theme_JSON::LATEST_SCHEMA,
6690-
'settings' => array(
6691-
'blocks' => array(
6692-
'core/image' => array(
6693-
'lightbox' => array(
6694-
'allowEditing' => true,
6698+
$settings = $theme_json->get_settings();
6699+
$this->assertTrue( $settings['blocks']['core/image']['lightbox']['enabled'] );
6700+
$this->assertFalse( $settings['blocks']['core/image']['lightbox']['allowEditing'] );
6701+
}
6702+
6703+
/**
6704+
* @covers WP_Theme_JSON::sanitize
6705+
* @covers WP_Theme_JSON::remove_keys_not_in_schema
6706+
*
6707+
* @ticket 64280
6708+
*/
6709+
public function test_sanitize_removes_non_boolean_values_in_block_settings() {
6710+
$theme_json = new WP_Theme_JSON(
6711+
array(
6712+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
6713+
'settings' => array(
6714+
'blocks' => array(
6715+
'core/image' => array(
6716+
'lightbox' => array(
6717+
'enabled' => 'string-value',
6718+
'allowEditing' => array( 'not', 'a', 'boolean' ),
6719+
),
66956720
),
66966721
),
66976722
),
6698-
),
6723+
)
66996724
);
67006725

6701-
$this->assertEqualSetsWithIndex( $expected, $actual );
6726+
$settings = $theme_json->get_settings();
6727+
$lightbox = $settings['blocks']['core/image']['lightbox'] ?? array();
6728+
$this->assertArrayNotHasKey( 'enabled', $lightbox );
6729+
$this->assertArrayNotHasKey( 'allowEditing', $lightbox );
6730+
}
6731+
6732+
/**
6733+
* @covers WP_Theme_JSON::sanitize
6734+
* @covers WP_Theme_JSON::remove_keys_not_in_schema
6735+
*
6736+
* @ticket 64280
6737+
*/
6738+
public function test_sanitize_preserves_null_schema_behavior() {
6739+
// Test that settings with null in schema (no type validation) still accept any type.
6740+
$theme_json = new WP_Theme_JSON(
6741+
array(
6742+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
6743+
'settings' => array(
6744+
'appearanceTools' => 'string-value', // null in schema, should accept any type.
6745+
'custom' => array( 'nested' => 'value' ), // null in schema, should accept any type.
6746+
),
6747+
)
6748+
);
6749+
6750+
$settings = $theme_json->get_settings();
6751+
$this->assertSame( 'string-value', $settings['appearanceTools'] );
6752+
$this->assertSame( array( 'nested' => 'value' ), $settings['custom'] );
67026753
}
67036754
}

0 commit comments

Comments
 (0)