diff --git a/globals/sanitize-functions.php b/globals/sanitize-functions.php index 47fe9d1489..c2143336b0 100644 --- a/globals/sanitize-functions.php +++ b/globals/sanitize-functions.php @@ -10,17 +10,48 @@ */ /** - * Function to sanitize alpha color. + * Check whether a value is a well formed CSS variable expression. * - * @param string $value Hex or RGBA color. + * @param mixed $value Value to check. + * + * @return bool + */ +function neve_is_css_var( $value ) { + if ( ! is_string( $value ) ) { + return false; + } + + $value = trim( $value ); + + if ( $value === '' || strlen( $value ) > 200 ) { + return false; + } + + $hex = '#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})'; + $color_fn = '(?:rgb|rgba|hsl|hsla)\(\s*[0-9a-z.,%\/\s-]+\)'; + $keyword = '[a-z]+(?:-[a-z]+)*'; + $fallback = '(?:(?P>nv_var)|' . $hex . '|' . $color_fn . '|' . $keyword . ')'; + $pattern = '/^(?Pvar\(\s*--[a-z0-9_-]+\s*(?:,\s*' . $fallback . '\s*)?\))$/i'; + + return (bool) preg_match( $pattern, $value ); +} + +/** + * Sanitize a color control value. + * + * @param mixed $value Color value: CSS variable, gradient, rgba() or hex. * * @return string */ function neve_sanitize_colors( $value ) { - $is_var = ( strpos( $value, 'var' ) !== false ); + if ( ! is_string( $value ) && ! is_numeric( $value ) ) { + return ''; + } + + $value = (string) $value; - if ( $is_var ) { - return sanitize_text_field( $value ); + if ( neve_is_css_var( $value ) ) { + return trim( $value ); } if ( false !== strpos( $value, 'gradient' ) ) { @@ -31,19 +62,25 @@ function neve_sanitize_colors( $value ) { $mode = ( false === strpos( $value, 'rgba' ) ) ? 'hex' : 'rgba'; if ( 'rgba' === $mode ) { return neve_sanitize_rgba( $value ); - } else { - return sanitize_hex_color( $value ); } + + $hex_color = sanitize_hex_color( $value ); + + return null === $hex_color ? '' : $hex_color; } /** * Sanitize rgba color. * - * @param string $value Color in rgba format. + * @param mixed $value Color in rgba format. * * @return string */ function neve_sanitize_rgba( $value ) { + if ( ! is_string( $value ) ) { + return 'rgba(0,0,0,0)'; + } + $red = 'rgba(0,0,0,0)'; $green = 'rgba(0,0,0,0)'; $blue = 'rgba(0,0,0,0)'; @@ -165,18 +202,17 @@ function neve_sanitize_background( $value ) { } - $value['imageUrl'] = esc_url( $value['imageUrl'] ); - $value['colorValue'] = neve_sanitize_colors( $value['colorValue'] ); - $value['overlayColorValue'] = neve_sanitize_colors( $value['overlayColorValue'] ); - + $value['imageUrl'] = esc_url( $value['imageUrl'] ?? '' ); + $value['colorValue'] = neve_sanitize_colors( $value['colorValue'] ?? '' ); + $value['overlayColorValue'] = neve_sanitize_colors( $value['overlayColorValue'] ?? '' ); - $value['overlayOpacity'] = (int) $value['overlayOpacity']; + $value['overlayOpacity'] = (int) ( $value['overlayOpacity'] ?? 0 ); if ( $value['overlayOpacity'] > 100 || $value['overlayOpacity'] < 0 ) { $value['overlayOpacity'] = 50; } - $value['fixed'] = (bool) $value['fixed']; - $value['useFeatured'] = (bool) $value['useFeatured']; + $value['fixed'] = (bool) ( $value['fixed'] ?? false ); + $value['useFeatured'] = (bool) ( $value['useFeatured'] ?? false ); return $value; } diff --git a/tests/test-neve-sanitization.php b/tests/test-neve-sanitization.php index fbd32d8958..5281d25bea 100644 --- a/tests/test-neve-sanitization.php +++ b/tests/test-neve-sanitization.php @@ -134,6 +134,88 @@ public function test_sanitize_responsive_int_json() { $this->do_assertion_for_sanitize_responsive_int_json( $input_value, $expected_value ); } + /** + * Test that color sanitization does not fatal on array or non-string values. + */ + public function test_sanitize_colors_with_non_string_values() { + $this->assertSame( '', neve_sanitize_colors( [ '#ffffff', '#000000' ] ) ); + $this->assertSame( '', neve_sanitize_colors( [ 'mobile' => '#ffffff' ] ) ); + $this->assertSame( '', neve_sanitize_colors( [] ) ); + + // Other non-color values are rejected too. + $this->assertSame( '', neve_sanitize_colors( null ) ); + $this->assertSame( '', neve_sanitize_colors( true ) ); + $this->assertSame( '', neve_sanitize_colors( new stdClass() ) ); + + // Invalid strings return an empty string, never null. + $this->assertSame( '', neve_sanitize_colors( 'not-a-color' ) ); + $this->assertSame( '', neve_sanitize_colors( '' ) ); + + // Valid values keep working as before. + $this->assertSame( '#ff0000', neve_sanitize_colors( '#ff0000' ) ); + $this->assertSame( 'rgba(255,0,0,1)', neve_sanitize_colors( 'rgba(255, 0, 0, 1)' ) ); + $this->assertSame( 'var(--nv-primary-accent)', neve_sanitize_colors( 'var(--nv-primary-accent)' ) ); + } + + /** + * Test that only well formed CSS variable expressions are treated as CSS variables. + */ + public function test_is_css_var() { + $valid = [ + 'var(--nv-primary-accent)', + 'var(--nv-c-1,#E5E7EB)', + 'var(--nv-c-1, #e5e7eb)', + 'var( --nv-site-bg , #fff )', + 'var(--secondarybtnbg, transparent)', + 'var(--x, rgba(0,0,0,.5))', + 'var(--x, hsl(120 50% 50%))', + 'var(--x, var(--y, #fff))', + ]; + + foreach ( $valid as $value ) { + $this->assertTrue( neve_is_css_var( $value ), $value . ' should be a CSS var' ); + } + + $invalid = [ + 'avatar', + 'varsity', + '#var', + 'var', + 'var()', + 'var(--)', + 'var(--x', + 'var(--x))', + 'var(--x);color:red', + 'var(--x)}body{background:red}', + 'var(--x, url(evil.css))', + 'var(--x, "quoted")', + 'linear-gradient(var(--a), var(--b))', + '', + [ 'var(--x)' ], + null, + ]; + + foreach ( $invalid as $value ) { + $this->assertFalse( neve_is_css_var( $value ), var_export( $value, true ) . ' should not be a CSS var' ); + } + } + + /** + * Test that color sanitization does not pass off arbitrary strings as CSS variables. + */ + public function test_sanitize_colors_rejects_fake_css_vars() { + // Strings that merely contain "var" are not CSS variables. + $this->assertSame( '', neve_sanitize_colors( 'avatar' ) ); + $this->assertSame( '', neve_sanitize_colors( 'var(--x);color:red' ) ); + $this->assertSame( '', neve_sanitize_colors( 'var(--x)}body{background:red}' ) ); + $this->assertSame( '', neve_sanitize_colors( 'var(--x, url(evil.css))' ) ); + + // Well formed CSS variables pass through untouched. + $this->assertSame( 'var(--nv-site-bg)', neve_sanitize_colors( 'var(--nv-site-bg)' ) ); + $this->assertSame( 'var(--nv-c-1, #e5e7eb)', neve_sanitize_colors( ' var(--nv-c-1, #e5e7eb) ' ) ); + $this->assertSame( 'var(--x, var(--y, #fff))', neve_sanitize_colors( 'var(--x, var(--y, #fff))' ) ); + } + /** * Private reusable function for the assertion of sanitize responsive int json. *