Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update WP_Theme_JSON API so presets are always keyed by origin #32622

Merged
merged 4 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
163 changes: 93 additions & 70 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class WP_Theme_JSON_Gutenberg {
*/
private $theme_json = null;

/**
* What source of data this object represents.
* One of VALID_ORIGINS.
*
* @var string
*/
private $origin = null;
oandregal marked this conversation as resolved.
Show resolved Hide resolved

/**
* Holds block metadata extracted from block.json
* to be shared among all instances so we don't
Expand All @@ -34,6 +42,12 @@ class WP_Theme_JSON_Gutenberg {
*/
const ROOT_BLOCK_SELECTOR = 'body';

const VALID_ORIGINS = array(
'core',
'theme',
'user',
);

const VALID_TOP_LEVEL_KEYS = array(
'customTemplates',
'templateParts',
Expand Down Expand Up @@ -277,9 +291,16 @@ class WP_Theme_JSON_Gutenberg {
/**
* Constructor.
*
* @param array $theme_json A structure that follows the theme.json schema.
* @param array $theme_json A structure that follows the theme.json schema.
* @param string $origin What source of data this object represents. One of core, theme, or user. Default: theme.
*/
public function __construct( $theme_json = array() ) {
public function __construct( $theme_json = array(), $origin = 'theme' ) {
if ( in_array( $origin, self::VALID_ORIGINS, true ) ) {
$this->origin = $origin;
} else {
$this->origin = 'theme';
}

// The old format is not meant to be ported to core.
// We can remove it at that point.
if ( ! isset( $theme_json['version'] ) || 0 === $theme_json['version'] ) {
Expand All @@ -289,6 +310,28 @@ public function __construct( $theme_json = array() ) {
$valid_block_names = array_keys( self::get_blocks_metadata() );
$valid_element_names = array_keys( self::ELEMENTS );
$this->theme_json = self::sanitize( $theme_json, $valid_block_names, $valid_element_names );

// Internally, presets are keyed by origin.
jorgefilipecosta marked this conversation as resolved.
Show resolved Hide resolved
$nodes = self::get_setting_nodes( $this->theme_json );
foreach ( $nodes as $node ) {
foreach ( self::PRESETS_METADATA as $preset ) {
Copy link
Member

Choose a reason for hiding this comment

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

Not all presets are keyed by origin, only colors, gradients, font sizes, and font families are. For example, duotone is not. I think we need to take that into account inside the look.

Copy link
Member Author

Choose a reason for hiding this comment

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

duotone is not part of PRESETS_METADATA as far as I can see?

Copy link
Contributor

Choose a reason for hiding this comment

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

should all presets be keyed? Why have special cases?

Copy link
Member

Choose a reason for hiding this comment

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

duotone is not part of PRESETS_METADATA as far as I can see?

Yes, I wrongly thought it was part of presets metadata.

should all presets be keyed? Why have special cases?

The rationale was that duotone was dynamically injected there was no need to output class variables etc. Also, duotone does not references or contains a name the direct colors are store in attributes "duotone":["#000097","#ff4747]. We don't have a need for this system there. But for consistency reasons, we may reconsider.

$path = array_merge( $node['path'], $preset['path'] );
$preset = _wp_array_get( $this->theme_json, $path, array() );
if ( ! empty( $preset ) ) {
gutenberg_experimental_set( $this->theme_json, $path, array( $origin => $preset ) );
}
}
}
}

/**
* Returns the origin of data.
* One of the valid origins: core, theme, user.
*
* @return string
*/
private function get_origin() {
return $this->origin;
}

/**
Expand Down Expand Up @@ -657,9 +700,8 @@ private static function append_to_selector( $selector, $to_append ) {
* @return array Array of presets where each key is a slug and each value is the preset value.
*/
private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) {
$origins = array( 'core', 'theme', 'user' );
$result = array();
foreach ( $origins as $origin ) {
$result = array();
foreach ( self::VALID_ORIGINS as $origin ) {
if ( ! isset( $preset_per_origin[ $origin ] ) ) {
continue;
}
Expand Down Expand Up @@ -1133,59 +1175,35 @@ public function get_stylesheet( $type = 'all' ) {
* Merge new incoming data.
*
* @param WP_Theme_JSON $incoming Data to merge.
* @param string $origin origin of the incoming data (e.g: core, theme, or user).
*/
public function merge( $incoming, $origin ) {

public function merge( $incoming ) {
$origin = $incoming->get_origin();
oandregal marked this conversation as resolved.
Show resolved Hide resolved
$incoming_data = $incoming->get_raw_data();
$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );

// The array_replace_recursive algorithm merges at the leaf level.
// For leaf values that are arrays it will use the numeric indexes for replacement.
// In those cases, what we want is to use the incoming value, if it exists.
//
// These are the cases that have array values at the leaf levels.
$properties = array();
$properties[] = array( 'custom' );
$properties[] = array( 'spacing', 'units' );
$properties[] = array( 'color', 'duotone' );

$to_append = array();
$to_append[] = array( 'color', 'palette' );
$to_append[] = array( 'color', 'gradients' );
$to_append[] = array( 'typography', 'fontSizes' );
$to_append[] = array( 'typography', 'fontFamilies' );
// In those cases, we want to replace the existing with the incoming value, if it exists.
$to_replace = array();
$to_replace[] = array( 'custom' );
$to_replace[] = array( 'spacing', 'units' );
$to_replace[] = array( 'color', 'duotone' );
foreach ( self::VALID_ORIGINS as $origin ) {
$to_replace[] = array( 'color', 'palette', $origin );
$to_replace[] = array( 'color', 'gradients', $origin );
$to_replace[] = array( 'typography', 'fontSizes', $origin );
$to_replace[] = array( 'typography', 'fontFamilies', $origin );
}

$nodes = self::get_setting_nodes( $this->theme_json );
foreach ( $nodes as $metadata ) {
foreach ( $properties as $property_path ) {
foreach ( $to_replace as $property_path ) {
$path = array_merge( $metadata['path'], $property_path );
$node = _wp_array_get( $incoming_data, $path, array() );
if ( ! empty( $node ) ) {
gutenberg_experimental_set( $this->theme_json, $path, $node );
}
}

foreach ( $to_append as $property_path ) {
$path = array_merge( $metadata['path'], $property_path );
$node = _wp_array_get( $incoming_data, $path, null );
if ( null !== $node ) {
$existing_node = _wp_array_get( $this->theme_json, $path, null );
$new_node = array_filter(
$existing_node,
function ( $key ) {
return in_array( $key, array( 'core', 'theme', 'user ' ), true );
},
ARRAY_FILTER_USE_KEY
);
if ( isset( $node[ $origin ] ) ) {
$new_node[ $origin ] = $node[ $origin ];
} else {
$new_node[ $origin ] = $node;
}
gutenberg_experimental_set( $this->theme_json, $path, $new_node );
}
}
}

}
Expand All @@ -1201,40 +1219,45 @@ function ( $key ) {
private static function remove_insecure_settings( $input ) {
oandregal marked this conversation as resolved.
Show resolved Hide resolved
$output = array();
foreach ( self::PRESETS_METADATA as $preset_metadata ) {
$current_preset = _wp_array_get( $input, $preset_metadata['path'], null );
if ( null === $current_preset ) {
$preset_by_origin = _wp_array_get( $input, $preset_metadata['path'], null );
if ( null === $preset_by_origin ) {
continue;
}

$escaped_preset = array();
foreach ( $current_preset as $single_preset ) {
if (
esc_attr( esc_html( $single_preset['name'] ) ) === $single_preset['name'] &&
sanitize_html_class( $single_preset['slug'] ) === $single_preset['slug']
) {
$value = $single_preset[ $preset_metadata['value_key'] ];
$single_preset_is_valid = null;
if ( isset( $preset_metadata['classes'] ) && count( $preset_metadata['classes'] ) > 0 ) {
$single_preset_is_valid = true;
foreach ( $preset_metadata['classes'] as $class_meta_data ) {
$property = $class_meta_data['property_name'];
if ( ! self::is_safe_css_declaration( $property, $value ) ) {
$single_preset_is_valid = false;
break;
foreach ( self::VALID_ORIGINS as $origin ) {
if ( ! isset( $preset_by_origin[ $origin ] ) ) {
continue;
}

$escaped_preset = array();
foreach ( $preset_by_origin[ $origin ] as $single_preset ) {
if (
esc_attr( esc_html( $single_preset['name'] ) ) === $single_preset['name'] &&
sanitize_html_class( $single_preset['slug'] ) === $single_preset['slug']
) {
$value = $single_preset[ $preset_metadata['value_key'] ];
$single_preset_is_valid = null;
if ( isset( $preset_metadata['classes'] ) && count( $preset_metadata['classes'] ) > 0 ) {
$single_preset_is_valid = true;
foreach ( $preset_metadata['classes'] as $class_meta_data ) {
$property = $class_meta_data['property_name'];
if ( ! self::is_safe_css_declaration( $property, $value ) ) {
$single_preset_is_valid = false;
break;
}
}
} else {
$property = $preset_metadata['css_var_infix'];
$single_preset_is_valid = self::is_safe_css_declaration( $property, $value );
}
if ( $single_preset_is_valid ) {
$escaped_preset[] = $single_preset;
}
} else {
$property = $preset_metadata['css_var_infix'];
$single_preset_is_valid = self::is_safe_css_declaration( $property, $value );
}
if ( $single_preset_is_valid ) {
$escaped_preset[] = $single_preset;
}
}
}

if ( ! empty( $escaped_preset ) ) {
gutenberg_experimental_set( $output, $preset_metadata['path'], $escaped_preset );
if ( ! empty( $escaped_preset ) ) {
gutenberg_experimental_set( $output, array_merge( $preset_metadata['path'], array( $origin ) ), $escaped_preset );
}
}
}

Expand Down
14 changes: 7 additions & 7 deletions lib/class-wp-theme-json-resolver-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public static function get_core_data() {

$config = self::read_json_file( __DIR__ . '/experimental-default-theme.json' );
$config = self::translate( $config );
self::$core = new WP_Theme_JSON_Gutenberg( $config );
self::$core = new WP_Theme_JSON_Gutenberg( $config, 'core' );

return self::$core;
}
Expand Down Expand Up @@ -290,7 +290,7 @@ public static function get_theme_data( $theme_support_data = array() ) {
* to override the ones declared via add_theme_support.
*/
$with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data );
$with_theme_supports->merge( self::$theme, 'theme' );
$with_theme_supports->merge( self::$theme );

return $with_theme_supports;
}
Expand Down Expand Up @@ -368,7 +368,7 @@ public static function get_user_data() {
$json_decoding_error = json_last_error();
if ( JSON_ERROR_NONE !== $json_decoding_error ) {
trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() );
return new WP_Theme_JSON_Gutenberg( $config );
return new WP_Theme_JSON_Gutenberg( $config, 'user' );
}

// Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
Expand All @@ -382,7 +382,7 @@ public static function get_user_data() {
$config = $decoded_data;
}
}
self::$user = new WP_Theme_JSON_Gutenberg( $config );
self::$user = new WP_Theme_JSON_Gutenberg( $config, 'user' );

return self::$user;
}
Expand Down Expand Up @@ -415,11 +415,11 @@ public static function get_merged_data( $settings = array(), $origin = 'user' )
$theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( $settings );

$result = new WP_Theme_JSON_Gutenberg();
$result->merge( self::get_core_data(), 'core' );
$result->merge( self::get_theme_data( $theme_support_data ), 'theme' );
$result->merge( self::get_core_data() );
$result->merge( self::get_theme_data( $theme_support_data ) );

if ( 'user' === $origin ) {
$result->merge( self::get_user_data(), 'user' );
$result->merge( self::get_user_data() );
}

return $result;
Expand Down
2 changes: 1 addition & 1 deletion lib/global-styles.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ function gutenberg_global_styles_filter_post( $content ) {
$decoded_data['isGlobalStylesUserThemeJSON']
) {
unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
$theme_json = new WP_Theme_JSON_Gutenberg( $decoded_data );
$theme_json = new WP_Theme_JSON_Gutenberg( $decoded_data, 'user' );
$theme_json->remove_insecure_properties();
$data_to_encode = $theme_json->get_raw_data();
$data_to_encode['isGlobalStylesUserThemeJSON'] = true;
Expand Down
30 changes: 17 additions & 13 deletions phpunit/class-wp-theme-json-resolver-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,17 @@ function test_translations_are_applied() {
array(
'color' => array(
'palette' => array(
array(
'slug' => 'light',
'name' => 'Jasny',
'color' => '#f5f7f9',
),
array(
'slug' => 'dark',
'name' => 'Ciemny',
'color' => '#000',
'theme' => array(
array(
'slug' => 'light',
'name' => 'Jasny',
'color' => '#f5f7f9',
),
array(
'slug' => 'dark',
'name' => 'Ciemny',
'color' => '#000',
),
),
),
'custom' => false,
Expand All @@ -172,10 +174,12 @@ function test_translations_are_applied() {
'core/paragraph' => array(
'color' => array(
'palette' => array(
array(
'slug' => 'light',
'name' => 'Jasny',
'color' => '#f5f7f9',
'theme' => array(
array(
'slug' => 'light',
'name' => 'Jasny',
'color' => '#f5f7f9',
),
),
),
),
Expand Down
Loading