Skip to content

Commit 8a4a40e

Browse files
committed
WP_Theme_JSON: Prevent implicit coercion in to_ruleset
`to_ruleset` used string concatenation (`$element['name'] . ': ' . $element['value'] . ';'`), so PHP implicitly coerced non-string values (e.g. booleans → `'1'`/`''`, arrays → `'Array'`). That could emit invalid or misleading CSS. At the same time, pass a `style` theme.json path in `test_get_styles_with_appearance_tools()` to simulate a style node. Before it was `settings`. Props ramonopoly, andrewserong, isabel_brison. Fixes #64848. --This line, and those below, will be ignored-- M src/wp-includes/class-wp-theme-json.php M tests/phpunit/tests/theme/wpThemeJson.php git-svn-id: https://develop.svn.wordpress.org/trunk@62347 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 24c2f3f commit 8a4a40e

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1982,6 +1982,7 @@ protected function get_css_variables( $nodes, $origins ) {
19821982
* creates the corresponding ruleset.
19831983
*
19841984
* @since 5.8.0
1985+
* @since 7.1.0 Skip declarations whose value is not a plain string (booleans, arrays, objects, etc.).
19851986
*
19861987
* @param string $selector CSS selector.
19871988
* @param array $declarations List of declarations.
@@ -1995,7 +1996,18 @@ protected static function to_ruleset( $selector, $declarations ) {
19951996
$declaration_block = array_reduce(
19961997
$declarations,
19971998
static function ( $carry, $element ) {
1998-
return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
1999+
$value = $element['value'];
2000+
2001+
if ( is_numeric( $value ) ) {
2002+
$value = (string) $value;
2003+
}
2004+
2005+
if ( ! is_string( $value ) ) {
2006+
return $carry;
2007+
}
2008+
2009+
return $carry .= $element['name'] . ': ' . $value . ';';
2010+
},
19992011
''
20002012
);
20012013

tests/phpunit/tests/theme/wpThemeJson.php

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4138,6 +4138,7 @@ public function test_get_styles_with_content_width() {
41384138
* @ticket 60936
41394139
* @ticket 61165
41404140
* @ticket 61829
4141+
* @ticket 64848
41414142
*/
41424143
public function test_get_styles_with_appearance_tools() {
41434144
$theme_json = new WP_Theme_JSON(
@@ -4150,11 +4151,11 @@ public function test_get_styles_with_appearance_tools() {
41504151
);
41514152

41524153
$metadata = array(
4153-
'path' => array( 'settings' ),
4154+
'path' => array( 'styles' ),
41544155
'selector' => 'body',
41554156
);
41564157

4157-
$expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1;}:root :where(.is-layout-grid){gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}';
4158+
$expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}';
41584159
$this->assertSame( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ) );
41594160
}
41604161

@@ -7054,4 +7055,44 @@ public function test_sanitize_preserves_null_schema_behavior() {
70547055
$this->assertSame( 'string-value', $settings['appearanceTools'], 'Appearance tools should be string value' );
70557056
$this->assertSame( array( 'nested' => 'value' ), $settings['custom'], 'Custom should be array value' );
70567057
}
7058+
7059+
/**
7060+
* @covers WP_Theme_JSON::to_ruleset
7061+
*
7062+
* @ticket 64848
7063+
*/
7064+
public function test_to_ruleset_skips_non_scalar_values_and_casts_numerics() {
7065+
$reflection = new ReflectionMethod( WP_Theme_JSON::class, 'to_ruleset' );
7066+
if ( PHP_VERSION_ID < 80100 ) {
7067+
$reflection->setAccessible( true );
7068+
}
7069+
$declarations = array(
7070+
array(
7071+
'name' => 'color',
7072+
'value' => 'red',
7073+
),
7074+
array(
7075+
'name' => 'opacity',
7076+
'value' => true,
7077+
),
7078+
array(
7079+
'name' => 'margin',
7080+
'value' => 0,
7081+
),
7082+
array(
7083+
'name' => 'padding',
7084+
'value' => false,
7085+
),
7086+
array(
7087+
'name' => 'gap',
7088+
'value' => array(),
7089+
),
7090+
);
7091+
$result = $reflection->invoke( null, '.test', $declarations );
7092+
$this->assertStringContainsString( 'color: red;', $result, 'Color declaration should be included' );
7093+
$this->assertStringContainsString( 'margin: 0;', $result, 'Numeric value should be cast to string' );
7094+
$this->assertStringNotContainsString( 'opacity', $result, 'Boolean value should be skipped' );
7095+
$this->assertStringNotContainsString( 'padding', $result, 'Boolean value should be skipped' );
7096+
$this->assertStringNotContainsString( 'gap', $result, 'Array value should be skipped' );
7097+
}
70577098
}

0 commit comments

Comments
 (0)