diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 2908fb94d7e69..37ed1e2295a18 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -931,6 +931,27 @@ public function get_settings() { } } + /** + * Processes the CSS, to apply nesting. + * + * @param string $css The CSS to process. + * @param string $selector The selector to nest. + * + * @return string The processed CSS. + */ + public function process_blocks_custom_css( $css, $selector ) { + $processed_css = ''; + + // Split CSS nested rules. + $parts = explode( '&', $css ); + foreach ( $parts as $part ) { + $processed_css .= ( ! str_contains( $part, '{' ) ) + ? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level. + : trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character. + } + return $processed_css; + } + /** * Returns the stylesheet that results of processing * the theme.json structure this object represents. @@ -1003,6 +1024,73 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); } + // Load the custom CSS last so it has the highest specificity. + if ( in_array( 'custom-css', $types, true ) ) { + // 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 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 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; } diff --git a/src/wp-includes/global-styles-and-settings.php b/src/wp-includes/global-styles-and-settings.php index fe2393abea974..df68ccb858e2e 100644 --- a/src/wp-includes/global-styles-and-settings.php +++ b/src/wp-includes/global-styles-and-settings.php @@ -126,6 +126,7 @@ function wp_get_global_styles( $path = array(), $context = array() ) { * * @since 5.9.0 * @since 6.1.0 Added 'base-layout-styles' support. + * @since 6.2.0 Support for custom-css type added. * * @param array $types Optional. Types of styles to load. * It accepts as values 'variables', 'presets', 'styles', 'base-layout-styles'. @@ -174,7 +175,7 @@ function wp_get_global_stylesheet( $types = array() ) { if ( empty( $types ) && ! $supports_theme_json ) { $types = array( 'variables', 'presets', 'base-layout-styles' ); } elseif ( empty( $types ) ) { - $types = array( 'variables', 'styles', 'presets' ); + $types = array( 'variables', 'styles', 'presets', 'custom-css' ); } /* diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index c8eee60ed3632..d27d5e4d58d13 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -324,6 +324,13 @@ protected function prepare_item_for_database( $request ) { } } $config['styles'] = $request['styles']; + if ( isset( $config['styles']['css'] ) ) { + $css_validation_result = $this->validate_custom_css( $config['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']; } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index efa709ef756dc..39dadf3d020e1 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -4624,4 +4624,129 @@ public function data_custom_css_for_user_caps() { ), ); } + + /** + * 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', + ), + ), + ), + ), + ), + ), + ); + } + + /** + * @dataProvider data_process_blocks_custom_css + * + * @param array $input An array containing the selector and css to test. + * @param string $expected Expected results. + */ + public function test_process_blocks_custom_css( $input, $expected ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array(), + ) + ); + + $this->assertEquals( $expected, $theme_json->process_blocks_custom_css( $input['css'], $input['selector'] ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_process_blocks_custom_css() { + return array( + // Simple CSS without any child selectors. + 'no child selectors' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto;', + ), + 'expected' => '.foo{color: red; margin: auto;}', + ), + // CSS with child selectors. + 'with children' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto; & .bar{color: blue;}', + ), + 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}', + ), + // CSS with child selectors and pseudo elements. + 'with children and pseudo elements' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto; & .bar{color: blue;} &::before{color: green;}', + ), + 'expected' => '.foo{color: red; margin: auto;}.foo .bar{color: blue;}.foo::before{color: green;}', + ), + ); + } }