Skip to content

Commit

Permalink
Global styles: add custom CSS panel to site editor (#46141)
Browse files Browse the repository at this point in the history
  • Loading branch information
glendaviesnz authored Dec 13, 2022
1 parent 72cb08b commit 436666d
Show file tree
Hide file tree
Showing 15 changed files with 591 additions and 5 deletions.
28 changes: 28 additions & 0 deletions lib/compat/wordpress-6.2/block-editor-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Adds settings to the block editor.
*
* @package gutenberg
*/

/**
* Adds styles and __experimentalFeatures to the block editor settings.
*
* @param array $settings Existing block editor settings.
*
* @return array New block editor settings.
*/
function gutenberg_get_block_editor_settings_6_2( $settings ) {
if ( wp_theme_has_theme_json() ) {
// Add the custom CSS as separate style sheet so any invalid CSS entered by users does not break other global styles.
$settings['styles'][] = array(
'css' => gutenberg_get_global_stylesheet( array( 'custom-css' ) ),
'__unstableType' => 'user',
'isGlobalStyles' => true,
);
}

return $settings;
}

add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_6_2', PHP_INT_MAX );
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php
/**
* REST API: Gutenberg_REST_Global_Styles_Controller class
*
* @package Gutenberg
* @subpackage REST_API
*/

/**
* Base Global Styles REST API Controller.
*/
class Gutenberg_REST_Global_Styles_Controller_6_2 extends WP_REST_Global_Styles_Controller {
/**
* Registers the controllers routes.
*
* @return void
*/
public function register_routes() {
parent::register_routes();
}

/**
* Prepare a global styles config output for response.
*
* @since 5.9.0
* @since 6.2 Handling of style.css was added to WP_Theme_JSON.
*
* @param WP_Post $post Global Styles post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$raw_config = json_decode( $post->post_content, true );
$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
$config = array();
if ( $is_global_styles_user_theme_json ) {
$config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data();
}

// Base fields for every post.
$data = array();
$fields = $this->get_fields_for_response( $request );

if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $post->ID;
}

if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $post->post_title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );

$data['title']['rendered'] = get_the_title( $post->ID );

remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}

if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
}

if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

// Wrap the data in a response object.
$response = rest_ensure_response( $data );

if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $post->ID );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions();
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}

return $response;
}

/**
* Updates a single global style config.
*
* @since 5.9.0
* @since 6.2.0 Added validation of styles.css property.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$post_before = $this->get_post( $request['id'] );
if ( is_wp_error( $post_before ) ) {
return $post_before;
}

$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;
}

$post = get_post( $request['id'] );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}

wp_after_insert_post( $post, true, $post_before );

$response = $this->prepare_item_for_response( $post, $request );

return rest_ensure_response( $response );
}
/**
* 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.
*/
protected function prepare_item_for_database( $request ) {
$changes = new stdClass();
$changes->ID = $request['id'];
$post = get_post( $request['id'] );
$existing_config = array();
if ( $post ) {
$existing_config = json_decode( $post->post_content, true );
$json_decoding_error = json_last_error();
if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
! $existing_config['isGlobalStylesUserThemeJSON'] ) {
$existing_config = array();
}
}
if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
$config = array();
if ( isset( $request['styles'] ) ) {
$config['styles'] = $request['styles'];
$validate_custom_css = $this->validate_custom_css( $request['styles']['css'] );
if ( is_wp_error( $validate_custom_css ) ) {
return $validate_custom_css;
}
} elseif ( isset( $existing_config['styles'] ) ) {
$config['styles'] = $existing_config['styles'];
}
if ( isset( $request['settings'] ) ) {
$config['settings'] = $request['settings'];
} elseif ( isset( $existing_config['settings'] ) ) {
$config['settings'] = $existing_config['settings'];
}
$config['isGlobalStylesUserThemeJSON'] = true;
$config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA;
$changes->post_content = wp_json_encode( $config );
}
// Post title.
if ( isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$changes->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$changes->post_title = $request['title']['raw'];
}
}
return $changes;
}

/**
* 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.', 'gutenberg' ),
array( 'status' => 400 )
);
}
return true;
}
}
111 changes: 111 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 @@ -194,6 +194,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 {
* @since 6.1.0 Added new side properties for `border`,
* added new property `shadow`,
* updated `blockGap` to be allowed at any level.
* @since 6.2.0 Added new property `css`.
* @var array
*/
const VALID_STYLES = array(
Expand Down Expand Up @@ -234,6 +235,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 {
'textDecoration' => null,
'textTransform' => null,
),
'css' => null,
);

/**
Expand Down Expand Up @@ -277,4 +279,113 @@ protected static function remove_insecure_styles( $input ) {

return $output;
}

/**
* Returns the stylesheet that results of processing
* the theme.json structure this object represents.
*
* @param array $types Types of styles to load. Will load all by default. It accepts:
* 'variables': only the CSS Custom Properties for presets & custom ones.
* 'styles': only the styles section in theme.json.
* 'presets': only the classes for the presets.
* 'custom-css': only the css from global styles.css.
* @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
* @param array $options An array of options for now used for internal purposes only (may change without notice).
* The options currently supported are 'scope' that makes sure all style are scoped to a given selector,
* and root_selector which overwrites and forces a given selector to be used on the root node.
* @return string Stylesheet.
*/
public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
if ( null === $origins ) {
$origins = static::VALID_ORIGINS;
}

if ( is_string( $types ) ) {
// Dispatch error and map old arguments to new ones.
_deprecated_argument( __FUNCTION__, '5.9' );
if ( 'block_styles' === $types ) {
$types = array( 'styles', 'presets' );
} elseif ( 'css_variables' === $types ) {
$types = array( 'variables' );
} else {
$types = array( 'variables', 'styles', 'presets' );
}
}

$blocks_metadata = static::get_blocks_metadata();
$style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );

$root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
$root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );

if ( ! empty( $options['scope'] ) ) {
foreach ( $setting_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
}
foreach ( $style_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
}
}

if ( ! empty( $options['root_selector'] ) ) {
if ( false !== $root_settings_key ) {
$setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
}
if ( false !== $root_style_key ) {
$setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
}
}

$stylesheet = '';

if ( in_array( 'variables', $types, true ) ) {
$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
}

if ( in_array( 'styles', $types, true ) ) {
if ( false !== $root_style_key ) {
$stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
}
$stylesheet .= $this->get_block_classes( $style_nodes );
} elseif ( in_array( 'base-layout-styles', $types, true ) ) {
$root_selector = static::ROOT_BLOCK_SELECTOR;
$columns_selector = '.wp-block-columns';
if ( ! empty( $options['scope'] ) ) {
$root_selector = static::scope_selector( $options['scope'], $root_selector );
$columns_selector = static::scope_selector( $options['scope'], $columns_selector );
}
if ( ! empty( $options['root_selector'] ) ) {
$root_selector = $options['root_selector'];
}
// Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
// For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
$base_styles_nodes = array(
array(
'path' => array( 'styles' ),
'selector' => $root_selector,
),
array(
'path' => array( 'styles', 'blocks', 'core/columns' ),
'selector' => $columns_selector,
'name' => 'core/columns',
),
);

foreach ( $base_styles_nodes as $base_style_node ) {
$stylesheet .= $this->get_layout_styles( $base_style_node );
}
}

if ( in_array( 'presets', $types, true ) ) {
$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 ) ) {
$stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
}

return $stylesheet;
}
}
Loading

0 comments on commit 436666d

Please sign in to comment.