diff --git a/library/HTMLPurifier/AttrDef/CSS/Color.php b/library/HTMLPurifier/AttrDef/CSS/Color.php
index e64bc936..2773487f 100644
--- a/library/HTMLPurifier/AttrDef/CSS/Color.php
+++ b/library/HTMLPurifier/AttrDef/CSS/Color.php
@@ -29,39 +29,63 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
return $colors[$lower];
}
- if (preg_match('#(rgb|rgba)\(#', $color, $matches) === 1) {
- // get used function : rgb or rgba
- $function = $matches[1];
- if ($function == 'rgba') {
- $parameters_size = 4;
- } else {
- $parameters_size = 3;
- }
-
- // rgb literal handling
+ if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) {
$length = strlen($color);
if (strpos($color, ')') !== $length - 1) {
return false;
}
- $values = substr($color, strlen($function) + 1, $length - strlen($function) - 2);
+ // get used function : rgb, rgba, hsl or hsla
+ $function = $matches[1];
+
+ $parameters_size = 3;
+ $alpha_channel = false;
+ if (substr($function, -1) === 'a') {
+ $parameters_size = 4;
+ $alpha_channel = true;
+ }
+
+ /*
+ * Allowed types for values :
+ * parameter_position => [type => max_value]
+ */
+ $allowed_types = [
+ 1 => ['percentage' => 100, 'integer' => 255],
+ 2 => ['percentage' => 100, 'integer' => 255],
+ 3 => ['percentage' => 100, 'integer' => 255],
+ ];
+ $allow_different_types = false;
+
+ if (strpos($function, 'hsl') !== false) {
+ $allowed_types = [
+ 1 => ['integer' => 360],
+ 2 => ['percentage' => 100],
+ 3 => ['percentage' => 100],
+ ];
+ $allow_different_types = true;
+ }
+
+ $values = trim(str_replace($function, '', $color), ' ()');
$parts = explode(',', $values);
if (count($parts) !== $parameters_size) {
return false;
}
- $type = false; // to ensure that they're all the same type
+
+ $type = false;
$new_parts = array();
$i = 0;
+
foreach ($parts as $part) {
$i++;
$part = trim($part);
+
if ($part === '') {
return false;
}
// different check for alpha channel
- if ($function === 'rgba' && $i === count($parts)) {
+ if ($alpha_channel === true && $i === count($parts)) {
$result = (new HTMLPurifier_AttrDef_CSS_AlphaValue())->validate($part, $config, $context);
if ($result === false) {
@@ -72,41 +96,37 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
continue;
}
- $length = strlen($part);
- if ($part[$length - 1] === '%') {
- // handle percents
- if (!$type) {
- $type = 'percentage';
- } elseif ($type !== 'percentage') {
- return false;
- }
- $num = (float)substr($part, 0, $length - 1);
- if ($num < 0) {
- $num = 0;
- }
- if ($num > 100) {
- $num = 100;
- }
- $new_parts[] = "$num%";
+ if (substr($part, -1) === '%') {
+ $current_type = 'percentage';
} else {
- // handle integers
- if (!$type) {
- $type = 'integer';
- } elseif ($type !== 'integer') {
- return false;
- }
- $num = (int)$part;
- if ($num < 0) {
- $num = 0;
- }
- if ($num > 255) {
- $num = 255;
- }
- $new_parts[] = (string)$num;
+ $current_type = 'integer';
+ }
+
+ if (!array_key_exists($current_type, $allowed_types[$i])) {
+ return false;
+ }
+
+ if (!$type) {
+ $type = $current_type;
+ }
+
+ if ($allow_different_types === false && $type != $current_type) {
+ return false;
+ }
+
+ $max_value = $allowed_types[$i][$current_type];
+
+ if ($current_type == 'integer') {
+ // Return value between range 0 -> $max_value
+ $new_parts[] = (int)max(min($part, $max_value), 0);
+ } elseif ($current_type == 'percentage') {
+ $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%';
}
}
+
$new_values = implode(',', $new_parts);
- $color = "$function($new_values)";
+
+ $color = $function . '(' . $new_values . ')';
} else {
// hexadecimal handling
if ($color[0] === '#') {
diff --git a/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php b/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php
index 9586f063..e6b4d856 100644
--- a/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php
+++ b/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php
@@ -15,14 +15,29 @@ class HTMLPurifier_AttrDef_CSS_ColorTest extends HTMLPurifier_AttrDefHarness
$this->assertDef('rgb(255, 0, 0)', 'rgb(255,0,0)'); // rm spaces
$this->assertDef('rgb(100%,0%,0%)');
$this->assertDef('rgb(50.5%,23.2%,43.9%)'); // decimals okay
+ $this->assertDef('rgb(-5,0,0)', 'rgb(0,0,0)'); // negative values
+ $this->assertDef('rgb(295,0,0)', 'rgb(255,0,0)'); // max values
+ $this->assertDef('rgb(12%,150%,0%)', 'rgb(12%,100%,0%)'); // percentage max values
$this->assertDef('rgba(255, 0, 0, 0)', 'rgba(255,0,0,0)'); // rm spaces
$this->assertDef('rgba(100%,0%,0%,.4)');
$this->assertDef('rgba(38.1%,59.7%,1.8%,0.7)', 'rgba(38.1%,59.7%,1.8%,.7)'); // decimals okay
+ $this->assertDef('hsl(275, 45%, 81%)', 'hsl(275,45%,81%)'); // rm spaces
+ $this->assertDef('hsl(100,0%,0%)');
+ $this->assertDef('hsl(38,59.7%,1.8%)', 'hsl(38,59.7%,1.8%)'); // decimals okay
+ $this->assertDef('hsl(-11,-15%,25%)', 'hsl(0,0%,25%)'); // negative values
+ $this->assertDef('hsl(380,125%,0%)', 'hsl(360,100%,0%)'); // max values
+
+ $this->assertDef('hsla(100, 74%, 29%, 0)', 'hsla(100,74%,29%,0)'); // rm spaces
+ $this->assertDef('hsla(154,87%,21%,.4)');
+ $this->assertDef('hsla(45,94.3%,4.1%,0.7)', 'hsla(45,94.3%,4.1%,.7)'); // decimals okay
+
$this->assertDef('#G00', false);
$this->assertDef('cmyk(40, 23, 43, 23)', false);
- $this->assertDef('rgb(0%, 23, 68%)', false);
+ $this->assertDef('rgb(0%, 23, 68%)', false); // no mixed type
+ $this->assertDef('rgb(231, 144, 28.2%)', false); // no mixed type
+ $this->assertDef('hsl(18%,12%%,89%)', false); // integer, percentage, percentage
// clip numbers outside sRGB gamut
$this->assertDef('rgb(200%, -10%, 0%)', 'rgb(100%,0%,0%)');