diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 30c24173f4b0b..b28cab868149d 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -262,6 +262,35 @@ class WP_Theme_JSON { 'box-shadow' => array( 'shadow' ), ); + /** + * Indirect metadata for style properties that are not directly output. + * + * Each element maps from a CSS property name to an array of + * paths to the value in theme.json & block attributes. + * + * Indirect properties are not output directly by `compute_style_properties`, + * but are used elsewhere in the processing of global styles. The indirect + * property is used to validate whether or not a style value is allowed. + * + * @since 6.1.2 + * @var array + */ + const INDIRECT_PROPERTIES_METADATA = array( + 'gap' => array( + array( 'spacing', 'blockGap' ), + ), + 'column-gap' => array( + array( 'spacing', 'blockGap', 'left' ), + ), + 'row-gap' => array( + array( 'spacing', 'blockGap', 'top' ), + ), + 'max-width' => array( + array( 'layout', 'contentSize' ), + array( 'layout', 'wideSize' ), + ), + ); + /** * Protected style properties. * @@ -2949,6 +2978,10 @@ 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 ); + return $output; } @@ -2977,6 +3010,10 @@ protected static function remove_insecure_styles( $input ) { } } } + + // Ensure indirect properties not handled by `compute_style_properties` are allowed. + static::remove_indirect_properties( $input, $output ); + return $output; } @@ -2995,6 +3032,29 @@ protected static function is_safe_css_declaration( $property_name, $property_val return ! empty( trim( $filtered ) ); } + /** + * Removes indirect properties from the given input node and + * sets in the given output node. + * + * @since 6.1.2 + * + * @param array $input Node to process. + * @param array $output The processed node. Passed by reference. + */ + private static function remove_indirect_properties( $input, &$output ) { + foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { + foreach ( $paths as $path ) { + $value = _wp_array_get( $input, $path ); + if ( + is_string( $value ) && + static::is_safe_css_declaration( $property, $value ) + ) { + _wp_array_set( $output, $path, $value ); + } + } + } + } + /** * Returns the raw data. * diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index b2d05d9fe3d4a..26d410e322102 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -2426,6 +2426,67 @@ public function test_remove_insecure_properties_applies_safe_styles() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + /** + * @ticket 57321 + * + * @covers WP_Theme_JSON::remove_insecure_properties + */ + public function test_remove_insecure_properties_should_allow_indirect_properties() { + $actual = WP_Theme_JSON::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'spacing' => array( + 'blockGap' => '3em', + ), + 'blocks' => array( + 'core/social-links' => array( + 'spacing' => array( + 'blockGap' => array( + 'left' => '2em', + 'top' => '1em', + ), + ), + ), + ), + ), + 'settings' => array( + 'layout' => array( + 'contentSize' => '800px', + 'wideSize' => '1000px', + ), + ), + ) + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'spacing' => array( + 'blockGap' => '3em', + ), + 'blocks' => array( + 'core/social-links' => array( + 'spacing' => array( + 'blockGap' => array( + 'left' => '2em', + 'top' => '1em', + ), + ), + ), + ), + ), + 'settings' => array( + 'layout' => array( + 'contentSize' => '800px', + 'wideSize' => '1000px', + ), + ), + ); + + $this->assertSameSetsWithIndex( $expected, $actual ); + } + /** * @ticket 56467 */