Skip to content
Open
49 changes: 47 additions & 2 deletions src/wp-includes/class-wp-theme-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -442,8 +443,8 @@ class WP_Theme_JSON {
'allowCustomContentAndWideSize' => null,
),
'lightbox' => array(
'enabled' => null,
'allowEditing' => null,
'enabled' => true,
'allowEditing' => true,
),
'position' => array(
'fixed' => null,
Expand Down Expand Up @@ -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.
Expand All @@ -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 ] );
Expand Down Expand Up @@ -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<string|int> $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.
Expand Down Expand Up @@ -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;
}

Expand Down
132 changes: 130 additions & 2 deletions tests/phpunit/tests/theme/wpThemeJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' );
}
}
Loading