diff --git a/src/wp-includes/block-supports/background.php b/src/wp-includes/block-supports/background.php index cc4d451e1d8bd..dc73e48c378d7 100644 --- a/src/wp-includes/block-supports/background.php +++ b/src/wp-includes/block-supports/background.php @@ -43,6 +43,7 @@ function wp_register_background_support( $block_type ) { * @since 6.5.0 Added support for `backgroundPosition` and `backgroundRepeat` output. * @since 6.6.0 Removed requirement for `backgroundImage.source`. A file/url is the default. * @since 6.7.0 Added support for `backgroundAttachment` output. + * @since 7.1.0 Added support for `background.gradient` output. * * @access private * @@ -51,34 +52,49 @@ function wp_register_background_support( $block_type ) { * @return string Filtered block content. */ function wp_render_background_support( $block_content, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - $block_attributes = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array(); - $has_background_image_support = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false ); + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $block_attributes = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array(); + $has_background_image_support = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false ); + $has_background_gradient_support = block_has_support( $block_type, array( 'background', 'gradient' ), false ); if ( - ! $has_background_image_support || - wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ) || + ( ! $has_background_image_support && ! $has_background_gradient_support ) || ! isset( $block_attributes['style']['background'] ) ) { return $block_content; } - $background_styles = array(); - $background_styles['backgroundImage'] = $block_attributes['style']['background']['backgroundImage'] ?? null; - $background_styles['backgroundSize'] = $block_attributes['style']['background']['backgroundSize'] ?? null; - $background_styles['backgroundPosition'] = $block_attributes['style']['background']['backgroundPosition'] ?? null; - $background_styles['backgroundRepeat'] = $block_attributes['style']['background']['backgroundRepeat'] ?? null; - $background_styles['backgroundAttachment'] = $block_attributes['style']['background']['backgroundAttachment'] ?? null; + // Check serialization skip for each feature individually. + $skip_background_image = ! $has_background_image_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ); + $skip_background_gradient = ! $has_background_gradient_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'gradient' ); - if ( ! empty( $background_styles['backgroundImage'] ) ) { - $background_styles['backgroundSize'] = $background_styles['backgroundSize'] ?? 'cover'; + if ( $skip_background_image && $skip_background_gradient ) { + return $block_content; + } + + $background_styles = array(); + + if ( ! $skip_background_image ) { + $background_styles['backgroundImage'] = $block_attributes['style']['background']['backgroundImage'] ?? null; + $background_styles['backgroundSize'] = $block_attributes['style']['background']['backgroundSize'] ?? null; + $background_styles['backgroundPosition'] = $block_attributes['style']['background']['backgroundPosition'] ?? null; + $background_styles['backgroundRepeat'] = $block_attributes['style']['background']['backgroundRepeat'] ?? null; + $background_styles['backgroundAttachment'] = $block_attributes['style']['background']['backgroundAttachment'] ?? null; - // If the background size is set to `contain` and no position is set, set the position to `center`. - if ( 'contain' === $background_styles['backgroundSize'] && ! $background_styles['backgroundPosition'] ) { - $background_styles['backgroundPosition'] = '50% 50%'; + if ( ! empty( $background_styles['backgroundImage'] ) ) { + $background_styles['backgroundSize'] = $background_styles['backgroundSize'] ?? 'cover'; + + // If the background size is set to `contain` and no position is set, set the position to `center`. + if ( 'contain' === $background_styles['backgroundSize'] && ! $background_styles['backgroundPosition'] ) { + $background_styles['backgroundPosition'] = '50% 50%'; + } } } + if ( ! $skip_background_gradient ) { + $background_styles['gradient'] = $block_attributes['style']['background']['gradient'] ?? null; + } + $styles = wp_style_engine_get_styles( array( 'background' => $background_styles ) ); if ( ! empty( $styles['css'] ) ) { diff --git a/src/wp-includes/block-supports/colors.php b/src/wp-includes/block-supports/colors.php index 5d55f2f97251a..081cf473e4c11 100644 --- a/src/wp-includes/block-supports/colors.php +++ b/src/wp-includes/block-supports/colors.php @@ -114,7 +114,17 @@ function wp_apply_colors_support( $block_type, $block_attributes ) { } // Gradients. - if ( $has_gradients_support && ! wp_should_skip_block_supports_serialization( $block_type, 'color', 'gradients' ) ) { + // Suppress color.gradient CSS when background.gradient is supported and + // explicitly set. background.php owns CSS generation in that case, and + // emitting the background shorthand here would conflict with it. + $has_background_gradient_support = block_has_support( $block_type, array( 'background', 'gradient' ), false ); + $has_background_gradient_value = ! empty( $block_attributes['style']['background']['gradient'] ); + + if ( + $has_gradients_support && + ! wp_should_skip_block_supports_serialization( $block_type, 'color', 'gradients' ) && + ! ( $has_background_gradient_support && $has_background_gradient_value ) + ) { $preset_gradient_color = array_key_exists( 'gradient', $block_attributes ) ? "var:preset|gradient|{$block_attributes['gradient']}" : null; $custom_gradient_color = $block_attributes['style']['color']['gradient'] ?? null; $color_block_styles['gradient'] = $preset_gradient_color ? $preset_gradient_color : $custom_gradient_color; diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 5abd2817b8aa4..fb0fd4f921611 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -330,6 +330,7 @@ class WP_Theme_JSON { * * @since 6.2.0 * @since 6.6.0 Added background-image properties. + * @since 7.1.0 Added `background.gradient` to `background-image` paths. * @var array */ const INDIRECT_PROPERTIES_METADATA = array( @@ -348,6 +349,7 @@ class WP_Theme_JSON { ), 'background-image' => array( array( 'background', 'backgroundImage', 'url' ), + array( 'background', 'gradient' ), ), ); @@ -412,6 +414,7 @@ class WP_Theme_JSON { * @since 7.0.0 Added type markers to the schema for boolean values. * Added support for `dimensions.width` and `dimensions.height`. * Added support for `typography.textIndent`. + * @since 7.1.0 Added support for `background.gradient`. * @var array */ const VALID_SETTINGS = array( @@ -420,6 +423,7 @@ class WP_Theme_JSON { 'background' => array( 'backgroundImage' => null, 'backgroundSize' => null, + 'gradient' => null, ), 'border' => array( 'color' => null, @@ -549,6 +553,7 @@ class WP_Theme_JSON { * @since 6.5.0 Added support for `dimensions.aspectRatio`. * @since 6.6.0 Added `background` sub properties to top-level only. * @since 7.0.0 Added support for `dimensions.width` and `dimensions.height`. + * @since 7.1.0 Added `background.gradient`. * @var array */ const VALID_STYLES = array( @@ -558,6 +563,7 @@ class WP_Theme_JSON { 'backgroundRepeat' => null, 'backgroundSize' => null, 'backgroundAttachment' => null, + 'gradient' => null, ), 'border' => array( 'color' => null, @@ -791,11 +797,13 @@ public static function get_element_class_name( $element ) { * @since 6.4.0 Added `background.backgroundImage`. * @since 6.5.0 Added `background.backgroundSize` and `dimensions.aspectRatio`. * @since 7.0.0 Added `dimensions.width` and `dimensions.height`. + * @since 7.1.0 Added `background.gradient`. * @var array */ const APPEARANCE_TOOLS_OPT_INS = array( array( 'background', 'backgroundImage' ), array( 'background', 'backgroundSize' ), + array( 'background', 'gradient' ), array( 'border', 'color' ), array( 'border', 'radius' ), array( 'border', 'style' ), @@ -2434,11 +2442,21 @@ protected static function compute_style_properties( $styles, $settings = array() * For uploaded image (images with a database ID), apply size and position defaults, * equal to those applied in block supports in lib/background.php. */ - if ( 'background-image' === $css_property && ! empty( $value ) ) { - $background_styles = wp_style_engine_get_styles( - array( 'background' => array( 'backgroundImage' => $value ) ) - ); - $value = $background_styles['declarations'][ $css_property ]; + if ( 'background-image' === $css_property ) { + $background_image_input = array(); + if ( ! empty( $value ) ) { + $background_image_input['backgroundImage'] = $value; + } + $gradient_value = $styles['background']['gradient'] ?? null; + if ( ! empty( $gradient_value ) ) { + $background_image_input['gradient'] = $gradient_value; + } + if ( ! empty( $background_image_input ) ) { + $background_styles = wp_style_engine_get_styles( + array( 'background' => $background_image_input ) + ); + $value = $background_styles['declarations'][ $css_property ] ?? null; + } } if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) { if ( 'background-size' === $css_property ) { diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 062f85308512f..6533510a4374a 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2885,6 +2885,61 @@ function safecss_filter_attr( $css, $deprecated = '' ) { return $css; } +/** + * Allow combined gradient and url() background-image values in inline styles. + * + * WordPress's safecss_filter_attr() handles gradient and url() values + * separately for background-image, but fails when both appear in a single + * comma-separated declaration. The url() portion is stripped from the test + * string before the filter runs, but the gradient regex in core expects the + * gradient to be the only value and doesn't match when a trailing comma and + * whitespace remain. This leaves parentheses in the test string, which + * triggers the unsafe-character check. + * + * This filter catches that case: the test string still contains a valid + * gradient function with only commas and whitespace remaining after the + * url() was removed. + * + * @since 7.1.0 + * + * @param bool $allow_css Whether the CSS is allowed. + * @param string $css_test_string The CSS declaration to test. + * @return bool Whether the CSS is allowed. + */ +function wp_kses_allow_background_image_combined( $allow_css, $css_test_string ) { + if ( $allow_css ) { + return $allow_css; + } + + /* + * The test string at this point has url() values already removed by + * safecss_filter_attr. What remains is a gradient with comma/whitespace + * residue where the url() was stripped. Two possible forms: + * + * Gradient first: "background-image:(...), " + * URL first: "background-image:, (...)" + * + * A trailing or leading comma (with optional whitespace) must be present + * to confirm a url() was actually removed. Without that residue, the + * gradient alone would already pass core's own check. + */ + $gradient_pattern = '(?:linear|radial|conic|repeating-linear|repeating-radial|repeating-conic)-gradient\((?:[^()]|\([^()]*\))*\)'; + $var_pattern = 'var\(--[a-zA-Z0-9_-]+(?:--[a-zA-Z0-9_-]+)*\)'; + $value_pattern = "(?:$gradient_pattern|$var_pattern)"; + + // Gradient/var first, then comma+whitespace residue from stripped url(). + $pattern_gradient_first = '/^background-image\s*:\s*' . $value_pattern . '\s*,[\s,]*$/'; + // Stripped url() first (comma+whitespace residue), then gradient/var. + $pattern_url_first = '/^background-image\s*:[\s,]*,\s*' . $value_pattern . '\s*$/'; + + if ( preg_match( $pattern_gradient_first, $css_test_string ) || preg_match( $pattern_url_first, $css_test_string ) ) { + return true; + } + + return $allow_css; +} +add_filter( 'safecss_filter_attr_allow_css', 'wp_kses_allow_background_image_combined', 10, 2 ); + /** * Helper function to add global attributes to a tag in the allowed HTML list. * diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index be52699c642eb..f409fb79abc41 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -48,6 +48,7 @@ final class WP_Style_Engine { * - value_func => (string) the name of a function to generate a CSS definition array for a particular style object. The output of this function should be `array( "$property" => "$value", ... )`. * * @since 6.1.0 + * @since 7.1.0 Added `background.gradient` property. * @var array */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( @@ -83,6 +84,18 @@ final class WP_Style_Engine { ), 'path' => array( 'background', 'backgroundAttachment' ), ), + 'gradient' => array( + 'property_keys' => array( + 'default' => 'background-image', + ), + 'css_vars' => array( + 'gradient' => '--wp--preset--gradient--$slug', + ), + 'path' => array( 'background', 'gradient' ), + 'classnames' => array( + 'has-background' => true, + ), + ), ), 'color' => array( 'text' => array( @@ -489,6 +502,13 @@ public static function parse_block_styles( $block_styles, $options ) { $css_declarations = static::get_css_declarations( $style_value, $style_definition, $options ); if ( ! empty( $css_declarations ) ) { + /* + * Combine background gradient and background image into a single + * comma-separated background-image value, matching the JS style engine. + */ + if ( isset( $css_declarations['background-image'] ) && isset( $parsed_styles['declarations']['background-image'] ) ) { + $css_declarations['background-image'] = $css_declarations['background-image'] . ', ' . $parsed_styles['declarations']['background-image']; + } $parsed_styles['declarations'] = array_merge( $parsed_styles['declarations'], $css_declarations ); } } diff --git a/tests/phpunit/tests/block-supports/colors.php b/tests/phpunit/tests/block-supports/colors.php index 86dd8de38d0b0..30e4985c63ed9 100644 --- a/tests/phpunit/tests/block-supports/colors.php +++ b/tests/phpunit/tests/block-supports/colors.php @@ -148,4 +148,155 @@ public function test_gradient_with_individual_skipped_serialization_block_suppor $this->assertSame( $expected, $actual ); } + + /** + * Tests that color.gradient CSS is suppressed when background.gradient + * is supported and explicitly set. + * + * @ticket 64974 + * + * @covers ::wp_apply_colors_support + */ + public function test_color_gradient_suppressed_when_background_gradient_is_supported_and_set() { + $this->test_block_name = 'test/color-gradient-suppressed-by-background-gradient'; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => array( + 'gradients' => true, + ), + 'background' => array( + 'gradient' => true, + ), + ), + ) + ); + + $registry = WP_Block_Type_Registry::get_instance(); + $block_type = $registry->get_registered( $this->test_block_name ); + + // Both color.gradient and background.gradient are set — background.php + // owns CSS generation, so color.gradient CSS must be suppressed. + $block_atts = array( + 'style' => array( + 'color' => array( + 'gradient' => 'linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%)', + ), + 'background' => array( + 'gradient' => 'linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%)', + ), + ), + ); + + $actual = wp_apply_colors_support( $block_type, $block_atts ); + $expected = array(); + + $this->assertSame( $expected, $actual ); + } + + /** + * Tests that color.gradient CSS is emitted when background.gradient + * is supported but not set. + * + * @ticket 64974 + * + * @covers ::wp_apply_colors_support + */ + public function test_color_gradient_emitted_when_background_gradient_is_supported_but_not_set() { + $this->test_block_name = 'test/color-gradient-not-suppressed-without-background-gradient-value'; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => array( + 'gradients' => true, + ), + 'background' => array( + 'gradient' => true, + ), + ), + ) + ); + + $registry = WP_Block_Type_Registry::get_instance(); + $block_type = $registry->get_registered( $this->test_block_name ); + + // background.gradient is supported but not yet set — legacy color.gradient + // CSS must still be emitted to preserve existing content rendering. + $block_atts = array( + 'style' => array( + 'color' => array( + 'gradient' => 'linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%)', + ), + ), + ); + + $actual = wp_apply_colors_support( $block_type, $block_atts ); + $expected = array( + 'class' => 'has-background', + 'style' => 'background:linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%);', + ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Tests that color.gradient CSS is emitted when background.gradient + * is not supported. + * + * @ticket 64974 + * + * @covers ::wp_apply_colors_support + */ + public function test_color_gradient_emitted_when_background_gradient_is_not_supported() { + $this->test_block_name = 'test/color-gradient-no-background-gradient-support'; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => array( + 'gradients' => true, + ), + ), + ) + ); + + $registry = WP_Block_Type_Registry::get_instance(); + $block_type = $registry->get_registered( $this->test_block_name ); + + $block_atts = array( + 'style' => array( + 'color' => array( + 'gradient' => 'linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%)', + ), + ), + ); + + $actual = wp_apply_colors_support( $block_type, $block_atts ); + $expected = array( + 'class' => 'has-background', + 'style' => 'background:linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%);', + ); + + $this->assertSame( $expected, $actual ); + } } diff --git a/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php b/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php index 5a4faf46085c0..e4c5a50259c28 100644 --- a/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php +++ b/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php @@ -69,6 +69,7 @@ public function filter_set_theme_root() { * @ticket 61123 * @ticket 61720 * @ticket 61858 + * @ticket 64974 * * @covers ::wp_render_background_support * @@ -199,6 +200,74 @@ public function data_background_block_support() { 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), + 'background gradient style is applied' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-gradient-rules-are-output', + 'background_settings' => array( + 'gradient' => true, + ), + 'background_style' => array( + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), + 'background gradient style is not applied if the block does not support it' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-gradient-rules-are-not-output', + 'background_settings' => array( + 'gradient' => false, + ), + 'background_style' => array( + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), + 'background gradient style with preset slug is applied' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-gradient-preset-slug', + 'background_settings' => array( + 'gradient' => true, + ), + 'background_style' => array( + 'gradient' => 'var:preset|gradient|vivid-cyan-blue', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), + 'background gradient and image combined' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-gradient-and-image-combined', + 'background_settings' => array( + 'backgroundImage' => true, + 'gradient' => true, + ), + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + ), + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), + 'background gradient with hsl colors and image combined' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-gradient-hsl-and-image', + 'background_settings' => array( + 'backgroundImage' => true, + 'gradient' => true, + ), + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + ), + 'gradient' => 'linear-gradient(135deg,hsl(0,100%,50%) 0%,hsl(240,100%,50%) 100%)', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), 'background image style is not applied if the block does not support background image' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-rules-are-not-output', @@ -215,4 +284,75 @@ public function data_background_block_support() { ), ); } + + /** + * Tests that combined background gradient and image CSS values pass KSES. + * + * WordPress's safecss_filter_attr() handles gradient and url() values + * separately but strips the declaration when both appear in a single + * comma-separated background-image. The wp_kses_allow_background_image_combined + * filter should ensure these combined values survive sanitization. + * + * @ticket 64974 + * + * @covers ::wp_kses_allow_background_image_combined + * + * @dataProvider data_background_combined_values_pass_kses + * + * @param string $css The CSS declaration to test. + */ + public function test_background_combined_values_pass_kses( $css ) { + $result = safecss_filter_attr( $css ); + $this->assertNotEmpty( $result, "Expected CSS to be allowed: $css" ); + $this->assertStringContainsString( 'background-image', $result ); + } + + /** + * Data provider for combined background-image KSES tests. + * + * @return array[] + */ + public function data_background_combined_values_pass_kses() { + return array( + 'gradient first with rgb colors' => array( + 'background-image: linear-gradient(135deg, rgb(255,0,0) 0%, rgb(0,0,255) 100%), url(https://example.com/image.jpg)', + ), + 'url first with rgb colors' => array( + 'background-image: url(https://example.com/image.jpg), linear-gradient(135deg, rgb(255,0,0) 0%, rgb(0,0,255) 100%)', + ), + 'gradient first with rgba colors' => array( + 'background-image: linear-gradient(135deg, rgba(0,0,0,0.8) 0%, rgba(255,255,255,0.2) 100%), url(https://example.com/image.jpg)', + ), + 'gradient first with hsl colors' => array( + 'background-image: linear-gradient(135deg, hsl(0, 100%, 50%) 0%, hsl(240, 100%, 50%) 100%), url(https://example.com/image.jpg)', + ), + 'gradient first with hsla colors' => array( + 'background-image: linear-gradient(135deg, hsla(0, 100%, 50%, 0.8) 0%, hsla(240, 100%, 50%, 0.5) 100%), url(https://example.com/image.jpg)', + ), + 'gradient first with oklch colors' => array( + 'background-image: linear-gradient(135deg, oklch(0.7 0.15 30) 0%, oklch(0.5 0.2 260) 100%), url(https://example.com/image.jpg)', + ), + 'gradient first with lab colors' => array( + 'background-image: linear-gradient(135deg, lab(50% 40 59.5) 0%, lab(70% -45 0) 100%), url(https://example.com/image.jpg)', + ), + 'radial gradient with url' => array( + 'background-image: radial-gradient(circle, rgb(255,0,0) 0%, rgb(0,0,255) 100%), url(https://example.com/image.jpg)', + ), + 'conic gradient with url' => array( + 'background-image: conic-gradient(rgb(255,0,0), rgb(0,0,255)), url(https://example.com/image.jpg)', + ), + 'repeating-linear gradient with url' => array( + 'background-image: repeating-linear-gradient(45deg, rgb(255,0,0) 0px, rgb(0,0,255) 40px), url(https://example.com/image.jpg)', + ), + 'var preset gradient first with url' => array( + 'background-image: var(--wp--preset--gradient--vivid-cyan-blue), url(https://example.com/image.jpg)', + ), + 'url first with var preset gradient' => array( + 'background-image: url(https://example.com/image.jpg), var(--wp--preset--gradient--vivid-cyan-blue)', + ), + 'gradient with hex colors' => array( + 'background-image: linear-gradient(135deg, #ff0000 0%, #0000ff 100%), url(https://example.com/image.jpg)', + ), + ); + } } diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index f6e0444faf74b..dd8f3a46f6c20 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -23,6 +23,7 @@ class Tests_wpStyleEngine extends WP_UnitTestCase { * @ticket 61720 * @ticket 62189 * @ticket 63799 + * @ticket 64974 * * @covers ::wp_style_engine_get_styles * @@ -573,6 +574,57 @@ public function data_wp_style_engine_get_styles() { ), ), + 'inline_background_gradient_only' => array( + 'block_styles' => array( + 'background' => array( + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'background-image:linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%);', + 'declarations' => array( + 'background-image' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + 'classnames' => 'has-background', + ), + ), + + 'inline_background_gradient_with_preset_slug' => array( + 'block_styles' => array( + 'background' => array( + 'gradient' => 'var:preset|gradient|vivid-cyan-blue', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'background-image:var(--wp--preset--gradient--vivid-cyan-blue);', + 'declarations' => array( + 'background-image' => 'var(--wp--preset--gradient--vivid-cyan-blue)', + ), + 'classnames' => 'has-background', + ), + ), + + 'inline_background_gradient_and_image_combined' => array( + 'block_styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + ), + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => "background-image:linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%), url('https://example.com/image.jpg');", + 'declarations' => array( + 'background-image' => "linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%), url('https://example.com/image.jpg')", + ), + 'classnames' => 'has-background', + ), + ), + 'inline_background_image_url_with_background_size' => array( 'block_styles' => array( 'background' => array( diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 95de67c69f98c..ecdeecb8aae18 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -265,6 +265,7 @@ public function test_get_settings_appearance_true_opts_in() { 'background' => array( 'backgroundImage' => true, 'backgroundSize' => true, + 'gradient' => true, ), 'border' => array( 'width' => true, @@ -306,6 +307,7 @@ public function test_get_settings_appearance_true_opts_in() { 'background' => array( 'backgroundImage' => true, 'backgroundSize' => true, + 'gradient' => true, ), 'border' => array( 'width' => true, @@ -3206,6 +3208,169 @@ public function test_remove_insecure_properties_should_allow_indirect_properties $this->assertSameSetsWithIndex( $expected, $actual ); } + /** + * Tests that remove_insecure_properties allows combined background gradient and image. + * + * @ticket 64974 + * + * @covers WP_Theme_JSON::remove_insecure_properties + */ + public function test_remove_insecure_properties_allows_combined_background_gradient_and_image() { + $actual = WP_Theme_JSON::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + ), + 'gradient' => 'linear-gradient(135deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%)', + ), + ), + ), + true + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + ), + 'gradient' => 'linear-gradient(135deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%)', + ), + ), + ); + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + + /** + * Tests that remove_insecure_properties allows background gradient only. + * + * @ticket 64974 + * + * @covers WP_Theme_JSON::remove_insecure_properties + */ + public function test_remove_insecure_properties_allows_background_gradient_only() { + $actual = WP_Theme_JSON::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'gradient' => 'linear-gradient(135deg, #fff 0%, #000 100%)', + ), + ), + ), + true + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'gradient' => 'linear-gradient(135deg, #fff 0%, #000 100%)', + ), + ), + ); + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + + /** + * Tests that background gradient styles are output correctly in theme.json. + * + * @ticket 64974 + * + * @covers WP_Theme_JSON::get_styles_for_block + */ + public function test_get_background_gradient_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + ), + ), + ) + ); + + $body_node = array( + 'path' => array( 'styles' ), + 'selector' => 'body', + ); + + $expected_styles = 'html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%);}'; + $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $body_node ), 'Styles returned from "::get_styles_for_block()" with top-level background gradient do not match expectations' ); + } + + /** + * Tests that background gradient preset slug styles are resolved correctly in theme.json. + * + * @ticket 64974 + * + * @covers WP_Theme_JSON::get_styles_for_block + */ + public function test_get_background_gradient_preset_slug_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'gradient' => 'var:preset|gradient|vivid-cyan-blue', + ), + ), + ) + ); + + $body_node = array( + 'path' => array( 'styles' ), + 'selector' => 'body', + ); + + $expected_styles = 'html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: var(--wp--preset--gradient--vivid-cyan-blue);}'; + $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $body_node ), 'Styles returned from "::get_styles_for_block()" with background gradient preset slug do not match expectations' ); + } + + /** + * Tests that background gradient and image are combined correctly in theme.json. + * + * @ticket 64974 + * + * @covers WP_Theme_JSON::get_styles_for_block + */ + public function test_get_background_gradient_and_image_combined_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'gradient' => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)', + 'backgroundImage' => array( + 'url' => 'http://example.org/image.png', + ), + ), + ), + ), + ), + ) + ); + + $group_node = array( + 'name' => 'core/group', + 'path' => array( 'styles', 'blocks', 'core/group' ), + 'selector' => '.wp-block-group', + 'selectors' => array( + 'root' => '.wp-block-group', + ), + ); + + $expected_styles = ":root :where(.wp-block-group){background-image: linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%), url('http://example.org/image.png');}"; + $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $group_node ), 'Styles returned from "::get_styles_for_block()" with combined background gradient and image do not match expectations' ); + } + /** * @ticket 56467 */