diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 2c1cf07d160cd..8d1091aefb831 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -644,6 +644,19 @@ class WP_Theme_JSON { 'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ), ); + /** + * Responsive breakpoint state keys and their corresponding CSS media queries. + * These are available for all blocks and wrap their styles in the given media query. + * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx. + * + * @since 7.1.0 + * @var array + */ + const RESPONSIVE_BREAKPOINTS = array( + 'mobile' => '@media (width <= 480px)', + 'tablet' => '@media (480px < width <= 782px)', + ); + /** * The valid elements that can be found under styles. * @@ -1054,11 +1067,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_elements = array(); /* - * Set allowed element pseudo selectors based on per element allow list. + * Set allowed element pseudo selectors and responsive breakpoint states.. * Target data structure in schema: * e.g. * - top level elements: `$schema['styles']['elements']['link'][':hover']`. * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. + * - block responsive elements: `$schema['styles']['blocks']['core/button']['tablet']['elements']['link'][':hover']`. */ foreach ( $valid_element_names as $element ) { $schema_styles_elements[ $element ] = $styles_non_top_level; @@ -1068,6 +1082,11 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; } } + + // Add responsive breakpoint states for elements. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { + $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level; + } } $schema_styles_blocks = array(); @@ -1075,19 +1094,31 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n /* * Generate a schema for blocks. - * - Block styles can contain `elements` & `variations` definitions. + * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions. * - Variations definitions cannot be nested. - * - Variations can contain styles for inner `blocks`. - * - Variation inner `blocks` styles can contain `elements`. + * - Variations can contain styles for inner `blocks`, `elements`, and responsive breakpoint states. + * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states. * - * As each variation needs a `blocks` schema but further nested - * inner `blocks`, the overall schema will be generated in multiple passes. + * As each variation needs both a `blocks` schema and responsive `blocks` schemas + * for further nested inner `blocks`, the overall schema is generated in multiple passes. */ foreach ( $valid_block_names as $block ) { $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; $schema_styles_blocks[ $block ] = $styles_non_top_level; $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + // Add responsive breakpoint states for all blocks. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { + $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level; + $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements; + + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { + $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level; + } + } + } + // Add pseudo-selectors for blocks that support them if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { @@ -1119,6 +1150,19 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n foreach ( $style_variation_names as $variation_name ) { $variation_schema = $block_style_variation_styles; + // Add responsive breakpoint states to block style variations. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { + $variation_schema[ $breakpoint_state ] = $styles_non_top_level; + $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements; + $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks; + + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { + $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level; + } + } + } + // Add pseudo-selectors to variations for blocks that support them if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { @@ -1886,6 +1930,11 @@ protected function get_layout_styles( $block_metadata, $options = array() ) { } } } + + if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) { + $block_rules = $options['media_query'] . '{' . $block_rules . '}'; + } + return $block_rules; } @@ -2875,56 +2924,156 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt 'path' => $node_path, 'selector' => $selector, 'selectors' => $feature_selectors, + 'elements' => $selectors[ $name ]['elements'] ?? array(), 'duotone' => $duotone_selector, 'features' => $feature_selectors, 'variations' => $variation_selectors, 'css' => $selector, ); + // Responsive block nodes: emit one node per breakpoint that has styles. + // These are rendered immediately after the base block node so that + // the cascade order is: .block{} → @media{.block{}} + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) { + $nodes[] = array( + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name, $breakpoint ), + 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], + 'selector' => $selector, + 'selectors' => $feature_selectors, + 'elements' => $selectors[ $name ]['elements'] ?? array(), + 'variations' => $variation_selectors, + 'css' => $selector, + ); + } + } + // Handle any pseudo selectors for the block. if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) { foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) { - if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) { + $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ); + if ( $has_pseudo ) { + /* + * Append the pseudo-selector to each feature selector so that + * get_feature_declarations_for_node generates CSS scoped to the + * pseudo-state (e.g. '.wp-block-button:hover') rather than the + * default state (e.g. '.wp-block-button'). + */ + $pseudo_feature_selectors = array(); + foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) { + if ( is_array( $feature_selector ) ) { + $pseudo_feature_selectors[ $feature ] = array(); + foreach ( $feature_selector as $subfeature => $subfeature_selector ) { + $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector ); + } + } else { + $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector ); + } + } + $nodes[] = array( 'name' => $name, 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ), 'selector' => static::append_to_selector( $selector, $pseudo_selector ), - 'selectors' => $feature_selectors, + 'selectors' => $pseudo_feature_selectors, + 'elements' => $selectors[ $name ]['elements'] ?? array(), 'duotone' => $duotone_selector, 'variations' => $variation_selectors, 'css' => static::append_to_selector( $selector, $pseudo_selector ), ); + + // Responsive pseudo nodes: emit one node per breakpoint that has + // this pseudo state, immediately after the default pseudo node. + // Cascade order: .block:hover{} → @media{.block:hover{}} + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ), + 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], + 'selector' => static::append_to_selector( $selector, $pseudo_selector ), + 'selectors' => $pseudo_feature_selectors, + 'elements' => $selectors[ $name ]['elements'] ?? array(), + 'variations' => $variation_selectors, + 'css' => static::append_to_selector( $selector, $pseudo_selector ), + ); + } + } } } } } - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { - $node_path = array( 'styles', 'blocks', $name, 'elements', $element ); + $element_path = array( 'styles', 'blocks', $name, 'elements', $element ); if ( $include_node_paths_only ) { $nodes[] = array( - 'path' => $node_path, + 'path' => $element_path, ); continue; } + $element_selector = $selectors[ $name ]['elements'][ $element ]; + $nodes[] = array( - 'path' => $node_path, - 'selector' => $selectors[ $name ]['elements'][ $element ], + 'path' => $element_path, + 'selector' => $element_selector, ); + // Responsive element nodes: one node per breakpoint that has + // styles for this element. Cascade: a{} → @media{a{}} + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ), + 'selector' => $element_selector, + 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], + ); + } + } + // Handle any pseudo selectors for the element. if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { - $node_path = array( 'styles', 'blocks', $name, 'elements', $element ); + // Create element pseudo node if default or any responsive breakpoint has the pseudo. + $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ); + if ( ! $has_element_pseudo ) { + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) { + if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) { + $has_element_pseudo = true; + break; + } + } + } + + if ( $has_element_pseudo ) { + $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element ); + if ( $include_node_paths_only ) { + $nodes[] = array( + 'path' => $element_pseudo_path, + ); + continue; + } $nodes[] = array( - 'path' => $node_path, - 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), + 'path' => $element_pseudo_path, + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ), ); + + // Responsive element pseudo nodes: one node per breakpoint + // that has this pseudo state for this element. + // Cascade: a:hover{} → @media{a:hover{}} + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ), + 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ), + 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], + ); + } + } } } } @@ -2953,16 +3102,19 @@ public function get_styles_for_block( $block_metadata ) { $settings = $this->theme_json['settings'] ?? array(); $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node ); $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector; + $media_query = $block_metadata['media_query'] ?? null; // Update text indent selector for paragraph blocks based on the textIndent setting. $block_name = $block_metadata['name'] ?? null; $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ); + $block_elements = $block_metadata['elements'] ?? array(); // If there are style variations, generate the declarations for them, including any feature selectors the block may have. $style_variation_declarations = array(); $style_variation_custom_css = array(); + $style_variation_responsive_css = array(); $style_variation_layout_metadata = array(); - if ( ! empty( $block_metadata['variations'] ) ) { + if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) { foreach ( $block_metadata['variations'] as $style_variation ) { $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); $clean_style_variation_selector = trim( $style_variation['selector'] ); @@ -3028,6 +3180,135 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) { 'node' => $style_variation_node, ); } + + // Store responsive breakpoint CSS for the style variation. + // This includes both base properties and feature-level selectors. + $variation_responsive_css = ''; + + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( ! isset( $style_variation_node[ $breakpoint ] ) ) { + continue; + } + + $breakpoint_node = $style_variation_node[ $breakpoint ]; + $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ]; + // Process feature-level declarations for this breakpoint. + $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node ); + $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name ); + $breakpoint_feature_declarations = static::update_button_width_declarations( $breakpoint_feature_declarations, $settings ); + foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) { + $clean_feature_selector = preg_replace( '/,\s+/', ',', $feature_selector ); + $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_feature_selector ); + + if ( $block_metadata['selector'] && ! str_contains( $clean_feature_selector, $block_metadata['selector'] ) ) { + /* + * Feature selector is block-level (e.g. `.wp-block-button` for + * dimensions/width) — apply the variation class directly to it. + */ + $feature_element_selector = str_replace( $shortened_selector, '', $clean_style_variation_selector ); + $combined_selectors = str_replace( $feature_element_selector, '', $clean_style_variation_selector ); + } else { + // Prepend the variation selector to the current selector. + $split_selectors = explode( ',', $shortened_selector ); + $updated_selectors = array_map( + static function ( $split_selector ) use ( $clean_style_variation_selector ) { + return $clean_style_variation_selector . $split_selector; + }, + $split_selectors + ); + $combined_selectors = implode( ',', $updated_selectors ); + } + + $feature_ruleset = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl ); + $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}'; + } + + // Process base properties for this breakpoint. + $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json ); + if ( ! empty( $breakpoint_declarations ) ) { + $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations ); + $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}'; + } + + $breakpoint_pseudo_declarations = static::process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name ); + foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) { + if ( empty( $pseudo_declarations ) ) { + continue; + } + $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations ); + $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}'; + } + + // Process custom CSS for this breakpoint. + if ( isset( $breakpoint_node['css'] ) ) { + $breakpoint_custom_css = static::process_blocks_custom_css( $breakpoint_node['css'], $style_variation['selector'] ); + $variation_responsive_css .= $breakpoint_media . '{' . $breakpoint_custom_css . '}'; + } + + // Process blockGap responsive layout styles for this variation. + if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) { + $variation_layout_metadata = $style_variation; + $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css']; + $variation_responsive_css .= $this->get_layout_styles( + $variation_layout_metadata, + array( + 'node' => $breakpoint_node, + 'media_query' => $breakpoint_media, + ) + ); + } + + // Process nested element styles for this breakpoint state. + if ( isset( $breakpoint_node['elements'] ) && ! empty( $block_elements ) ) { + foreach ( $breakpoint_node['elements'] as $element_name => $element_node ) { + if ( ! isset( $block_elements[ $element_name ] ) ) { + continue; + } + + $clean_element_selector = preg_replace( '/,\s+/', ',', $block_elements[ $element_name ] ); + $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_element_selector ); + $split_selectors = explode( ',', $shortened_selector ); + $updated_selectors = array_map( + static function ( $split_selector ) use ( $clean_style_variation_selector ) { + return $clean_style_variation_selector . $split_selector; + }, + $split_selectors + ); + $variation_element_selector = implode( ',', $updated_selectors ); + + $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json ); + if ( ! empty( $element_declarations ) ) { + $element_ruleset = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations ); + $variation_responsive_css .= $breakpoint_media . '{' . $element_ruleset . '}'; + } + + if ( isset( $element_node['css'] ) ) { + $element_custom_css = static::process_blocks_custom_css( $element_node['css'], $variation_element_selector ); + $variation_responsive_css .= $breakpoint_media . '{' . $element_custom_css . '}'; + } + + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { + if ( ! isset( $element_node[ $pseudo_selector ] ) ) { + continue; + } + + $pseudo_declarations = static::compute_style_properties( $element_node[ $pseudo_selector ], $settings, null, $this->theme_json ); + if ( empty( $pseudo_declarations ) ) { + continue; + } + + $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations ); + $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}'; + } + } + } + } + } + + if ( ! empty( $variation_responsive_css ) ) { + $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css; + } } } /* @@ -3219,6 +3500,13 @@ static function ( $pseudo_selector ) use ( $selector ) { $block_rules .= $this->process_blocks_custom_css( $node['css'], $selector ); } + // 8. Wrap the entire block output in a media query if this is a responsive node. + // Responsive nodes are created by get_block_nodes() for each breakpoint and carry + // a 'media_query' key. + if ( $media_query && ! empty( $block_rules ) ) { + $block_rules = $media_query . '{' . $block_rules . '}'; + } + return $block_rules; } @@ -3714,6 +4002,10 @@ public static function remove_insecure_properties( $theme_json, $origin = 'theme continue; } + $block_name = in_array( 'blocks', $metadata['path'], true ) + ? static::get_block_name_from_metadata_path( $metadata ) + : null; + // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability. if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) { $output = $input; @@ -3739,6 +4031,34 @@ public static function remove_insecure_properties( $theme_json, $origin = 'theme } } + // Re-add and process responsive breakpoint styles. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $input[ $breakpoint ] ) ) { + $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] ); + + if ( isset( $input[ $breakpoint ]['elements'] ) ) { + $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'] ); + } + + if ( isset( $input[ $breakpoint ]['blocks'] ) ) { + $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'] ); + } + + if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { + if ( isset( $input[ $breakpoint ][ $pseudo_selector ] ) ) { + $output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $input[ $breakpoint ][ $pseudo_selector ] ); + } + } + } + + // Responsive custom CSS is allowed for users with 'edit_css' capability. + if ( isset( $input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) { + $output[ $breakpoint ]['css'] = $input[ $breakpoint ]['css']; + } + } + } + if ( ! empty( $output ) ) { _wp_array_set( $sanitized, $metadata['path'], $output ); } @@ -3760,6 +4080,34 @@ public static function remove_insecure_properties( $theme_json, $origin = 'theme $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] ); } + // Re-add and process responsive breakpoint styles for variations. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $variation_input[ $breakpoint ] ) ) { + $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] ); + + if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) { + $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'] ); + } + + if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) { + $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'] ); + } + + if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { + if ( isset( $variation_input[ $breakpoint ][ $pseudo_selector ] ) ) { + $variation_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $variation_input[ $breakpoint ][ $pseudo_selector ] ); + } + } + } + + // Responsive custom CSS is allowed for users with 'edit_css' capability. + if ( isset( $variation_input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) { + $variation_output[ $breakpoint ]['css'] = $variation_input[ $breakpoint ]['css']; + } + } + } + if ( ! empty( $variation_output ) ) { _wp_array_set( $sanitized, $variation['path'], $variation_output ); } @@ -3820,6 +4168,21 @@ protected static function remove_insecure_element_styles( $elements ) { } } + // Re-add and process responsive breakpoint styles for elements. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $element_input[ $breakpoint ] ) ) { + $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] ); + + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { + if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) { + $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] ); + } + } + } + } + } + $sanitized[ $element_name ] = $element_output; } } @@ -3843,6 +4206,21 @@ protected static function remove_insecure_inner_block_styles( $blocks ) { $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] ); } + // Re-add and process responsive breakpoint styles for inner blocks. + foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { + if ( isset( $block_input[ $breakpoint ] ) ) { + $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] ); + + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) { + if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) { + $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] ); + } + } + } + } + } + $sanitized[ $block_type ] = $block_output; } return $sanitized;