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

Global styles: Allow indirect properties when unfiltered_html is not allowed #46388

Merged
Show file tree
Hide file tree
Changes from 3 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
60 changes: 60 additions & 0 deletions lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 {
'box-shadow' => array( 'shadow' ),
);

/**
* Inferred metadata for style properties that are not directly output.
*
* Each element is a direct mapping from an inferred CSS property name to the
* path to the value in theme.json & block attributes.
*
* Inferred properties are not output directly by `compute_style_properties`,
* but are used elsewhere in the processing of global styles. The inferred
* property is used to validate whether or not a style value is allowed.
*
* @since 6.2.0
* @var array
*/
const INFERRED_PROPERTIES_METADATA = array(
'gap' => array( 'spacing', 'blockGap' ),
'column-gap' => array( 'spacing', 'blockGap', 'left' ),
'row-gap' => array( 'spacing', 'blockGap', 'top' ),
);

/**
* The valid properties under the settings key.
*
Expand Down Expand Up @@ -216,4 +235,45 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 {
'textTransform' => null,
),
);

/**
* Processes a style node and returns the same node
* without the insecure styles.
*
* @since 5.9.0
* @since 6.2.0 Allow inferred properties used outside of `compute_style_properties`.
*
* @param array $input Node to process.
* @return array
*/
protected static function remove_insecure_styles( $input ) {
$output = array();
$declarations = static::compute_style_properties( $input );

foreach ( $declarations as $declaration ) {
if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) {
$path = static::PROPERTIES_METADATA[ $declaration['name'] ];

// Check the value isn't an array before adding so as to not
// double up shorthand and longhand styles.
$value = _wp_array_get( $input, $path, array() );
if ( ! is_array( $value ) ) {
_wp_array_set( $output, $path, $value );
}
}
}

foreach ( static::INFERRED_PROPERTIES_METADATA as $property => $path ) {
$value = _wp_array_get( $input, $path, array() );
if (
isset( $value ) &&
! is_array( $value ) &&
static::is_safe_css_declaration( $property, $value )
) {
_wp_array_set( $output, $path, $value );
}
}

return $output;
}
}
53 changes: 53 additions & 0 deletions lib/experimental/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,56 @@ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_6_2 {
*/
protected static $blocks_metadata = null;
}

/**
* Sanitizes global styles user content removing unsafe rules.
*
* @since 5.9.0
*
* @param string $data Post content to filter.
* @return string Filtered post content with unsafe rules removed.
*/
function gutenberg_filter_global_styles_post( $data ) {
$decoded_data = json_decode( wp_unslash( $data ), true );
$json_decoding_error = json_last_error();
if (
JSON_ERROR_NONE === $json_decoding_error &&
is_array( $decoded_data ) &&
isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
$decoded_data['isGlobalStylesUserThemeJSON']
) {
unset( $decoded_data['isGlobalStylesUserThemeJSON'] );

$data_to_encode = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $decoded_data );

$data_to_encode['isGlobalStylesUserThemeJSON'] = true;
return wp_slash( wp_json_encode( $data_to_encode ) );
}
return $data;
}

/**
* Override core's kses_init_filters hooks for global styles,
* and use Gutenberg's version instead. This ensures that
* Gutenberg's `remove_insecure_properties` function can be called.
*
* The hooks are only set if they are already added, which ensures
* that global styles is only filtered for users without the `unfiltered_html`
* capability.
*
* This function should not be backported to core.
*/
function gutenberg_override_core_kses_init_filters() {
if ( has_filter( 'content_save_pre', 'wp_filter_global_styles_post' ) ) {
remove_filter( 'content_save_pre', 'wp_filter_global_styles_post', 9 );
add_filter( 'content_save_pre', 'gutenberg_filter_global_styles_post', 9 );
}

if ( has_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post' ) ) {
remove_filter( 'content_filtered_save_pre', 'wp_filter_global_styles_post', 9 );
add_filter( 'content_filtered_save_pre', 'gutenberg_filter_global_styles_post', 9 );
}

}
add_action( 'init', 'gutenberg_override_core_kses_init_filters' );
add_action( 'set_current_user', 'gutenberg_override_core_kses_init_filters' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these changes be in a kses.php file instead, to mirror core? Or perhaps we should have a file somewhere for changes that are not ever meant to be backported? It's best to keep class files for just the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should these changes be in a kses.php file instead, to mirror core?

Great point, yes! The lib/experimental directory appears to be a good place for things that won't be backported necessarily since it isn't yet in a release, so I think lib/experimental/kses.php might work. I can give that a go 🙂

24 changes: 24 additions & 0 deletions phpunit/class-wp-theme-json-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,30 @@ public function test_get_stylesheet_with_block_support_feature_level_selectors()
$this->assertEquals( $expected, $theme_json->get_stylesheet() );
}

public function test_allow_inferred_properties() {
$actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'spacing' => array(
'blockGap' => '3em',
),
),
)
);

$expected = array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'spacing' => array(
'blockGap' => '3em',
),
),
);

$this->assertEqualSetsWithIndex( $expected, $actual );
}

public function test_remove_invalid_element_pseudo_selectors() {
$actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties(
array(
Expand Down