From 42f688c6a7e2fb63bbc16d4bcbf4cf9311e9d31e Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 06:06:36 +0100 Subject: [PATCH 1/9] Update sanitize to support pseudo selectors --- .../wordpress-6.1/class-wp-theme-json-6-1.php | 243 +++++++++++++- .../test/use-global-styles-output.js | 98 +++++- .../global-styles/use-global-styles-output.js | 36 ++- phpunit/class-wp-theme-json-test.php | 304 ++++++++++++++++++ 4 files changed, 655 insertions(+), 26 deletions(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 946680fb44bdd7..27279227a6c435 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -15,6 +15,16 @@ * @access private */ class WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 { + + /** + * Whitelist which defines which pseudo selectors are enabled for + * which elements. + * Note: this will effect both top level and block level elements. + */ + const VALID_ELEMENT_PSEUDO_SELECTORS = array( + 'link' => array( ':hover', ':focus' ), + ); + const ELEMENTS = array( 'link' => 'a', 'h1' => 'h1', @@ -43,6 +53,176 @@ public static function get_element_class_name( $element ) { return array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ? static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] : ''; } + /** + * Sanitizes the input according to the schemas. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * + * @param array $input Structure to sanitize. + * @param array $valid_block_names List of valid block names. + * @param array $valid_element_names List of valid element names. + * @return array The sanitized output. + */ + protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { + + $output = array(); + + if ( ! is_array( $input ) ) { + return $output; + } + + // Preserve only the top most level keys. + $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); + + // Remove any rules that are annotated as "top" in VALID_STYLES constant. + // Some styles are only meant to be available at the top-level (e.g.: blockGap), + // hence, the schema for blocks & elements should not have them. + $styles_non_top_level = static::VALID_STYLES; + foreach ( array_keys( $styles_non_top_level ) as $section ) { + foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { + if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { + unset( $styles_non_top_level[ $section ][ $prop ] ); + } + } + } + + // Build the schema based on valid block & element names. + $schema = array(); + $schema_styles_elements = array(); + + // Set allowed element pseudo selectors based on per element allow list. + // Target data structure in schema: + // e.g. + // - top level elements: `$schema['styles']['elements']['link'][':hover']`. + // - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. + foreach ( $valid_element_names as $element ) { + $schema_styles_elements[ $element ] = $styles_non_top_level; + + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; + } + } + } + + $schema_styles_blocks = array(); + $schema_settings_blocks = array(); + foreach ( $valid_block_names as $block ) { + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + } + + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + $schema['settings']['blocks'] = $schema_settings_blocks; + + // Remove anything that's not present in the schema. + foreach ( array( 'styles', 'settings' ) as $subtree ) { + if ( ! isset( $input[ $subtree ] ) ) { + continue; + } + + if ( ! is_array( $input[ $subtree ] ) ) { + unset( $output[ $subtree ] ); + continue; + } + + $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); + + if ( empty( $result ) ) { + unset( $output[ $subtree ] ); + } else { + $output[ $subtree ] = $result; + } + } + + return $output; + } + + + + /** + * Removes insecure data from theme.json. + * + * @since 5.9.0 + * + * @param array $theme_json Structure to sanitize. + * @return array Sanitized structure. + */ + public static function remove_insecure_properties( $theme_json ) { + $sanitized = array(); + + $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + + $valid_block_names = array_keys( static::get_blocks_metadata() ); + $valid_element_names = array_keys( static::ELEMENTS ); + + $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); + + $blocks_metadata = static::get_blocks_metadata(); + $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); + + foreach ( $style_nodes as $metadata ) { + $input = _wp_array_get( $theme_json, $metadata['path'], array() ); + if ( empty( $input ) ) { + continue; + } + + $output = static::remove_insecure_styles( $input ); + + // Get a reference to element name from path. + // $metadata['path'] = array('styles','elements','link'); + $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; + + // $output is stripped of pseudo selectors. Readd and process them + // for insecure styles here. + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { + + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { + if ( isset( $input[ $pseudo_selector ] ) ) { + $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); + } + } + } + + if ( ! empty( $output ) ) { + _wp_array_set( $sanitized, $metadata['path'], $output ); + } + } + + $setting_nodes = static::get_setting_nodes( $theme_json ); + foreach ( $setting_nodes as $metadata ) { + $input = _wp_array_get( $theme_json, $metadata['path'], array() ); + if ( empty( $input ) ) { + continue; + } + + $output = static::remove_insecure_settings( $input ); + if ( ! empty( $output ) ) { + _wp_array_set( $sanitized, $metadata['path'], $output ); + } + } + + if ( empty( $sanitized['styles'] ) ) { + unset( $theme_json['styles'] ); + } else { + $theme_json['styles'] = $sanitized['styles']; + } + + if ( empty( $sanitized['settings'] ) ) { + unset( $theme_json['settings'] ); + } else { + $theme_json['settings'] = $sanitized['settings']; + } + + return $theme_json; + } + + /** * Returns the metadata for each block. * @@ -156,11 +336,27 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { ); if ( isset( $theme_json['styles']['elements'] ) ) { + foreach ( $theme_json['styles']['elements'] as $element => $node ) { + + // Handle element defaults. $nodes[] = array( 'path' => array( 'styles', 'elements', $element ), 'selector' => static::ELEMENTS[ $element ], ); + + // Handle any pseudo selectors for the element. + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + + if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'elements', $element ), + 'selector' => static::ELEMENTS[ $element ] . $pseudo_selector, + ); + } + } + } } } @@ -228,6 +424,18 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), 'selector' => $selectors[ $name ]['elements'][ $element ], ); + + // Handle any psuedo selectors for the element. + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), + 'selector' => $selectors[ $name ]['elements'][ $element ] . $pseudo_selector, + ); + } + } + } } } } @@ -243,11 +451,32 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { * @return string Styles for the block. */ public function get_styles_for_block( $block_metadata ) { - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); - $selector = $block_metadata['selector']; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - $declarations = static::compute_style_properties( $node, $settings ); - $block_rules = ''; + + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + + $selector = $block_metadata['selector']; + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + + // Attempt to parse a pseudo selector (e.g. ":hover") from the $selector ("a:hover"). + $pseudo_matches = array(); + preg_match( '/:[a-z]+/', $selector, $pseudo_matches ); + $pseudo_selector = $pseudo_matches[0] ?? null; + + // Get a reference to element name from path. + // $block_metadata['path'] = array('styles','elements','link'); + $current_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; + + // If the current selector is a pseudo selector that's defined in the allow list for the current + // element then compute the style properties for it. + // Otherwise just compute the styles for the default selector as normal. + $declarations_pseudo_selectors = array(); + if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { + $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings ); + } else { + $declarations = static::compute_style_properties( $node, $settings ); + } + + $block_rules = ''; // 1. Separate the ones who use the general selector // and the ones who use the duotone selector. @@ -271,10 +500,10 @@ public function get_styles_for_block( $block_metadata ) { $block_rules .= 'body { margin: 0; }'; } - // 2. Generate the rules that use the general selector. + // 2. Generate and append the rules that use the general selector. $block_rules .= static::to_ruleset( $selector, $declarations ); - // 3. Generate the rules that use the duotone selector. + // 3. Generate and append the rules that use the duotone selector. if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); diff --git a/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js index d105ad7a673936..10e1d533e025ba 100644 --- a/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js @@ -40,6 +40,20 @@ describe( 'global styles renderer', () => { fontSize: '23px', }, }, + link: { + ':hover': { + color: { + background: 'green', + text: 'yellow', + }, + }, + ':focus': { + color: { + background: 'green', + text: 'yellow', + }, + }, + }, }, }, }, @@ -49,6 +63,18 @@ describe( 'global styles renderer', () => { background: 'yellow', text: 'yellow', }, + ':hover': { + color: { + background: 'hotpink', + text: 'black', + }, + }, + ':focus': { + color: { + background: 'hotpink', + text: 'black', + }, + }, }, }, }, @@ -58,6 +84,7 @@ describe( 'global styles renderer', () => { selector: '.my-heading1, .my-heading2', }, }; + expect( getNodesWithStyles( tree, blockSelectors ) ).toEqual( [ { styles: { @@ -74,6 +101,18 @@ describe( 'global styles renderer', () => { background: 'yellow', text: 'yellow', }, + ':hover': { + color: { + background: 'hotpink', + text: 'black', + }, + }, + ':focus': { + color: { + background: 'hotpink', + text: 'black', + }, + }, }, selector: ELEMENTS.link, }, @@ -102,6 +141,23 @@ describe( 'global styles renderer', () => { }, selector: '.my-heading1 h2, .my-heading2 h2', }, + { + styles: { + ':hover': { + color: { + background: 'green', + text: 'yellow', + }, + }, + ':focus': { + color: { + background: 'green', + text: 'yellow', + }, + }, + }, + selector: '.my-heading1 a, .my-heading2 a', + }, ] ); } ); } ); @@ -318,6 +374,21 @@ describe( 'global styles renderer', () => { fontSize: '42px', }, }, + link: { + color: { + text: 'blue', + }, + ':hover': { + color: { + text: 'orange', + }, + }, + ':focus': { + color: { + text: 'orange', + }, + }, + }, }, blocks: { 'core/group': { @@ -345,6 +416,16 @@ describe( 'global styles renderer', () => { color: { text: 'hotpink', }, + ':hover': { + color: { + text: 'red', + }, + }, + ':focus': { + color: { + text: 'red', + }, + }, }, }, }, @@ -358,27 +439,12 @@ describe( 'global styles renderer', () => { }, 'core/heading': { selector: 'h1,h2,h3,h4,h5,h6', - elements: { - link: - 'h1 ' + - ELEMENTS.link + - ',h2 ' + - ELEMENTS.link + - ',h3 ' + - ELEMENTS.link + - ',h4 ' + - ELEMENTS.link + - ',h5 ' + - ELEMENTS.link + - ',h6 ' + - ELEMENTS.link, - }, }, }; expect( toStyles( tree, blockSelectors ) ).toEqual( 'body {margin: 0;}' + - 'body{background-color: red;margin: 10px;padding: 10px;}h1{font-size: 42px;}.wp-block-group{margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}h1,h2,h3,h4,h5,h6{color: orange;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color: hotpink;}' + + 'body{background-color: red;margin: 10px;padding: 10px;}h1{font-size: 42px;}a{color: blue;}a:hover{color: orange;}a:focus{color: orange;}.wp-block-group{margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}h1,h2,h3,h4,h5,h6{color: orange;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color: hotpink;}h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color: red;}h1 a:focus,h2 a:focus,h3 a:focus,h4 a:focus,h5 a:focus,h6 a:focus{color: red;}' + '.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; }' + '.has-white-color{color: var(--wp--preset--color--white) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}h1.has-blue-color,h2.has-blue-color,h3.has-blue-color,h4.has-blue-color,h5.has-blue-color,h6.has-blue-color{color: var(--wp--preset--color--blue) !important;}h1.has-blue-background-color,h2.has-blue-background-color,h3.has-blue-background-color,h4.has-blue-background-color,h5.has-blue-background-color,h6.has-blue-background-color{background-color: var(--wp--preset--color--blue) !important;}h1.has-blue-border-color,h2.has-blue-border-color,h3.has-blue-border-color,h4.has-blue-border-color,h5.has-blue-border-color,h6.has-blue-border-color{border-color: var(--wp--preset--color--blue) !important;}' ); diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index 4f6265a56a920a..90f7eb3f58e9d3 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -397,10 +397,40 @@ export const toStyles = ( tree, blockSelectors, hasBlockGapSupport ) => { // Process the remaning block styles (they use either normal block class or __experimentalSelector). const declarations = getStylesDeclarations( styles ); - if ( declarations.length === 0 ) { - return; + if ( declarations?.length ) { + ruleset = ruleset + `${ selector }{${ declarations.join( ';' ) };}`; + } + + // Check for pseudo selector in `styles` and handle separately. + const psuedoSelectorStyles = Object.entries( styles ).filter( + ( [ key ] ) => key.startsWith( ':' ) + ); + + if ( psuedoSelectorStyles?.length ) { + psuedoSelectorStyles.forEach( ( [ pseudoKey, pseudoRule ] ) => { + const pseudoDeclarations = getStylesDeclarations( pseudoRule ); + + if ( pseudoDeclarations?.length ) { + // `selector` maybe provided in a form + // where block level selectors have sub element + // selectors appended to them as a comma seperated + // string. + // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`; + // Split and append pseudo selector to create + // the proper rules to target the elements. + const _selector = selector + .split( ',' ) + .map( ( sel ) => sel + pseudoKey ) + .join( ',' ); + + const psuedoRule = `${ _selector }{${ pseudoDeclarations.join( + ';' + ) };}`; + + ruleset = ruleset + psuedoRule; + } + } ); } - ruleset = ruleset + `${ selector }{${ declarations.join( ';' ) };}`; } ); /* Add alignment / layout styles */ diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index e53b2db53afcae..c7ef6604d4773b 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -685,6 +685,261 @@ public function test_get_stylesheet_preset_values_are_marked_as_important() { ); } + function test_get_stylesheet_handles_whitelisted_element_pseudo_selectors() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + function test_get_stylesheet_handles_only_psuedo_selector_rules_for_given_property() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + function test_get_stylesheet_ignores_pseudo_selectors_on_non_whitelisted_elements() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'h4' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'h4{background-color: red;color: green;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + function test_get_stylesheet_ignores_non_whitelisted_pseudo_selectors() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + ), + ':levitate' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertStringNotContainsString( 'a:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + function test_get_stylesheet_handles_priority_of_elements_vs_block_elements_pseudo_selectors() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = '.wp-block-group a{background-color: red;color: green;}.wp-block-group a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}.wp-block-group a:focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_selectors() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + ) + ), + ), + 'blocks' => array( + 'core/group' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ) + ), + ), + ), + ), + ), + ) + ); + + $base_styles = '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; }'; + + $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;}.wp-block-group a:hover{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + public function test_merge_incoming_data() { $theme_json = new WP_Theme_JSON_Gutenberg( array( @@ -1518,6 +1773,8 @@ public function test_merge_incoming_data_presets_use_default_names() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + + function test_remove_insecure_properties_removes_unsafe_styles() { $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( array( @@ -2016,6 +2273,53 @@ function test_remove_insecure_properties_applies_safe_styles() { $this->assertEqualSetsWithIndex( $expected, $actual ); } + function test_remove_invalid_element_pseudo_selectors() { + $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'hotpink', + 'background' => 'yellow', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'blue', + ), + ), + ), + ), + ), + ), + true + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'hotpink', + 'background' => 'yellow', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'blue', + ), + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + function test_get_custom_templates() { $theme_json = new WP_Theme_JSON_Gutenberg( array( From 4b6f4cbdece9a43c77fcdb0d2bc9573edb243115 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 12:00:47 +0100 Subject: [PATCH 2/9] Add :active as valid selector See https://github.com/WordPress/gutenberg/pull/41786#discussion_r903434915 --- lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 27279227a6c435..5cdf0219a7c2c8 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -22,7 +22,7 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 { * Note: this will effect both top level and block level elements. */ const VALID_ELEMENT_PSEUDO_SELECTORS = array( - 'link' => array( ':hover', ':focus' ), + 'link' => array( ':hover', ':focus', ':active' ), ); const ELEMENTS = array( From 4dc636d7cb55d4a9fa715f90ac1db8ce2f740ae5 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 12:02:25 +0100 Subject: [PATCH 3/9] Check path is an element path Co-authored-by: Adam Zielinski --- lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 5cdf0219a7c2c8..18409081d40386 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -464,7 +464,10 @@ public function get_styles_for_block( $block_metadata ) { // Get a reference to element name from path. // $block_metadata['path'] = array('styles','elements','link'); - $current_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; + // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. + // Skip non-element paths like just ['styles'] + $is_processing_element = count( $block_metadata['path'] ) === 3 && $block_metadata['path'][1] === 'elements'; + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : nulll; // If the current selector is a pseudo selector that's defined in the allow list for the current // element then compute the style properties for it. From 1e7b946b6aee32ee5320aa5aea3f62d0f6e6e85f Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 12:05:38 +0100 Subject: [PATCH 4/9] Fix null and yoda condition --- lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 18409081d40386..9a4b1ba3ea0fc3 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -466,8 +466,8 @@ public function get_styles_for_block( $block_metadata ) { // $block_metadata['path'] = array('styles','elements','link'); // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. // Skip non-element paths like just ['styles'] - $is_processing_element = count( $block_metadata['path'] ) === 3 && $block_metadata['path'][1] === 'elements'; - $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : nulll; + $is_processing_element = count( $block_metadata['path'] ) === 3 && 'elements' === $block_metadata['path'][1]; + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; // If the current selector is a pseudo selector that's defined in the allow list for the current // element then compute the style properties for it. From 5c61dcd53c0565e900b2911f8365c6374a64b476 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 12:12:52 +0100 Subject: [PATCH 5/9] Fix check for elements to avoid relying on hardcoded array indexes Resolves https://github.com/WordPress/gutenberg/pull/41786#discussion_r903602186 --- lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 9a4b1ba3ea0fc3..9b795d99268972 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -466,14 +466,15 @@ public function get_styles_for_block( $block_metadata ) { // $block_metadata['path'] = array('styles','elements','link'); // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. // Skip non-element paths like just ['styles'] - $is_processing_element = count( $block_metadata['path'] ) === 3 && 'elements' === $block_metadata['path'][1]; - $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; + $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); + + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; // If the current selector is a pseudo selector that's defined in the allow list for the current // element then compute the style properties for it. // Otherwise just compute the styles for the default selector as normal. $declarations_pseudo_selectors = array(); - if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { + if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) { $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings ); } else { $declarations = static::compute_style_properties( $node, $settings ); From bf0a1c36a0e7c527cb4a63caad858c1758894d25 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 12:16:15 +0100 Subject: [PATCH 6/9] Exit loop early --- .../global-styles/use-global-styles-output.js | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index 90f7eb3f58e9d3..9a8ad4f54282b7 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -410,25 +410,27 @@ export const toStyles = ( tree, blockSelectors, hasBlockGapSupport ) => { psuedoSelectorStyles.forEach( ( [ pseudoKey, pseudoRule ] ) => { const pseudoDeclarations = getStylesDeclarations( pseudoRule ); - if ( pseudoDeclarations?.length ) { - // `selector` maybe provided in a form - // where block level selectors have sub element - // selectors appended to them as a comma seperated - // string. - // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`; - // Split and append pseudo selector to create - // the proper rules to target the elements. - const _selector = selector - .split( ',' ) - .map( ( sel ) => sel + pseudoKey ) - .join( ',' ); - - const psuedoRule = `${ _selector }{${ pseudoDeclarations.join( - ';' - ) };}`; - - ruleset = ruleset + psuedoRule; + if ( ! pseudoDeclarations?.length ) { + return; } + + // `selector` maybe provided in a form + // where block level selectors have sub element + // selectors appended to them as a comma seperated + // string. + // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`; + // Split and append pseudo selector to create + // the proper rules to target the elements. + const _selector = selector + .split( ',' ) + .map( ( sel ) => sel + pseudoKey ) + .join( ',' ); + + const psuedoRule = `${ _selector }{${ pseudoDeclarations.join( + ';' + ) };}`; + + ruleset = ruleset + psuedoRule; } ); } } ); From cd6ab644035eb712ebb4c6cc55482a2d2a598fe9 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 22 Jun 2022 14:50:24 +0100 Subject: [PATCH 7/9] Fix linting --- lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php | 7 +++---- phpunit/class-wp-theme-json-test.php | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index 9b795d99268972..b0da19db742bd5 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -175,7 +175,7 @@ public static function remove_insecure_properties( $theme_json ) { $output = static::remove_insecure_styles( $input ); // Get a reference to element name from path. - // $metadata['path'] = array('styles','elements','link'); + // $metadata['path'] = array('styles','elements','link');. $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; // $output is stripped of pseudo selectors. Readd and process them @@ -460,12 +460,12 @@ public function get_styles_for_block( $block_metadata ) { // Attempt to parse a pseudo selector (e.g. ":hover") from the $selector ("a:hover"). $pseudo_matches = array(); preg_match( '/:[a-z]+/', $selector, $pseudo_matches ); - $pseudo_selector = $pseudo_matches[0] ?? null; + $pseudo_selector = $pseudo_matches[0] ? $pseudo_matches[0] : null; // Get a reference to element name from path. // $block_metadata['path'] = array('styles','elements','link'); // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. - // Skip non-element paths like just ['styles'] + // Skip non-element paths like just ['styles']. $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; @@ -473,7 +473,6 @@ public function get_styles_for_block( $block_metadata ) { // If the current selector is a pseudo selector that's defined in the allow list for the current // element then compute the style properties for it. // Otherwise just compute the styles for the default selector as normal. - $declarations_pseudo_selectors = array(); if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) { $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings ); } else { diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index c7ef6604d4773b..78267771bcc460 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -893,7 +893,7 @@ function test_get_stylesheet_handles_priority_of_elements_vs_block_elements_pseu $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } - function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_selectors() { + function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_selectors() { $theme_json = new WP_Theme_JSON_Gutenberg( array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, @@ -905,23 +905,23 @@ function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_sele 'background' => 'red', ), ':hover' => array( - 'color' => array( + 'color' => array( 'text' => 'red', 'background' => 'green', ), - ) + ), ), ), - 'blocks' => array( + 'blocks' => array( 'core/group' => array( 'elements' => array( 'link' => array( ':hover' => array( - 'color' => array( + 'color' => array( 'text' => 'yellow', 'background' => 'black', ), - ) + ), ), ), ), From 13842c600f4cc69e901688dcf0c33368165ee274 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 23 Jun 2022 10:10:59 +0100 Subject: [PATCH 8/9] Fix offset bug when no pseudo selector present --- lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php index b0da19db742bd5..4bfdd34e2b2fb8 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php @@ -460,7 +460,7 @@ public function get_styles_for_block( $block_metadata ) { // Attempt to parse a pseudo selector (e.g. ":hover") from the $selector ("a:hover"). $pseudo_matches = array(); preg_match( '/:[a-z]+/', $selector, $pseudo_matches ); - $pseudo_selector = $pseudo_matches[0] ? $pseudo_matches[0] : null; + $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; // Get a reference to element name from path. // $block_metadata['path'] = array('styles','elements','link'); From 82f5a3760df85b4ad5fccdc8bdaec4c4518dfe07 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Thu, 23 Jun 2022 10:58:51 +0100 Subject: [PATCH 9/9] Specifically declare that links should be underlined --- lib/compat/wordpress-6.1/theme.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/compat/wordpress-6.1/theme.json b/lib/compat/wordpress-6.1/theme.json index 00acba508d0336..eb37ea27068873 100644 --- a/lib/compat/wordpress-6.1/theme.json +++ b/lib/compat/wordpress-6.1/theme.json @@ -250,6 +250,11 @@ "fontSize": "1.125em", "textDecoration": "none" } + }, + "link": { + "typography": { + "textDecoration": "underline" + } } }, "spacing": { "blockGap": "24px" }