PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/block-supports/custom-css.php +214 -23 23.0.1 → trunk View file →
@@ -11,19 +11,30 @@
11 11 * @since 7.0.0
12 12 *
13 13 * @param array $parsed_block The parsed block.
14 14 * @return array The same parsed block with custom CSS class name added if appropriate.
15 + *
16 + * @phpstan-param array{
17 + * blockName: string|null,
18 + * attrs: array{
19 + * className?: string,
20 + * style?: array{
21 + * css?: string,
22 + * ...
23 + * },
24 + * ...
25 + * },
26 + * ...
27 + * } $parsed_block
15 28 */
16 29 function gutenberg_render_custom_css_support_styles( $parsed_block ) {
17 - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );
18 -
19 - if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
30 + $custom_css = $parsed_block['attrs']['style']['css'] ?? null;
31 + if ( ! is_string( $custom_css ) || '' === trim( $custom_css ) ) {
20 32 return $parsed_block;
21 33 }
22 34
23 - $custom_css = trim( $parsed_block['attrs']['style']['css'] ?? '' );
24 -
25 - if ( empty( $custom_css ) ) {
35 + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );
36 + if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
26 37 return $parsed_block;
27 38 }
28 39
29 40 // Validate CSS doesn't contain HTML markup (same validation as global styles REST API).
@@ -31,14 +42,15 @@
31 42 return $parsed_block;
32 43 }
33 44
34 45 // Generate a unique class name for this block instance.
35 - $class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
36 - $updated_class_name = isset( $parsed_block['attrs']['className'] )
37 - ? $parsed_block['attrs']['className'] . " $class_name"
46 + $class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
47 + $existing_class_name = $parsed_block['attrs']['className'] ?? null;
48 + $updated_class_name = is_string( $existing_class_name )
49 + ? "$existing_class_name $class_name"
38 50 : $class_name;
39 51
40 - _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name );
52 + $parsed_block['attrs']['className'] = $updated_class_name;
41 53
42 54 // Process the custom CSS using the same method as global styles.
43 55 $selector = '.' . $class_name;
44 56 $processed_css = WP_Theme_JSON_Gutenberg::process_blocks_custom_css( $custom_css, $selector );
@@ -43,15 +55,25 @@
43 55 $selector = '.' . $class_name;
44 56 $processed_css = WP_Theme_JSON_Gutenberg::process_blocks_custom_css( $custom_css, $selector );
45 57
46 58 if ( ! empty( $processed_css ) ) {
47 - /*
48 - * Register and add inline style for block custom CSS.
49 - * The style depends on global-styles to ensure custom CSS loads after
50 - * and can override global styles.
59 + /**
60 + * Reuse one handle so identical custom CSS is enqueued only once via
61 + * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
62 + * dependency so `global-styles` is guaranteed to print after it, preventing
63 + * block default styles from unintentionally overriding global styles.
51 64 */
52 - wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) );
53 - wp_add_inline_style( 'wp-block-custom-css', $processed_css );
65 + $handle = 'wp-block-custom-css';
66 + if ( ! wp_style_is( $handle, 'registered' ) ) {
67 + wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
68 + }
69 + $after_styles = wp_styles()->get_data( $handle, 'after' );
70 + if ( ! is_array( $after_styles ) ) {
71 + $after_styles = array();
72 + }
73 + if ( ! in_array( $processed_css, $after_styles, true ) ) {
74 + wp_add_inline_style( $handle, $processed_css );
75 + }
54 76 }
55 77
56 78 return $parsed_block;
57 79 }
@@ -67,9 +89,9 @@
67 89
68 90 /**
69 91 * Applies the custom CSS class name to the block's rendered HTML.
70 92 *
71 - * The class name is generated in `gutenberg_render_custom_css_support_styles`
93 + * The class name is generated in {@see gutenberg_render_custom_css_support_styles()}
72 94 * and stored in block attributes. This filter adds it to the actual markup.
73 95 *
74 96 * @since 7.0.0
75 97 *
@@ -74,23 +96,44 @@
74 96 * @since 7.0.0
75 97 *
76 98 * @param string $block_content Rendered block content.
77 99 * @param array $block Block object.
78 - * @return string Filtered block content.
100 + * @return string Filtered block content.
101 + *
102 + * @phpstan-param array{
103 + * attrs: array{
104 + * className?: string,
105 + * ...
106 + * },
107 + * ...
108 + * } $block
79 109 */
80 110 function gutenberg_render_custom_css_class_name( $block_content, $block ) {
81 - $class_string = $block['attrs']['className'] ?? '';
82 - preg_match( '/\bwp-custom-css-\S+\b/', $class_string, $matches );
111 + $class_name_attr = $block['attrs']['className'] ?? null;
112 + $class_name_prefix = 'wp-custom-css-';
113 + if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, $class_name_prefix ) ) {
114 + return $block_content;
115 + }
83 116
84 - if ( empty( $matches ) ) {
117 + // Parse out the 'wp-custom-css-*' class name added by gutenberg_render_custom_css_support_styles().
118 + $matched_class_name = null;
119 + $token_delimiter = " \t\f\r\n";
120 + $class_token = strtok( $class_name_attr, $token_delimiter );
121 + while ( false !== $class_token ) {
122 + if ( str_starts_with( $class_token, $class_name_prefix ) ) {
123 + $matched_class_name = $class_token;
124 + break;
125 + }
126 + $class_token = strtok( $token_delimiter );
127 + }
128 + if ( null === $matched_class_name ) {
85 129 return $block_content;
86 130 }
87 131
88 132 $tags = new WP_HTML_Tag_Processor( $block_content );
89 -
90 133 if ( $tags->next_tag() ) {
91 134 $tags->add_class( 'has-custom-css' );
92 - $tags->add_class( $matches[0] );
135 + $tags->add_class( $matched_class_name );
93 136 }
94 137
95 138 return $tags->get_updated_html();
96 139 }
@@ -113,8 +156,10 @@
113 156
114 157 /**
115 158 * Registers the style block attribute for block types that support it.
116 159 *
160 + * @since 7.0.0
161 + *
117 162 * @param WP_Block_Type $block_type Block Type.
118 163 */
119 164 function gutenberg_register_custom_css_support( $block_type ) {
120 165 // Setup attributes and styles within that if needed.
@@ -134,8 +179,154 @@
134 179 'type' => 'object',
135 180 );
136 181 }
137 182 }
183 +
184 +/**
185 + * Strips `style.css` attributes from all blocks in post content.
186 + *
187 + * Uses WP_Block_Parser::next_token() to scan block tokens and surgically
188 + * replace only the attribute JSON that changed — no parse_blocks() +
189 + * serialize_blocks() round-trip needed.
190 + *
191 + * @since 7.0.0
192 + *
193 + * @param string $content Post content to filter, expected to be escaped with slashes.
194 + * @return string Filtered post content with block custom CSS removed.
195 + */
196 +function gutenberg_strip_custom_css_from_blocks( $content ) {
197 + if ( ! has_blocks( $content ) ) {
198 + return $content;
199 + }
200 +
201 + $unslashed = stripslashes( $content );
202 +
203 + $parser = new WP_Block_Parser();
204 + $parser->document = $unslashed;
205 + $parser->offset = 0;
206 + $end = strlen( $unslashed );
207 + $replacements = array();
208 +
209 + while ( $parser->offset < $end ) {
210 + $next_token = $parser->next_token();
211 + list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token;
212 +
213 + if ( 'no-more-tokens' === $token_type ) {
214 + break;
215 + }
216 +
217 + $parser->offset = $start_offset + $token_length;
218 +
219 + if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) {
220 + continue;
221 + }
222 +
223 + if ( ! isset( $attrs['style']['css'] ) ) {
224 + continue;
225 + }
226 +
227 + // Remove css and clean up empty style.
228 + unset( $attrs['style']['css'] );
229 + if ( empty( $attrs['style'] ) ) {
230 + unset( $attrs['style'] );
231 + }
232 +
233 + // Locate the JSON portion within the token.
234 + $token_string = substr( $unslashed, $start_offset, $token_length );
235 + $json_rel_start = strcspn( $token_string, '{' );
236 + $json_rel_end = strrpos( $token_string, '}' );
237 +
238 + $json_start = $start_offset + $json_rel_start;
239 + $json_length = $json_rel_end - $json_rel_start + 1;
240 +
241 + // Re-encode attributes. If attrs is now empty, remove JSON and trailing space.
242 + if ( empty( $attrs ) ) {
243 + // Remove the trailing space after JSON: `{"style":{"css":"x"}} ` → ``
244 + $replacements[] = array( $json_start, $json_length + 1, '' );
245 + } else {
246 + $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) );
247 + }
248 + }
249 +
250 + if ( empty( $replacements ) ) {
251 + return $content;
252 + }
253 +
254 + // Build the result by splicing replacements into the original string.
255 + $result = '';
256 + $was_at = 0;
257 +
258 + foreach ( $replacements as $replacement ) {
259 + list( $offset, $length, $new_json ) = $replacement;
260 + $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json;
261 + $was_at = $offset + $length;
262 + }
263 +
264 + if ( $was_at < $end ) {
265 + $result .= substr( $unslashed, $was_at );
266 + }
267 +
268 + return addslashes( $result );
269 +}
270 +
271 +/**
272 + * Adds the filters to strip custom CSS from block content on save.
273 + *
274 + * @since 7.0.0
275 + * @access private
276 + */
277 +function gutenberg_custom_css_kses_init_filters() {
278 + add_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
279 + add_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
280 +}
281 +
282 +/**
283 + * Removes the filters that strip custom CSS from block content on save.
284 + *
285 + * @since 7.0.0
286 + * @access private
287 + */
288 +function gutenberg_custom_css_remove_filters() {
289 + remove_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
290 + remove_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
291 +}
292 +
293 +/**
294 + * Registers the custom CSS content filters if the user does not have the edit_css capability.
295 + *
296 + * @since 7.0.0
297 + * @access private
298 + */
299 +function gutenberg_custom_css_kses_init() {
300 + gutenberg_custom_css_remove_filters();
301 + if ( ! current_user_can( 'edit_css' ) ) {
302 + gutenberg_custom_css_kses_init_filters();
303 + }
304 +}
305 +
306 +/**
307 + * Initializes custom CSS content filters when imported data should be filtered.
308 + *
309 + * This filter is the last being executed on force_filtered_html_on_import.
310 + * If the input of the filter is true it means we are in an import situation and should
311 + * enable the custom CSS filters, independently of the user capabilities.
312 + *
313 + * @since 7.0.0
314 + * @access private
315 + *
316 + * @param mixed $arg Input argument of the filter.
317 + * @return mixed Input argument of the filter.
318 + */
319 +function gutenberg_custom_css_force_filtered_html_on_import_filter( $arg ) {
320 + if ( $arg ) {
321 + gutenberg_custom_css_kses_init_filters();
322 + }
323 + return $arg;
324 +}
325 +
326 +add_action( 'init', 'gutenberg_custom_css_kses_init', 20 );
327 +add_action( 'set_current_user', 'gutenberg_custom_css_kses_init' );
328 +add_filter( 'force_filtered_html_on_import', 'gutenberg_custom_css_force_filtered_html_on_import_filter', 999 );
138 329
139 330 // Register the block support.
140 331 WP_Block_Supports::get_instance()->register(
141 332 'custom-css',