PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
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 → 24.1.0 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',