Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 239 additions & 7 deletions lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', ':active' ),
);

const ELEMENTS = array(
'link' => 'a',
'h1' => 'h1',
Expand Down Expand Up @@ -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;
Comment thread
getdave marked this conversation as resolved.
}

$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 ] );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is why we need to update the schema to allow for pseudo selectors.


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.
*
Expand Down Expand Up @@ -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,
);
}
}
}
}
}

Expand Down Expand Up @@ -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 ] ) ) {
Comment thread
getdave marked this conversation as resolved.
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,
);
}
}
}
}
}
}
Expand All @@ -243,11 +451,35 @@ 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 = isset( $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'].
$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.
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 );
}

$block_rules = '';

// 1. Separate the ones who use the general selector
// and the ones who use the duotone selector.
Expand All @@ -271,10 +503,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 );
Expand Down
5 changes: 5 additions & 0 deletions lib/compat/wordpress-6.1/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@
"fontSize": "1.125em",
"textDecoration": "none"
}
},
"link": {
"typography": {
"textDecoration": "underline"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you recall why we have to explicitly set the link style to "underline" in the core theme.json file? I mean the default behavior of browser is to underline links, so why we had to add this explicitly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really can't I'm afraid. If I had to guess, I'd say that for a11y reasons Navigation links would need to be underlined and perhaps default styling at the time was not not have them underlined? It may be that things have changed and browser defaults are now allowed.

Perhaps @MaggieCabrera or @mikachan would know?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason right now! Maybe we had some reset CSS back then of some kind? Nowadays it seems like we could go without it

}
},
"spacing": { "blockGap": "24px" }
Expand Down
Loading