| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Custom CSS block support. 4 * 5 * @package WordPress 6 */ 7 8 /** 9 * Checks whether a style node has a non-empty custom CSS string. 10 * 11 * @since 7.2.0 12 * @access private 13 * 14 * @param mixed $style_node A style node, possibly containing a `css` key. 15 * @return bool Whether the node has a non-empty custom CSS string. 16 */ 17 function wp_custom_css_has_value( $style_node ) { 18 return is_array( $style_node ) 19 && isset( $style_node['css'] ) 20 && is_string( $style_node['css'] ) 21 && '' !== trim( $style_node['css'] ); 22 } 23 24 /** 25 * Collects custom CSS defined for a block instance across its default, 26 * pseudo-selector, and viewport states. 27 * 28 * @since 7.2.0 29 * @access private 30 * 31 * @param array $style The block's `style` attribute. 32 * @param string $block_name Block name. 33 * @param array $responsive_media_queries Media queries keyed by viewport state name. 34 * @return array[] Custom CSS entries with `css`, `pseudo`, and `media_query` keys. 35 */ 36 function wp_get_custom_css_state_entries( $style, $block_name, $responsive_media_queries ) { 37 $entries = array(); 38 if ( empty( $style ) || ! is_array( $style ) ) { 39 return $entries; 40 } 41 42 $supported_pseudo_states = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array(); 43 44 if ( wp_custom_css_has_value( $style ) ) { 45 $entries[] = array( 46 'css' => $style['css'], 47 'pseudo' => null, 48 'media_query' => null, 49 ); 50 } 51 52 foreach ( $supported_pseudo_states as $pseudo_state ) { 53 $pseudo_style = $style[ $pseudo_state ] ?? null; 54 if ( wp_custom_css_has_value( $pseudo_style ) ) { 55 $entries[] = array( 56 'css' => $pseudo_style['css'], 57 'pseudo' => $pseudo_state, 58 'media_query' => null, 59 ); 60 } 61 } 62 63 foreach ( $responsive_media_queries as $breakpoint => $media_query ) { 64 $breakpoint_style = $style[ $breakpoint ] ?? null; 65 if ( ! is_array( $breakpoint_style ) ) { 66 continue; 67 } 68 69 if ( wp_custom_css_has_value( $breakpoint_style ) ) { 70 $entries[] = array( 71 'css' => $breakpoint_style['css'], 72 'pseudo' => null, 73 'media_query' => $media_query, 74 ); 75 } 76 77 foreach ( $supported_pseudo_states as $pseudo_state ) { 78 $breakpoint_pseudo_style = $breakpoint_style[ $pseudo_state ] ?? null; 79 if ( wp_custom_css_has_value( $breakpoint_pseudo_style ) ) { 80 $entries[] = array( 81 'css' => $breakpoint_pseudo_style['css'], 82 'pseudo' => $pseudo_state, 83 'media_query' => $media_query, 84 ); 85 } 86 } 87 } 88 89 return $entries; 90 } 91 92 /** 93 * Validates and processes custom CSS for each block style state. 94 * 95 * @since 7.2.0 96 * @access private 97 * 98 * @param array[] $state_entries Custom CSS entries. 99 * @param string $selector Selector scoping the block instance. 100 * @return string Processed CSS. 101 */ 102 function wp_process_custom_css_state_entries( $state_entries, $selector ) { 103 $processed_css = ''; 104 105 foreach ( $state_entries as $entry ) { 106 // Validate CSS doesn't contain HTML markup (same validation as global styles REST API). 107 if ( preg_match( '#</?\w+#', $entry['css'] ) ) { 108 continue; 109 } 110 111 $entry_selector = null !== $entry['pseudo'] ? $selector . $entry['pseudo'] : $selector; 112 $entry_css = WP_Theme_JSON::process_blocks_custom_css( $entry['css'], $entry_selector ); 113 114 if ( empty( $entry_css ) ) { 115 continue; 116 } 117 118 $processed_css .= null !== $entry['media_query'] 119 ? $entry['media_query'] . '{' . $entry_css . '}' 120 : $entry_css; 121 } 122 123 return $processed_css; 124 } 125 126 /** 127 * Generates the custom CSS class name for a block and registers its stylesheet. 128 * 129 * @since 7.2.0 130 * @access private 131 * 132 * @param array $parsed_block The parsed block. 133 * @return string|null The generated class name, or null if the block has no custom CSS to render. 134 * 135 * @phpstan-param array{ 136 * blockName: string|null, 137 * attrs: array{ 138 * className?: string, 139 * style?: array{ 140 * css?: string, 141 * ... 142 * }, 143 * ... 144 * }, 145 * ... 146 * } $parsed_block 147 */ 148 function wp_generate_custom_css_class_name( $parsed_block ) { 149 $style = $parsed_block['attrs']['style'] ?? null; 150 if ( empty( $style ) || ! is_array( $style ) ) { 151 return null; 152 } 153 154 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] ); 155 if ( ! block_has_support( $block_type, 'customCSS', true ) ) { 156 return null; 157 } 158 159 $viewport_settings = wp_get_global_settings( array( 'viewport' ) ); 160 $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings ); 161 $state_entries = wp_get_custom_css_state_entries( $style, $parsed_block['blockName'], $responsive_media_queries ); 162 if ( empty( $state_entries ) ) { 163 return null; 164 } 165 166 // Generate a unique class name for this block instance. 167 $class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' ); 168 $processed_css = wp_process_custom_css_state_entries( $state_entries, '.' . $class_name ); 169 if ( '' === $processed_css ) { 170 return null; 171 } 172 173 /** 174 * Reuse one handle so identical custom CSS is enqueued only once via 175 * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library` 176 * dependency so `global-styles` is guaranteed to print after it, preventing 177 * block default styles from unintentionally overriding global styles. 178 */ 179 $handle = 'wp-block-custom-css'; 180 if ( ! wp_style_is( $handle, 'registered' ) ) { 181 wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) ); 182 } 183 $after_styles = wp_styles()->get_data( $handle, 'after' ); 184 if ( ! is_array( $after_styles ) ) { 185 $after_styles = array(); 186 } 187 if ( ! in_array( $processed_css, $after_styles, true ) ) { 188 wp_add_inline_style( $handle, $processed_css ); 189 } 190 191 return $class_name; 192 } 193 194 /** 195 * Render the custom CSS stylesheet and add class name to block as required. 196 * 197 * @since 7.0.0 198 * 199 * @param array $parsed_block The parsed block. 200 * @return array The same parsed block with custom CSS class name added if appropriate. 201 * 202 * @phpstan-param array{ 203 * blockName: string|null, 204 * attrs: array{ 205 * className?: string, 206 * style?: array{ 207 * css?: string, 208 * ... 209 * }, 210 * ... 211 * }, 212 * ... 213 * } $parsed_block 214 * @phpstan-return array{ 215 * blockName: string|null, 216 * attrs: array{ 217 * className?: string, 218 * style?: array{ 219 * css?: string, 220 * ... 221 * }, 222 * ... 223 * }, 224 * ... 225 * } 226 */ 227 function wp_render_custom_css_support_styles( $parsed_block ) { 228 $class_name = wp_generate_custom_css_class_name( $parsed_block ); 229 if ( null === $class_name ) { 230 return $parsed_block; 231 } 232 233 $existing_class_name = $parsed_block['attrs']['className'] ?? null; 234 $updated_class_name = is_string( $existing_class_name ) 235 ? "$existing_class_name $class_name" 236 : $class_name; 237 238 $parsed_block['attrs']['className'] = $updated_class_name; 239 240 return $parsed_block; 241 } 242 243 /** 244 * Enqueues the block custom CSS styles. 245 * 246 * @since 7.0.0 247 */ 248 function wp_enqueue_block_custom_css() { 249 wp_enqueue_style( 'wp-block-custom-css' ); 250 } 251 252 /** 253 * Applies the custom CSS class name to the block's rendered HTML. 254 * 255 * Uses the class name that {@see wp_render_custom_css_support_styles()} 256 * stored in the block attributes. Blocks that a parent parsed and rendered 257 * itself never reach that filter, so the class name is generated here instead 258 * via {@see wp_generate_custom_css_class_name()}. 259 * 260 * @since 7.0.0 261 * @since 7.2.0 Generates the class name when the block attributes lack one. 262 * 263 * @param string $block_content Rendered block content. 264 * @param array $block Block object. 265 * @return string Filtered block content. 266 * 267 * @phpstan-param array{ 268 * blockName: string|null, 269 * attrs: array{ 270 * className?: string, 271 * style?: array{ 272 * css?: string, 273 * ... 274 * }, 275 * ... 276 * }, 277 * ... 278 * } $block 279 */ 280 function wp_render_custom_css_class_name( $block_content, $block ) { 281 $class_name_attr = $block['attrs']['className'] ?? null; 282 $class_name_prefix = 'wp-custom-css-'; 283 $matched_class_name = null; 284 285 // Parse out the 'wp-custom-css-*' class name added by wp_render_custom_css_support_styles(). 286 if ( is_string( $class_name_attr ) && str_contains( $class_name_attr, $class_name_prefix ) ) { 287 $token_delimiter = " \t\f\r\n"; 288 $class_token = strtok( $class_name_attr, $token_delimiter ); 289 while ( false !== $class_token ) { 290 if ( str_starts_with( $class_token, $class_name_prefix ) ) { 291 $matched_class_name = $class_token; 292 break; 293 } 294 $class_token = strtok( $token_delimiter ); 295 } 296 } 297 298 // Direct WP_Block::render() calls skip render_block_data for the block itself. 299 if ( null === $matched_class_name ) { 300 $matched_class_name = wp_generate_custom_css_class_name( $block ); 301 } 302 303 if ( null === $matched_class_name ) { 304 return $block_content; 305 } 306 307 $tags = new WP_HTML_Tag_Processor( $block_content ); 308 if ( $tags->next_tag() ) { 309 $tags->add_class( 'has-custom-css' ); 310 $tags->add_class( $matched_class_name ); 311 } 312 313 return $tags->get_updated_html(); 314 } 315 316 add_filter( 'render_block', 'wp_render_custom_css_class_name', 10, 2 ); 317 add_filter( 'render_block_data', 'wp_render_custom_css_support_styles', 10, 1 ); 318 add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_custom_css', 1 ); 319 320 /** 321 * Registers the style block attribute for block types that support it. 322 * 323 * @since 7.0.0 324 * 325 * @param WP_Block_Type $block_type Block Type. 326 */ 327 function wp_register_custom_css_support( $block_type ) { 328 // Setup attributes and styles within that if needed. 329 if ( ! $block_type->attributes ) { 330 $block_type->attributes = array(); 331 } 332 333 // Check for existing style attribute definition e.g. from block.json. 334 if ( array_key_exists( 'style', $block_type->attributes ) ) { 335 return; 336 } 337 338 $has_custom_css_support = block_has_support( $block_type, array( 'customCSS' ), true ); 339 340 if ( $has_custom_css_support ) { 341 $block_type->attributes['style'] = array( 342 'type' => 'object', 343 ); 344 } 345 } 346 347 /** 348 * Removes custom CSS from a style attribute and any nested style states. 349 * 350 * @since 7.2.0 351 * @access private 352 * 353 * @param array $style The block's `style` attribute. 354 * @return array The style attribute with all `css` keys removed. 355 */ 356 function wp_strip_custom_css_from_style_array( $style ) { 357 if ( ! is_array( $style ) ) { 358 return $style; 359 } 360 361 unset( $style['css'] ); 362 363 foreach ( $style as $key => $value ) { 364 if ( is_string( $key ) && is_array( $value ) && ( str_starts_with( $key, ':' ) || str_starts_with( $key, '@' ) ) ) { 365 $nested_style = wp_strip_custom_css_from_style_array( $value ); 366 if ( empty( $nested_style ) ) { 367 unset( $style[ $key ] ); 368 } else { 369 $style[ $key ] = $nested_style; 370 } 371 } 372 } 373 374 return $style; 375 } 376 377 /** 378 * Strips custom CSS (`style.css` in attributes) from all blocks in post content. 379 * 380 * Uses {@see WP_Block_Parser::next_token()} to scan block tokens and surgically 381 * replace only the attribute JSON that changed — no parse_blocks() + 382 * serialize_blocks() round-trip needed. 383 * 384 * @since 7.0.0 385 * @access private 386 * 387 * @param string $content Post content to filter, expected to be escaped with slashes. 388 * @return string Filtered post content with block custom CSS removed. 389 */ 390 function wp_strip_custom_css_from_blocks( $content ) { 391 if ( ! has_blocks( $content ) ) { 392 return $content; 393 } 394 395 $unslashed = stripslashes( $content ); 396 397 $parser = new WP_Block_Parser(); 398 $parser->document = $unslashed; 399 $parser->offset = 0; 400 $end = strlen( $unslashed ); 401 $replacements = array(); 402 403 while ( $parser->offset < $end ) { 404 $next_token = $parser->next_token(); 405 406 if ( 'no-more-tokens' === $next_token[0] ) { 407 break; 408 } 409 410 list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token; 411 412 $parser->offset = $start_offset + $token_length; 413 414 if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) { 415 continue; 416 } 417 418 if ( ! isset( $attrs['style'] ) || ! is_array( $attrs['style'] ) ) { 419 continue; 420 } 421 422 $stripped_style = wp_strip_custom_css_from_style_array( $attrs['style'] ); 423 if ( $stripped_style === $attrs['style'] ) { 424 continue; 425 } 426 427 // Remove css and clean up empty style. 428 if ( empty( $stripped_style ) ) { 429 unset( $attrs['style'] ); 430 } else { 431 $attrs['style'] = $stripped_style; 432 } 433 434 // Locate the JSON portion within the token. 435 $token_string = substr( $unslashed, $start_offset, $token_length ); 436 $json_rel_start = strcspn( $token_string, '{' ); 437 $json_rel_end = strrpos( $token_string, '}' ); 438 439 $json_start = $start_offset + $json_rel_start; 440 $json_length = $json_rel_end - $json_rel_start + 1; 441 442 // Re-encode attributes. If attrs is now empty, remove JSON and trailing space. 443 if ( empty( $attrs ) ) { 444 // Remove the trailing space after JSON. 445 $replacements[] = array( $json_start, $json_length + 1, '' ); 446 } else { 447 $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) ); 448 } 449 } 450 451 if ( empty( $replacements ) ) { 452 return $content; 453 } 454 455 // Build the result by splicing replacements into the original string. 456 $result = ''; 457 $was_at = 0; 458 459 foreach ( $replacements as $replacement ) { 460 list( $offset, $length, $new_json ) = $replacement; 461 $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json; 462 $was_at = $offset + $length; 463 } 464 465 if ( $was_at < $end ) { 466 $result .= substr( $unslashed, $was_at ); 467 } 468 469 return addslashes( $result ); 470 } 471 472 /** 473 * Adds the filters to strip custom CSS from block content on save. 474 * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). 475 * 476 * @since 7.0.0 477 * @access private 478 */ 479 function wp_custom_css_kses_init_filters() { 480 add_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 481 add_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 482 } 483 484 /** 485 * Removes the filters that strip custom CSS from block content on save. 486 * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). 487 * 488 * @since 7.0.0 489 * @access private 490 */ 491 function wp_custom_css_remove_filters() { 492 remove_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 493 remove_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 494 } 495 496 /** 497 * Registers the custom CSS content filters if the user does not have the edit_css capability. 498 * 499 * @since 7.0.0 500 * @access private 501 */ 502 function wp_custom_css_kses_init() { 503 wp_custom_css_remove_filters(); 504 if ( ! current_user_can( 'edit_css' ) ) { 505 wp_custom_css_kses_init_filters(); 506 } 507 } 508 509 /** 510 * Initializes custom CSS content filters when imported data should be filtered. 511 * 512 * Runs at priority 999 on {@see 'force_filtered_html_on_import'} to ensure it 513 * fires after general KSES initialization, independently of user capabilities. 514 * If the input of the filter is true it means we are in an import situation and should 515 * enable the custom CSS filters, independently of the user capabilities. 516 * 517 * @since 7.0.0 518 * @access private 519 * 520 * @param mixed $arg Input argument of the filter. 521 * @return mixed Input argument of the filter. 522 */ 523 function wp_custom_css_force_filtered_html_on_import_filter( $arg ) { 524 if ( $arg ) { 525 wp_custom_css_kses_init_filters(); 526 } 527 return $arg; 528 } 529 530 // Run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). 531 add_action( 'init', 'wp_custom_css_kses_init', 20 ); 532 add_action( 'set_current_user', 'wp_custom_css_kses_init' ); 533 add_filter( 'force_filtered_html_on_import', 'wp_custom_css_force_filtered_html_on_import_filter', 999 ); 534 535 // Register the block support. 536 WP_Block_Supports::get_instance()->register( 537 'custom-css', 538 array( 539 'register_attribute' => 'wp_register_custom_css_support', 540 ) 541 );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Nov 2 08:20:36 2026 | Cross-referenced by PHPXref |