| @@ -1,131 +1,306 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Global styles functions, filters and actions, etc. | |
| 3 | + * Bootstraps Global Styles. | |
| 4 | 4 | * |
| 5 | 5 | * @package gutenberg |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | /** |
| 9 | - * Function that given a branch of the global styles object recursively generates | |
| 10 | - * an array defining all the css vars that the global styles object represents. | |
| 11 | - * | |
| 12 | - * @param array $global_styles_branch Array representing a brach of the global styles object. | |
| 13 | - * @param string $prefix Prefix to append to each variable. | |
| 14 | - * @param bool $is_start Indicates if we are on the first call to gutenberg_get_css_vars (outside the recursion). | |
| 15 | - * @return array An array whose keys are css variable names and whose values are the css variables value. | |
| 9 | + * Fetches the preferences for each origin (core, theme, user) | |
| 10 | + * and enqueues the resulting stylesheet. | |
| 16 | 11 | */ |
| 17 | -function gutenberg_get_css_vars( $global_styles_branch, $prefix = '', $is_start = true ) { | |
| 18 | - $result = array(); | |
| 19 | - foreach ( $global_styles_branch as $key => $value ) { | |
| 20 | - $processed_key = str_replace( '/', '-', $key ); | |
| 21 | - $separator = $is_start ? '' : '--'; | |
| 22 | - $new_key = ( $prefix ? $prefix . $separator : '' ) . $processed_key; | |
| 12 | +function gutenberg_experimental_global_styles_enqueue_assets() { | |
| 13 | + $stylesheet = wp_get_global_stylesheet(); | |
| 14 | + if ( empty( $stylesheet ) ) { | |
| 15 | + return; | |
| 16 | + } | |
| 23 | 17 | |
| 24 | - if ( is_array( $value ) ) { | |
| 25 | - $result = array_merge( | |
| 26 | - $result, | |
| 27 | - gutenberg_get_css_vars( $value, $new_key, false ) | |
| 28 | - ); | |
| 29 | - } else { | |
| 30 | - $result[ $new_key ] = $value; | |
| 31 | - } | |
| 18 | + if ( isset( wp_styles()->registered['global-styles'] ) ) { | |
| 19 | + wp_styles()->registered['global-styles']->extra['after'][0] = $stylesheet; | |
| 20 | + } else { | |
| 21 | + wp_register_style( 'global-styles', false, array(), true, true ); | |
| 22 | + wp_add_inline_style( 'global-styles', $stylesheet ); | |
| 23 | + wp_enqueue_style( 'global-styles' ); | |
| 32 | 24 | } |
| 33 | - return $result; | |
| 34 | 25 | } |
| 35 | 26 | |
| 36 | 27 | /** |
| 37 | - * Function responsible for enqueuing the style that define the global styles css variables. | |
| 28 | + * Adds the necessary settings for the Global Styles client UI. | |
| 29 | + * | |
| 30 | + * @param array $settings Existing block editor settings. | |
| 31 | + * | |
| 32 | + * @return array New block editor settings. | |
| 38 | 33 | */ |
| 39 | -function gutenberg_enqueue_global_styles_assets() { | |
| 40 | - if ( ! locate_template( 'experimental-theme.json' ) ) { | |
| 41 | - return; | |
| 34 | +function gutenberg_experimental_global_styles_settings( $settings ) { | |
| 35 | + // Set what is the context for this data request. | |
| 36 | + $context = 'other'; | |
| 37 | + if ( | |
| 38 | + is_callable( 'get_current_screen' ) && | |
| 39 | + function_exists( 'gutenberg_is_edit_site_page' ) && | |
| 40 | + gutenberg_is_edit_site_page( get_current_screen()->id ) | |
| 41 | + ) { | |
| 42 | + $context = 'site-editor'; | |
| 42 | 43 | } |
| 43 | - $default_global_styles_path = dirname( dirname( __FILE__ ) ) . '/experimental-default-global-styles.json'; | |
| 44 | - $default_global_styles = null; | |
| 45 | 44 | |
| 46 | - if ( file_exists( $default_global_styles_path ) ) { | |
| 47 | - $default_global_styles = json_decode( | |
| 48 | - file_get_contents( $default_global_styles_path ), | |
| 49 | - true | |
| 45 | + if ( | |
| 46 | + defined( 'REST_REQUEST' ) && | |
| 47 | + REST_REQUEST && | |
| 48 | + isset( $_GET['context'] ) && | |
| 49 | + 'mobile' === $_GET['context'] | |
| 50 | + ) { | |
| 51 | + $context = 'mobile'; | |
| 52 | + } | |
| 53 | + | |
| 54 | + if ( 'mobile' === $context && WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { | |
| 55 | + $settings['__experimentalStyles'] = wp_get_global_styles(); | |
| 56 | + } | |
| 57 | + | |
| 58 | + if ( 'other' === $context ) { | |
| 59 | + // Make sure the styles array exists. | |
| 60 | + // In some contexts, like the navigation editor, it doesn't. | |
| 61 | + if ( ! isset( $settings['styles'] ) ) { | |
| 62 | + $settings['styles'] = array(); | |
| 63 | + } | |
| 64 | + | |
| 65 | + // Reset existing global styles. | |
| 66 | + $styles_without_existing_global_styles = array(); | |
| 67 | + foreach ( $settings['styles'] as $style ) { | |
| 68 | + if ( ! isset( $style['__unstableType'] ) || 'globalStyles' !== $style['__unstableType'] ) { | |
| 69 | + $styles_without_existing_global_styles[] = $style; | |
| 70 | + } | |
| 71 | + } | |
| 72 | + | |
| 73 | + $new_global_styles = array(); | |
| 74 | + $new_presets = array( | |
| 75 | + array( | |
| 76 | + 'css' => 'variables', | |
| 77 | + '__unstableType' => 'presets', | |
| 78 | + '__experimentalNoWrapper' => true, | |
| 79 | + ), | |
| 80 | + array( | |
| 81 | + 'css' => 'presets', | |
| 82 | + '__unstableType' => 'presets', | |
| 83 | + ), | |
| 50 | 84 | ); |
| 85 | + foreach ( $new_presets as $new_style ) { | |
| 86 | + $style_css = wp_get_global_stylesheet( array( $new_style['css'] ) ); | |
| 87 | + if ( '' !== $style_css ) { | |
| 88 | + $new_style['css'] = $style_css; | |
| 89 | + $new_global_styles[] = $new_style; | |
| 90 | + } | |
| 91 | + } | |
| 92 | + | |
| 93 | + $new_block_classes = array( | |
| 94 | + 'css' => 'styles', | |
| 95 | + '__unstableType' => 'theme', | |
| 96 | + ); | |
| 97 | + if ( WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { | |
| 98 | + $style_css = wp_get_global_stylesheet( array( $new_block_classes['css'] ) ); | |
| 99 | + if ( '' !== $style_css ) { | |
| 100 | + $new_block_classes['css'] = $style_css; | |
| 101 | + $new_global_styles[] = $new_block_classes; | |
| 102 | + } | |
| 103 | + } | |
| 104 | + | |
| 105 | + $settings['styles'] = array_merge( $styles_without_existing_global_styles, $new_global_styles ); | |
| 51 | 106 | } |
| 52 | 107 | |
| 53 | - $theme_json_path = locate_template( 'experimental-theme.json' ); | |
| 54 | - $theme_json_global_styles = null; | |
| 55 | - if ( $theme_json_path ) { | |
| 56 | - $theme_json_global_styles = json_decode( | |
| 57 | - file_get_contents( $theme_json_path ), | |
| 58 | - true | |
| 59 | - ); | |
| 108 | + // Copied from get_block_editor_settings() at wordpress-develop/block-editor.php. | |
| 109 | + $settings['__experimentalFeatures'] = wp_get_global_settings(); | |
| 110 | + | |
| 111 | + if ( isset( $settings['__experimentalFeatures']['color']['palette'] ) ) { | |
| 112 | + $colors_by_origin = $settings['__experimentalFeatures']['color']['palette']; | |
| 113 | + $settings['colors'] = isset( $colors_by_origin['custom'] ) ? | |
| 114 | + $colors_by_origin['custom'] : ( | |
| 115 | + isset( $colors_by_origin['theme'] ) ? | |
| 116 | + $colors_by_origin['theme'] : | |
| 117 | + $colors_by_origin['default'] | |
| 118 | + ); | |
| 60 | 119 | } |
| 61 | 120 | |
| 62 | - // To-do: Load user customizations from a CPT. | |
| 63 | - $css_vars = array(); | |
| 64 | - foreach ( | |
| 65 | - array( | |
| 66 | - $default_global_styles, | |
| 67 | - $theme_json_global_styles, | |
| 68 | - ) as $global_styles_definition | |
| 69 | - ) { | |
| 70 | - if ( ! $global_styles_definition ) { | |
| 71 | - continue; | |
| 72 | - } | |
| 73 | - if ( isset( $global_styles_definition['global'] ) ) { | |
| 74 | - $css_vars = array_merge( | |
| 75 | - $css_vars, | |
| 76 | - gutenberg_get_css_vars( $global_styles_definition['global'], '--wp-' ) | |
| 121 | + if ( isset( $settings['__experimentalFeatures']['color']['gradients'] ) ) { | |
| 122 | + $gradients_by_origin = $settings['__experimentalFeatures']['color']['gradients']; | |
| 123 | + $settings['gradients'] = isset( $gradients_by_origin['custom'] ) ? | |
| 124 | + $gradients_by_origin['custom'] : ( | |
| 125 | + isset( $gradients_by_origin['theme'] ) ? | |
| 126 | + $gradients_by_origin['theme'] : | |
| 127 | + $gradients_by_origin['default'] | |
| 77 | 128 | ); |
| 78 | - } | |
| 79 | - if ( isset( $global_styles_definition['blocks'] ) ) { | |
| 80 | - $css_vars = array_merge( | |
| 81 | - $css_vars, | |
| 82 | - gutenberg_get_css_vars( $global_styles_definition['blocks'], '--wp-block-' ) | |
| 129 | + } | |
| 130 | + | |
| 131 | + if ( isset( $settings['__experimentalFeatures']['typography']['fontSizes'] ) ) { | |
| 132 | + $font_sizes_by_origin = $settings['__experimentalFeatures']['typography']['fontSizes']; | |
| 133 | + $settings['fontSizes'] = isset( $font_sizes_by_origin['custom'] ) ? | |
| 134 | + $font_sizes_by_origin['user'] : ( | |
| 135 | + isset( $font_sizes_by_origin['theme'] ) ? | |
| 136 | + $font_sizes_by_origin['theme'] : | |
| 137 | + $font_sizes_by_origin['default'] | |
| 83 | 138 | ); |
| 84 | - } | |
| 85 | 139 | } |
| 86 | 140 | |
| 87 | - if ( empty( $css_vars ) ) { | |
| 88 | - return; | |
| 141 | + if ( isset( $settings['__experimentalFeatures']['color']['custom'] ) ) { | |
| 142 | + $settings['disableCustomColors'] = ! $settings['__experimentalFeatures']['color']['custom']; | |
| 143 | + unset( $settings['__experimentalFeatures']['color']['custom'] ); | |
| 89 | 144 | } |
| 145 | + if ( isset( $settings['__experimentalFeatures']['color']['customGradient'] ) ) { | |
| 146 | + $settings['disableCustomGradients'] = ! $settings['__experimentalFeatures']['color']['customGradient']; | |
| 147 | + unset( $settings['__experimentalFeatures']['color']['customGradient'] ); | |
| 148 | + } | |
| 149 | + if ( isset( $settings['__experimentalFeatures']['typography']['customFontSize'] ) ) { | |
| 150 | + $settings['disableCustomFontSizes'] = ! $settings['__experimentalFeatures']['typography']['customFontSize']; | |
| 151 | + unset( $settings['__experimentalFeatures']['typography']['customFontSize'] ); | |
| 152 | + } | |
| 153 | + if ( isset( $settings['__experimentalFeatures']['typography']['lineHeight'] ) ) { | |
| 154 | + $settings['enableCustomLineHeight'] = $settings['__experimentalFeatures']['typography']['lineHeight']; | |
| 155 | + unset( $settings['__experimentalFeatures']['typography']['lineHeight'] ); | |
| 156 | + } | |
| 157 | + if ( isset( $settings['__experimentalFeatures']['spacing']['units'] ) ) { | |
| 158 | + $settings['enableCustomUnits'] = $settings['__experimentalFeatures']['spacing']['units']; | |
| 159 | + } | |
| 160 | + if ( isset( $settings['__experimentalFeatures']['spacing']['padding'] ) ) { | |
| 161 | + $settings['enableCustomSpacing'] = $settings['__experimentalFeatures']['spacing']['padding']; | |
| 162 | + unset( $settings['__experimentalFeatures']['spacing']['padding'] ); | |
| 163 | + } | |
| 90 | 164 | |
| 91 | - $inline_style = ":root {\n"; | |
| 92 | - foreach ( $css_vars as $var => $value ) { | |
| 93 | - $inline_style .= "\t" . $var . ': ' . $value . ";\n"; | |
| 165 | + return $settings; | |
| 166 | +} | |
| 167 | + | |
| 168 | +/** | |
| 169 | + * Whether or not the Site Editor is available. | |
| 170 | + * | |
| 171 | + * @return boolean | |
| 172 | + */ | |
| 173 | +function gutenberg_experimental_is_site_editor_available() { | |
| 174 | + return gutenberg_is_fse_theme(); | |
| 175 | +} | |
| 176 | + | |
| 177 | +/** | |
| 178 | + * Register CPT to store/access user data. | |
| 179 | + * | |
| 180 | + * @return void | |
| 181 | + */ | |
| 182 | +function gutenberg_experimental_global_styles_register_user_cpt() { | |
| 183 | + if ( gutenberg_experimental_is_site_editor_available() ) { | |
| 184 | + register_global_styles_custom_post_type(); | |
| 94 | 185 | } |
| 95 | - $inline_style .= '}'; | |
| 186 | +} | |
| 96 | 187 | |
| 97 | - wp_register_style( 'global-styles', false, array(), true, true ); | |
| 98 | - wp_add_inline_style( 'global-styles', $inline_style ); | |
| 99 | - wp_enqueue_style( 'global-styles' ); | |
| 188 | +/** | |
| 189 | + * Sanitizes global styles user content removing unsafe rules. | |
| 190 | + * | |
| 191 | + * @param string $content Post content to filter. | |
| 192 | + * @return string Filtered post content with unsafe rules removed. | |
| 193 | + */ | |
| 194 | +function gutenberg_global_styles_filter_post( $content ) { | |
| 195 | + $decoded_data = json_decode( wp_unslash( $content ), true ); | |
| 196 | + $json_decoding_error = json_last_error(); | |
| 197 | + if ( | |
| 198 | + JSON_ERROR_NONE === $json_decoding_error && | |
| 199 | + is_array( $decoded_data ) && | |
| 200 | + isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && | |
| 201 | + $decoded_data['isGlobalStylesUserThemeJSON'] | |
| 202 | + ) { | |
| 203 | + unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); | |
| 204 | + | |
| 205 | + $data_to_encode = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $decoded_data ); | |
| 206 | + | |
| 207 | + $data_to_encode['isGlobalStylesUserThemeJSON'] = true; | |
| 208 | + return wp_slash( wp_json_encode( $data_to_encode ) ); | |
| 209 | + } | |
| 210 | + return $content; | |
| 100 | 211 | } |
| 101 | -add_action( 'enqueue_block_assets', 'gutenberg_enqueue_global_styles_assets' ); | |
| 102 | 212 | |
| 103 | 213 | /** |
| 104 | - * Adds class wp-gs to the frontend body class if the theme defines a experimental-theme.json. | |
| 214 | + * Adds the filters to filter global styles user theme.json. | |
| 215 | + */ | |
| 216 | +function gutenberg_global_styles_kses_init_filters() { | |
| 217 | + add_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post' ); | |
| 218 | +} | |
| 219 | + | |
| 220 | +/** | |
| 221 | + * Removes the filters to filter global styles user theme.json. | |
| 222 | + */ | |
| 223 | +function gutenberg_global_styles_kses_remove_filters() { | |
| 224 | + remove_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post' ); | |
| 225 | +} | |
| 226 | + | |
| 227 | +/** | |
| 228 | + * Register global styles kses filters if the user does not have unfiltered_html capability. | |
| 105 | 229 | * |
| 106 | - * @param array $classes Existing body classes. | |
| 107 | - * @return array The filtered array of body classes. | |
| 230 | + * @uses render_block_core_navigation() | |
| 231 | + * @throws WP_Error An WP_Error exception parsing the block definition. | |
| 108 | 232 | */ |
| 109 | -function gutenberg_add_wp_gs_class_front_end( $classes ) { | |
| 110 | - if ( locate_template( 'experimental-theme.json' ) ) { | |
| 111 | - return array_merge( $classes, array( 'wp-gs' ) ); | |
| 233 | +function gutenberg_global_styles_kses_init() { | |
| 234 | + gutenberg_global_styles_kses_remove_filters(); | |
| 235 | + if ( ! current_user_can( 'unfiltered_html' ) ) { | |
| 236 | + gutenberg_global_styles_kses_init_filters(); | |
| 112 | 237 | } |
| 113 | - return $classes; | |
| 114 | 238 | } |
| 115 | -add_filter( 'body_class', 'gutenberg_add_wp_gs_class_front_end' ); | |
| 116 | 239 | |
| 240 | +/** | |
| 241 | + * This filter is the last being executed on force_filtered_html_on_import. | |
| 242 | + * If the input of the filter is true it means we are in an import situation and should | |
| 243 | + * enable kses, independently of the user capabilities. | |
| 244 | + * So in that case we call gutenberg_global_styles_kses_init_filters; | |
| 245 | + * | |
| 246 | + * @param string $arg Input argument of the filter. | |
| 247 | + * @return string Exactly what was passed as argument. | |
| 248 | + */ | |
| 249 | +function gutenberg_global_styles_force_filtered_html_on_import_filter( $arg ) { | |
| 250 | + // force_filtered_html_on_import is true we need to init the global styles kses filters. | |
| 251 | + if ( $arg ) { | |
| 252 | + gutenberg_global_styles_kses_init_filters(); | |
| 253 | + } | |
| 254 | + return $arg; | |
| 255 | +} | |
| 117 | 256 | |
| 257 | +// TODO: Remove this filter when minimum supported version is WP 5.8 | |
| 258 | +// As all this code is not needed anymore now core supports all variables. | |
| 118 | 259 | /** |
| 119 | - * Adds class wp-gs to the block-editor body class if the theme defines a experimental-theme.json. | |
| 260 | + * This filter is the last being executed on force_filtered_html_on_import. | |
| 261 | + * If the input of the filter is true it means we are in an import situation and should | |
| 262 | + * enable kses, independently of the user capabilities. | |
| 263 | + * So in that case we call gutenberg_global_styles_kses_init_filters; | |
| 120 | 264 | * |
| 121 | - * @param string $classes Existing body classes separated by space. | |
| 122 | - * @return string The filtered string of body classes. | |
| 265 | + * @param bool $allow_css Whether the CSS in the test string is considered safe. | |
| 266 | + * @param bool $css_test_string The CSS string to test.. | |
| 267 | + * @return bool If $allow_css is true it returns true. | |
| 268 | + * If $allow_css is false and the CSS rule is referencing a WordPress css variable it returns true. | |
| 269 | + * Otherwise the function return false. | |
| 123 | 270 | */ |
| 124 | -function gutenberg_add_wp_gs_class_editor( $classes ) { | |
| 125 | - global $current_screen; | |
| 126 | - if ( $current_screen->is_block_editor() && locate_template( 'experimental-theme.json' ) ) { | |
| 127 | - return $classes . ' wp-gs'; | |
| 271 | +function gutenberg_global_styles_include_support_for_wp_variables( $allow_css, $css_test_string ) { | |
| 272 | + if ( $allow_css ) { | |
| 273 | + return $allow_css; | |
| 128 | 274 | } |
| 129 | - return $classes; | |
| 275 | + $allowed_preset_attributes = array( | |
| 276 | + 'background', | |
| 277 | + 'background-color', | |
| 278 | + 'border-color', | |
| 279 | + 'color', | |
| 280 | + 'font-family', | |
| 281 | + 'font-size', | |
| 282 | + ); | |
| 283 | + $parts = explode( ':', $css_test_string, 2 ); | |
| 284 | + | |
| 285 | + if ( ! in_array( trim( $parts[0] ), $allowed_preset_attributes, true ) ) { | |
| 286 | + return $allow_css; | |
| 287 | + } | |
| 288 | + return ! ! preg_match( '/^var\(--wp-[a-zA-Z0-9\-]+\)$/', trim( $parts[1] ) ); | |
| 130 | 289 | } |
| 131 | -add_filter( 'admin_body_class', 'gutenberg_add_wp_gs_class_editor' ); | |
| 290 | + | |
| 291 | +// The else clause can be removed when plugin support requires WordPress 5.8.0+. | |
| 292 | +if ( function_exists( 'get_block_editor_settings' ) ) { | |
| 293 | + add_filter( 'block_editor_settings_all', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX ); | |
| 294 | +} else { | |
| 295 | + add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX ); | |
| 296 | +} | |
| 297 | + | |
| 298 | +add_action( 'init', 'gutenberg_experimental_global_styles_register_user_cpt' ); | |
| 299 | +add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' ); | |
| 300 | + | |
| 301 | +// kses actions&filters. | |
| 302 | +add_action( 'init', 'gutenberg_global_styles_kses_init' ); | |
| 303 | +add_action( 'set_current_user', 'gutenberg_global_styles_kses_init' ); | |
| 304 | +add_filter( 'force_filtered_html_on_import', 'gutenberg_global_styles_force_filtered_html_on_import_filter', 999 ); | |
| 305 | +add_filter( 'safecss_filter_attr_allow_css', 'gutenberg_global_styles_include_support_for_wp_variables', 10, 2 ); | |
| 306 | +// This filter needs to be executed last. | |