Make WordPress Core

Changeset 55192


Ignore:
Timestamp:
02/02/2023 06:50:54 PM (2 years ago)
Author:
flixos90
Message:

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.

Location:
trunk
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-editor.php

    r55181 r55192  
    411411            $global_styles[]      = $block_classes;
    412412        }
     413
     414        /*
     415         * Add the custom CSS as a separate stylesheet so any invalid CSS
     416         * entered by users does not break other global styles.
     417         */
     418        $editor_settings['styles'][] = array(
     419            'css'            => wp_get_global_styles_custom_css(),
     420            '__unstableType' => 'user',
     421            'isGlobalStyles' => true,
     422        );
    413423    } else {
    414424        // If there is no `theme.json` file, ensure base layout styles are still available.
  • trunk/src/wp-includes/class-wp-theme-json.php

    r55176 r55192  
    426426            'textTransform'  => null,
    427427        ),
     428        'css'        => null,
    428429    );
    429430
     
    10011002        if ( in_array( 'presets', $types, true ) ) {
    10021003            $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
     1004        }
     1005
     1006        return $stylesheet;
     1007    }
     1008
     1009    /**
     1010     * Returns the global styles custom css.
     1011     *
     1012     * @since 6.2.0
     1013     *
     1014     * @return string
     1015     */
     1016    public function get_custom_css() {
     1017        // Add the global styles root CSS.
     1018        $stylesheet = _wp_array_get( $this->theme_json, array( 'styles', 'css' ), '' );
     1019
     1020        // Add the global styles block CSS.
     1021        if ( isset( $this->theme_json['styles']['blocks'] ) ) {
     1022            foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
     1023                $custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) );
     1024                if ( $custom_block_css ) {
     1025                    $selector    = static::$blocks_metadata[ $name ]['selector'];
     1026                    $stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
     1027                }
     1028            }
    10031029        }
    10041030
     
    27412767            }
    27422768
    2743             $output = static::remove_insecure_styles( $input );
     2769            // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
     2770            if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
     2771                $output = $input;
     2772            } else {
     2773                $output = static::remove_insecure_styles( $input );
     2774            }
    27442775
    27452776            /*
  • trunk/src/wp-includes/default-filters.php

    r55086 r55192  
    578578add_action( 'wp_footer', 'wp_enqueue_global_styles', 1 );
    579579
     580// Global styles custom CSS.
     581add_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles_custom_css' );
     582
    580583// Block supports, and other styles parsed and stored in the Style Engine.
    581584add_action( 'wp_enqueue_scripts', 'wp_enqueue_stored_styles' );
  • trunk/src/wp-includes/global-styles-and-settings.php

    r55185 r55192  
    219219
    220220    $stylesheet = $styles_variables . $styles_rest;
     221    if ( $can_use_cached ) {
     222        wp_cache_set( $cache_key, $stylesheet, $cache_group );
     223    }
     224
     225    return $stylesheet;
     226}
     227
     228/**
     229 * Gets the global styles custom css from theme.json.
     230 *
     231 * @since 6.2.0
     232 *
     233 * @return string Stylesheet.
     234 */
     235function wp_get_global_styles_custom_css() {
     236    if ( ! wp_theme_has_theme_json() ) {
     237        return '';
     238    }
     239    /*
     240     * Ignore cache when `WP_DEBUG` is enabled, so it doesn't interfere with the theme
     241     * developer's workflow.
     242     *
     243     * @todo Replace `WP_DEBUG` once an "in development mode" check is available in Core.
     244     */
     245    $can_use_cached = ! WP_DEBUG;
     246
     247    /*
     248     * By using the 'theme_json' group, this data is marked to be non-persistent across requests.
     249     * @see `wp_cache_add_non_persistent_groups()`.
     250     *
     251     * The rationale for this is to make sure derived data from theme.json
     252     * is always fresh from the potential modifications done via hooks
     253     * that can use dynamic data (modify the stylesheet depending on some option,
     254     * settings depending on user permissions, etc.).
     255     * See some of the existing hooks to modify theme.json behavior:
     256     * @see https://make.wordpress.org/core/2022/10/10/filters-for-theme-json-data/
     257     *
     258     * A different alternative considered was to invalidate the cache upon certain
     259     * events such as options add/update/delete, user meta, etc.
     260     * It was judged not enough, hence this approach.
     261     * @see https://github.com/WordPress/gutenberg/pull/45372
     262     */
     263    $cache_key   = 'wp_get_global_styles_custom_css';
     264    $cache_group = 'theme_json';
     265    if ( $can_use_cached ) {
     266        $cached = wp_cache_get( $cache_key, $cache_group );
     267        if ( $cached ) {
     268            return $cached;
     269        }
     270    }
     271
     272    $tree       = WP_Theme_JSON_Resolver::get_merged_data();
     273    $stylesheet = $tree->get_custom_css();
     274
    221275    if ( $can_use_cached ) {
    222276        wp_cache_set( $cache_key, $stylesheet, $cache_group );
     
    370424    wp_cache_delete( 'wp_get_global_settings_custom', 'theme_json' );
    371425    wp_cache_delete( 'wp_get_global_settings_theme', 'theme_json' );
     426    wp_cache_delete( 'wp_get_global_styles_custom_css', 'theme_json' );
    372427    WP_Theme_JSON_Resolver::clean_cached_data();
    373428}
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php

    r55177 r55192  
    269269
    270270        $changes = $this->prepare_item_for_database( $request );
     271        if ( is_wp_error( $changes ) ) {
     272            return $changes;
     273        }
     274
    271275        $result  = wp_update_post( wp_slash( (array) $changes ), true, false );
    272276        if ( is_wp_error( $result ) ) {
     
    291295     *
    292296     * @since 5.9.0
     297     * @since 6.2.0 Added validation of styles.css property.
    293298     *
    294299     * @param WP_REST_Request $request Request object.
    295      * @return stdClass Changes to pass to wp_update_post.
     300     * @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
    296301     */
    297302    protected function prepare_item_for_database( $request ) {
     
    313318            $config = array();
    314319            if ( isset( $request['styles'] ) ) {
     320                if ( isset( $request['styles']['css'] ) ) {
     321                    $css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
     322                    if ( is_wp_error( $css_validation_result ) ) {
     323                        return $css_validation_result;
     324                    }
     325                }
    315326                $config['styles'] = $request['styles'];
    316327            } elseif ( isset( $existing_config['styles'] ) ) {
     
    658669        return $response;
    659670    }
     671
     672    /**
     673     * Validate style.css as valid CSS.
     674     *
     675     * Currently just checks for invalid markup.
     676     *
     677     * @since 6.2.0
     678     *
     679     * @param string $css CSS to validate.
     680     * @return true|WP_Error True if the input was validated, otherwise WP_Error.
     681     */
     682    private function validate_custom_css( $css ) {
     683        if ( preg_match( '#</?\w+#', $css ) ) {
     684            return new WP_Error(
     685                'rest_custom_css_illegal_markup',
     686                __( 'Markup is not allowed in CSS.' ),
     687                array( 'status' => 400 )
     688            );
     689        }
     690        return true;
     691    }
    660692}
  • trunk/src/wp-includes/script-loader.php

    r55179 r55192  
    24562456
    24572457/**
     2458 * Enqueues the global styles custom css defined via theme.json.
     2459 *
     2460 * @since 6.2.0
     2461 */
     2462function wp_enqueue_global_styles_custom_css() {
     2463    if ( ! wp_is_block_theme() ) {
     2464        return;
     2465    }
     2466
     2467    // Don't enqueue Customizer's custom CSS separately.
     2468    remove_action( 'wp_head', 'wp_custom_css_cb', 101 );
     2469
     2470    $custom_css  = wp_get_custom_css();
     2471    $custom_css .= wp_get_global_styles_custom_css();
     2472
     2473    if ( ! empty( $custom_css ) ) {
     2474        wp_add_inline_style( 'global-styles', $custom_css );
     2475    }
     2476}
     2477
     2478/**
    24582479 * Renders the SVG filters supplied by theme.json.
    24592480 *
  • trunk/tests/phpunit/tests/rest-api/rest-global-styles-controller.php

    r55177 r55192  
    533533        }
    534534    }
     535
     536    /**
     537     * @covers WP_REST_Global_Styles_Controller::update_item
     538     * @ticket 57536
     539     */
     540    public function test_update_item_valid_styles_css() {
     541        wp_set_current_user( self::$admin_id );
     542        if ( is_multisite() ) {
     543            grant_super_admin( self::$admin_id );
     544        }
     545        $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
     546        $request->set_body_params(
     547            array(
     548                'styles' => array( 'css' => 'body { color: red; }' ),
     549            )
     550        );
     551        $response = rest_get_server()->dispatch( $request );
     552        $data     = $response->get_data();
     553        $this->assertSame( 'body { color: red; }', $data['styles']['css'] );
     554    }
     555
     556    /**
     557     * @covers WP_REST_Global_Styles_Controller::update_item
     558     * @ticket 57536
     559     */
     560    public function test_update_item_invalid_styles_css() {
     561        wp_set_current_user( self::$admin_id );
     562        if ( is_multisite() ) {
     563            grant_super_admin( self::$admin_id );
     564        }
     565        $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
     566        $request->set_body_params(
     567            array(
     568                'styles' => array( 'css' => '<p>test</p> body { color: red; }' ),
     569            )
     570        );
     571        $response = rest_get_server()->dispatch( $request );
     572        $this->assertErrorResponse( 'rest_custom_css_illegal_markup', $response, 400 );
     573    }
    535574}
  • trunk/tests/phpunit/tests/theme/wpThemeJson.php

    r55176 r55192  
    1414 */
    1515class Tests_Theme_wpThemeJson extends WP_UnitTestCase {
     16
     17    /**
     18     * Administrator ID.
     19     *
     20     * @var int
     21     */
     22    private static $administrator_id;
     23
     24    /**
     25     * User ID.
     26     *
     27     * @var int
     28     */
     29    private static $user_id;
     30
     31    public static function set_up_before_class() {
     32        parent::set_up_before_class();
     33
     34        static::$administrator_id = self::factory()->user->create(
     35            array(
     36                'role' => 'administrator',
     37            )
     38        );
     39
     40        if ( is_multisite() ) {
     41            grant_super_admin( self::$administrator_id );
     42        }
     43
     44        static::$user_id = self::factory()->user->create();
     45    }
    1646
    1747    /**
     
    45064536        $this->assertSame( $expected_styles, $theme_json->get_stylesheet() );
    45074537    }
     4538
     4539    /**
     4540     * @ticket 57536
     4541     */
     4542    public function test_get_custom_css_handles_global_custom_css() {
     4543        $theme_json = new WP_Theme_JSON(
     4544            array(
     4545                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     4546                'styles'  => array(
     4547                    'css' => 'body { color:purple; }',
     4548                ),
     4549            )
     4550        );
     4551        $custom_css = 'body { color:purple; }';
     4552        $this->assertSame( $custom_css, $theme_json->get_custom_css() );
     4553    }
     4554
     4555    /**
     4556     * Tests that custom CSS is kept for users with correct capabilities and removed for others.
     4557     *
     4558     * @ticket 57536
     4559     *
     4560     * @dataProvider data_custom_css_for_user_caps
     4561     *
     4562     * @param string $user_property The property name for current user.
     4563     * @param array  $expected      Expected results.
     4564     */
     4565    public function test_custom_css_for_user_caps( $user_property, array $expected ) {
     4566        wp_set_current_user( static::${$user_property} );
     4567
     4568        $actual = WP_Theme_JSON::remove_insecure_properties(
     4569            array(
     4570                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     4571                'styles'  => array(
     4572                    'css'    => 'body { color:purple; }',
     4573                    'blocks' => array(
     4574                        'core/separator' => array(
     4575                            'color' => array(
     4576                                'background' => 'blue',
     4577                            ),
     4578                        ),
     4579                    ),
     4580                ),
     4581            )
     4582        );
     4583
     4584        $this->assertSameSetsWithIndex( $expected, $actual );
     4585    }
     4586
     4587    /**
     4588     * Data provider.
     4589     *
     4590     * @return array[]
     4591     */
     4592    public function data_custom_css_for_user_caps() {
     4593        return array(
     4594            'allows custom css for users with caps'     => array(
     4595                'user_property' => 'administrator_id',
     4596                'expected'      => array(
     4597                    'version' => WP_Theme_JSON::LATEST_SCHEMA,
     4598                    'styles'  => array(
     4599                        'css'    => 'body { color:purple; }',
     4600                        'blocks' => array(
     4601                            'core/separator' => array(
     4602                                'color' => array(
     4603                                    'background' => 'blue',
     4604                                ),
     4605                            ),
     4606                        ),
     4607                    ),
     4608                ),
     4609            ),
     4610            'removes custom css for users without caps' => array(
     4611                'user_property' => 'user_id',
     4612                'expected'      => array(
     4613                    'version' => WP_Theme_JSON::LATEST_SCHEMA,
     4614                    'styles'  => array(
     4615                        'blocks' => array(
     4616                            'core/separator' => array(
     4617                                'color' => array(
     4618                                    'background' => 'blue',
     4619                                ),
     4620                            ),
     4621                        ),
     4622                    ),
     4623                ),
     4624            ),
     4625        );
     4626    }
    45084627}
Note: See TracChangeset for help on using the changeset viewer.