diff --git a/wp-includes/block-editor.php b/wp-includes/block-editor.php index bbb139d756..acf7b5d550 100644 --- a/wp-includes/block-editor.php +++ b/wp-includes/block-editor.php @@ -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( diff --git a/wp-includes/class-wp-theme-json.php b/wp-includes/class-wp-theme-json.php index 71801df2af..2908fb94d7 100644 --- a/wp-includes/class-wp-theme-json.php +++ b/wp-includes/class-wp-theme-json.php @@ -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. diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index 4e6340700c..09be9e576b 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -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 ); diff --git a/wp-includes/global-styles-and-settings.php b/wp-includes/global-styles-and-settings.php index 8195aaaa34..fe2393abea 100644 --- a/wp-includes/global-styles-and-settings.php +++ b/wp-includes/global-styles-and-settings.php @@ -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(); } diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index b504e23c35..c8eee60ed3 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -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( '# 400 ) + ); + } + return true; + } } diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php index 63f5c4fe28..2245b08c57 100644 --- a/wp-includes/script-loader.php +++ b/wp-includes/script-loader.php @@ -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. * diff --git a/wp-includes/version.php b/wp-includes/version.php index 2b2967e49a..e3e08e4050 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.2-alpha-55191'; +$wp_version = '6.2-alpha-55192'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.