mirror of
https://github.com/gosticks/wordpress-develop.git
synced 2026-06-30 23:30:05 +00:00
Editor: Add support for custom CSS in global styles.
This changeset introduces functions `wp_get_global_styles_custom_css()` and `wp_enqueue_global_styles_custom_css()`, which allow accessing and enqueuing custom CSS added via global styles. Custom CSS via global styles is handled separately from custom CSS via the Customizer. If a site uses both features, the custom CSS from both sources will be loaded. The global styles custom CSS is then loaded after the Customizer custom CSS, so if there are any conflicts between the rules, the global styles take precedence. Similarly to e.g. [55185], the result is cached in a non-persistent cache, except when `WP_DEBUG` is on to avoid interrupting the theme developer's workflow. Props glendaviesnz, oandregal, ntsekouras, mamaduka, davidbaumwald, hellofromtonya, flixos90. Fixes #57536. git-svn-id: https://develop.svn.wordpress.org/trunk@55192 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
@@ -410,6 +410,16 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex
|
||||
$block_classes['css'] = $actual_css;
|
||||
$global_styles[] = $block_classes;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the custom CSS as a separate stylesheet so any invalid CSS
|
||||
* entered by users does not break other global styles.
|
||||
*/
|
||||
$editor_settings['styles'][] = array(
|
||||
'css' => wp_get_global_styles_custom_css(),
|
||||
'__unstableType' => 'user',
|
||||
'isGlobalStyles' => true,
|
||||
);
|
||||
} else {
|
||||
// If there is no `theme.json` file, ensure base layout styles are still available.
|
||||
$block_classes = array(
|
||||
|
||||
@@ -425,6 +425,7 @@ class WP_Theme_JSON {
|
||||
'textDecoration' => null,
|
||||
'textTransform' => null,
|
||||
),
|
||||
'css' => null,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -1005,6 +1006,31 @@ class WP_Theme_JSON {
|
||||
return $stylesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the global styles custom css.
|
||||
*
|
||||
* @since 6.2.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_custom_css() {
|
||||
// Add the global styles root CSS.
|
||||
$stylesheet = _wp_array_get( $this->theme_json, array( 'styles', 'css' ), '' );
|
||||
|
||||
// Add the global styles block CSS.
|
||||
if ( isset( $this->theme_json['styles']['blocks'] ) ) {
|
||||
foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
|
||||
$custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) );
|
||||
if ( $custom_block_css ) {
|
||||
$selector = static::$blocks_metadata[ $name ]['selector'];
|
||||
$stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stylesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the page templates of the active theme.
|
||||
*
|
||||
@@ -2740,7 +2766,12 @@ class WP_Theme_JSON {
|
||||
continue;
|
||||
}
|
||||
|
||||
$output = static::remove_insecure_styles( $input );
|
||||
// The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
|
||||
if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
|
||||
$output = $input;
|
||||
} else {
|
||||
$output = static::remove_insecure_styles( $input );
|
||||
}
|
||||
|
||||
/*
|
||||
* Get a reference to element name from path.
|
||||
|
||||
@@ -577,6 +577,9 @@ add_filter( 'block_editor_settings_all', 'wp_add_editor_classic_theme_styles' );
|
||||
add_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles' );
|
||||
add_action( 'wp_footer', 'wp_enqueue_global_styles', 1 );
|
||||
|
||||
// Global styles custom CSS.
|
||||
add_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles_custom_css' );
|
||||
|
||||
// Block supports, and other styles parsed and stored in the Style Engine.
|
||||
add_action( 'wp_enqueue_scripts', 'wp_enqueue_stored_styles' );
|
||||
add_action( 'wp_footer', 'wp_enqueue_stored_styles', 1 );
|
||||
|
||||
@@ -225,6 +225,60 @@ function wp_get_global_stylesheet( $types = array() ) {
|
||||
return $stylesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global styles custom css from theme.json.
|
||||
*
|
||||
* @since 6.2.0
|
||||
*
|
||||
* @return string Stylesheet.
|
||||
*/
|
||||
function wp_get_global_styles_custom_css() {
|
||||
if ( ! wp_theme_has_theme_json() ) {
|
||||
return '';
|
||||
}
|
||||
/*
|
||||
* Ignore cache when `WP_DEBUG` is enabled, so it doesn't interfere with the theme
|
||||
* developer's workflow.
|
||||
*
|
||||
* @todo Replace `WP_DEBUG` once an "in development mode" check is available in Core.
|
||||
*/
|
||||
$can_use_cached = ! WP_DEBUG;
|
||||
|
||||
/*
|
||||
* By using the 'theme_json' group, this data is marked to be non-persistent across requests.
|
||||
* @see `wp_cache_add_non_persistent_groups()`.
|
||||
*
|
||||
* The rationale for this is to make sure derived data from theme.json
|
||||
* is always fresh from the potential modifications done via hooks
|
||||
* that can use dynamic data (modify the stylesheet depending on some option,
|
||||
* settings depending on user permissions, etc.).
|
||||
* See some of the existing hooks to modify theme.json behavior:
|
||||
* @see https://make.wordpress.org/core/2022/10/10/filters-for-theme-json-data/
|
||||
*
|
||||
* A different alternative considered was to invalidate the cache upon certain
|
||||
* events such as options add/update/delete, user meta, etc.
|
||||
* It was judged not enough, hence this approach.
|
||||
* @see https://github.com/WordPress/gutenberg/pull/45372
|
||||
*/
|
||||
$cache_key = 'wp_get_global_styles_custom_css';
|
||||
$cache_group = 'theme_json';
|
||||
if ( $can_use_cached ) {
|
||||
$cached = wp_cache_get( $cache_key, $cache_group );
|
||||
if ( $cached ) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$tree = WP_Theme_JSON_Resolver::get_merged_data();
|
||||
$stylesheet = $tree->get_custom_css();
|
||||
|
||||
if ( $can_use_cached ) {
|
||||
wp_cache_set( $cache_key, $stylesheet, $cache_group );
|
||||
}
|
||||
|
||||
return $stylesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the SVGs to be referenced as filters (duotone).
|
||||
*
|
||||
@@ -369,5 +423,6 @@ function wp_clean_theme_json_cache() {
|
||||
wp_cache_delete( 'wp_get_global_styles_svg_filters', 'theme_json' );
|
||||
wp_cache_delete( 'wp_get_global_settings_custom', 'theme_json' );
|
||||
wp_cache_delete( 'wp_get_global_settings_theme', 'theme_json' );
|
||||
wp_cache_delete( 'wp_get_global_styles_custom_css', 'theme_json' );
|
||||
WP_Theme_JSON_Resolver::clean_cached_data();
|
||||
}
|
||||
|
||||
@@ -268,6 +268,10 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
|
||||
}
|
||||
|
||||
$changes = $this->prepare_item_for_database( $request );
|
||||
if ( is_wp_error( $changes ) ) {
|
||||
return $changes;
|
||||
}
|
||||
|
||||
$result = wp_update_post( wp_slash( (array) $changes ), true, false );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
@@ -290,9 +294,10 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
|
||||
* Prepares a single global styles config for update.
|
||||
*
|
||||
* @since 5.9.0
|
||||
* @since 6.2.0 Added validation of styles.css property.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return stdClass Changes to pass to wp_update_post.
|
||||
* @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
|
||||
*/
|
||||
protected function prepare_item_for_database( $request ) {
|
||||
$changes = new stdClass();
|
||||
@@ -312,6 +317,12 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
|
||||
if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
|
||||
$config = array();
|
||||
if ( isset( $request['styles'] ) ) {
|
||||
if ( isset( $request['styles']['css'] ) ) {
|
||||
$css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
|
||||
if ( is_wp_error( $css_validation_result ) ) {
|
||||
return $css_validation_result;
|
||||
}
|
||||
}
|
||||
$config['styles'] = $request['styles'];
|
||||
} elseif ( isset( $existing_config['styles'] ) ) {
|
||||
$config['styles'] = $existing_config['styles'];
|
||||
@@ -657,4 +668,25 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate style.css as valid CSS.
|
||||
*
|
||||
* Currently just checks for invalid markup.
|
||||
*
|
||||
* @since 6.2.0
|
||||
*
|
||||
* @param string $css CSS to validate.
|
||||
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
|
||||
*/
|
||||
private function validate_custom_css( $css ) {
|
||||
if ( preg_match( '#</?\w+#', $css ) ) {
|
||||
return new WP_Error(
|
||||
'rest_custom_css_illegal_markup',
|
||||
__( 'Markup is not allowed in CSS.' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2454,6 +2454,27 @@ function wp_enqueue_global_styles() {
|
||||
wp_add_global_styles_for_blocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the global styles custom css defined via theme.json.
|
||||
*
|
||||
* @since 6.2.0
|
||||
*/
|
||||
function wp_enqueue_global_styles_custom_css() {
|
||||
if ( ! wp_is_block_theme() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't enqueue Customizer's custom CSS separately.
|
||||
remove_action( 'wp_head', 'wp_custom_css_cb', 101 );
|
||||
|
||||
$custom_css = wp_get_custom_css();
|
||||
$custom_css .= wp_get_global_styles_custom_css();
|
||||
|
||||
if ( ! empty( $custom_css ) ) {
|
||||
wp_add_inline_style( 'global-styles', $custom_css );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the SVG filters supplied by theme.json.
|
||||
*
|
||||
|
||||
@@ -532,4 +532,43 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test
|
||||
$this->assertArrayHasKey( 'https://api.w.org/action-edit-css', $links );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers WP_REST_Global_Styles_Controller::update_item
|
||||
* @ticket 57536
|
||||
*/
|
||||
public function test_update_item_valid_styles_css() {
|
||||
wp_set_current_user( self::$admin_id );
|
||||
if ( is_multisite() ) {
|
||||
grant_super_admin( self::$admin_id );
|
||||
}
|
||||
$request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'styles' => array( 'css' => 'body { color: red; }' ),
|
||||
)
|
||||
);
|
||||
$response = rest_get_server()->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
$this->assertSame( 'body { color: red; }', $data['styles']['css'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers WP_REST_Global_Styles_Controller::update_item
|
||||
* @ticket 57536
|
||||
*/
|
||||
public function test_update_item_invalid_styles_css() {
|
||||
wp_set_current_user( self::$admin_id );
|
||||
if ( is_multisite() ) {
|
||||
grant_super_admin( self::$admin_id );
|
||||
}
|
||||
$request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
|
||||
$request->set_body_params(
|
||||
array(
|
||||
'styles' => array( 'css' => '<p>test</p> body { color: red; }' ),
|
||||
)
|
||||
);
|
||||
$response = rest_get_server()->dispatch( $request );
|
||||
$this->assertErrorResponse( 'rest_custom_css_illegal_markup', $response, 400 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,36 @@
|
||||
*/
|
||||
class Tests_Theme_wpThemeJson extends WP_UnitTestCase {
|
||||
|
||||
/**
|
||||
* Administrator ID.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $administrator_id;
|
||||
|
||||
/**
|
||||
* User ID.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $user_id;
|
||||
|
||||
public static function set_up_before_class() {
|
||||
parent::set_up_before_class();
|
||||
|
||||
static::$administrator_id = self::factory()->user->create(
|
||||
array(
|
||||
'role' => 'administrator',
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_multisite() ) {
|
||||
grant_super_admin( self::$administrator_id );
|
||||
}
|
||||
|
||||
static::$user_id = self::factory()->user->create();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ticket 52991
|
||||
* @ticket 54336
|
||||
@@ -4505,4 +4535,93 @@ class Tests_Theme_wpThemeJson extends WP_UnitTestCase {
|
||||
|
||||
$this->assertSame( $expected_styles, $theme_json->get_stylesheet() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @ticket 57536
|
||||
*/
|
||||
public function test_get_custom_css_handles_global_custom_css() {
|
||||
$theme_json = new WP_Theme_JSON(
|
||||
array(
|
||||
'version' => WP_Theme_JSON::LATEST_SCHEMA,
|
||||
'styles' => array(
|
||||
'css' => 'body { color:purple; }',
|
||||
),
|
||||
)
|
||||
);
|
||||
$custom_css = 'body { color:purple; }';
|
||||
$this->assertSame( $custom_css, $theme_json->get_custom_css() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that custom CSS is kept for users with correct capabilities and removed for others.
|
||||
*
|
||||
* @ticket 57536
|
||||
*
|
||||
* @dataProvider data_custom_css_for_user_caps
|
||||
*
|
||||
* @param string $user_property The property name for current user.
|
||||
* @param array $expected Expected results.
|
||||
*/
|
||||
public function test_custom_css_for_user_caps( $user_property, array $expected ) {
|
||||
wp_set_current_user( static::${$user_property} );
|
||||
|
||||
$actual = WP_Theme_JSON::remove_insecure_properties(
|
||||
array(
|
||||
'version' => WP_Theme_JSON::LATEST_SCHEMA,
|
||||
'styles' => array(
|
||||
'css' => 'body { color:purple; }',
|
||||
'blocks' => array(
|
||||
'core/separator' => array(
|
||||
'color' => array(
|
||||
'background' => 'blue',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertSameSetsWithIndex( $expected, $actual );
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function data_custom_css_for_user_caps() {
|
||||
return array(
|
||||
'allows custom css for users with caps' => array(
|
||||
'user_property' => 'administrator_id',
|
||||
'expected' => array(
|
||||
'version' => WP_Theme_JSON::LATEST_SCHEMA,
|
||||
'styles' => array(
|
||||
'css' => 'body { color:purple; }',
|
||||
'blocks' => array(
|
||||
'core/separator' => array(
|
||||
'color' => array(
|
||||
'background' => 'blue',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'removes custom css for users without caps' => array(
|
||||
'user_property' => 'user_id',
|
||||
'expected' => array(
|
||||
'version' => WP_Theme_JSON::LATEST_SCHEMA,
|
||||
'styles' => array(
|
||||
'blocks' => array(
|
||||
'core/separator' => array(
|
||||
'color' => array(
|
||||
'background' => 'blue',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user