diff --git a/src/Output/QREps.php b/src/Output/QREps.php index 13c6f824f..f98bddcd3 100644 --- a/src/Output/QREps.php +++ b/src/Output/QREps.php @@ -32,7 +32,7 @@ class QREps extends QROutputAbstract{ } // check the first values of the array - foreach($value as $i => $val){ + foreach(array_values($value) as $i => $val){ if($i > 3){ break; @@ -61,8 +61,8 @@ class QREps extends QROutputAbstract{ break; } - // clamp value and convert from 0-255 to 0-1 RGB/CMYK range - $values[] = round((max(0, min(255, $val)) / 255), 6); + // clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range + $values[] = round((max(0, min(255, intval($val))) / 255), 6); } return $values; diff --git a/src/Output/QRFpdf.php b/src/Output/QRFpdf.php index 9fd23f3b2..1ecabc540 100644 --- a/src/Output/QRFpdf.php +++ b/src/Output/QRFpdf.php @@ -15,7 +15,7 @@ use chillerlan\QRCode\Data\QRMatrix; use chillerlan\Settings\SettingsContainerInterface; use FPDF; -use function class_exists, count, is_array, is_numeric, max, min; +use function array_values, class_exists, count, intval, is_array, is_numeric, max, min; /** * QRFpdf output module (requires fpdf) @@ -54,27 +54,39 @@ class QRFpdf extends QROutputAbstract{ } // check the first 3 values of the array - for($i = 0; $i < 3; $i++){ - if(!is_numeric($value[$i])){ + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + if(!is_numeric($val)){ return false; } + } return true; } /** + * @param array $value + * * @inheritDoc */ protected function getModuleValue($value):array{ - $v = []; + $values = []; - for($i = 0; $i < 3; $i++){ - // clamp value - $v[] = (int)max(0, min(255, $value[$i])); + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + $values[] = max(0, min(255, intval($val))); } - return $v; + return $values; } /** diff --git a/src/Output/QRGdImage.php b/src/Output/QRGdImage.php index fa0c9026c..0299ceb9f 100644 --- a/src/Output/QRGdImage.php +++ b/src/Output/QRGdImage.php @@ -15,9 +15,9 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\Data\QRMatrix; use chillerlan\Settings\SettingsContainerInterface; use ErrorException, Throwable; -use function count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor, - imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, is_array, is_numeric, - max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler; +use function array_values, count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor, + imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, intval, + is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler; /** * Converts the matrix into GD images, raw or base64 output (requires ext-gd) @@ -89,29 +89,41 @@ class QRGdImage extends QROutputAbstract{ } // check the first 3 values of the array - for($i = 0; $i < 3; $i++){ - if(!is_numeric($value[$i])){ + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + if(!is_numeric($val)){ return false; } + } return true; } /** + * @param array $value + * * @inheritDoc * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ protected function getModuleValue($value):int{ - $v = []; + $values = []; - for($i = 0; $i < 3; $i++){ - // clamp value - $v[] = (int)max(0, min(255, $value[$i])); + foreach(array_values($value) as $i => $val){ + + if($i > 2){ + break; + } + + $values[] = max(0, min(255, intval($val))); } /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ - $color = imagecolorallocate($this->image, ...$v); + $color = imagecolorallocate($this->image, ...$values); if($color === false){ throw new QRCodeOutputException('could not set color: imagecolorallocate() error'); diff --git a/src/Output/QRImagick.php b/src/Output/QRImagick.php index 19b8e5352..d8203ce34 100644 --- a/src/Output/QRImagick.php +++ b/src/Output/QRImagick.php @@ -15,7 +15,7 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\Data\QRMatrix; use chillerlan\Settings\SettingsContainerInterface; use finfo, Imagick, ImagickDraw, ImagickPixel; -use function extension_loaded, is_string; +use function extension_loaded, in_array, is_string, preg_match, strlen; use const FILEINFO_MIME_TYPE; /** @@ -60,16 +60,44 @@ class QRImagick extends QROutputAbstract{ } /** - * @todo: check/validate possible values + * note: we're not necessarily validating the several values, just checking the general syntax + * * @see https://www.php.net/manual/imagickpixel.construct.php * @inheritDoc */ public static function moduleValueIsValid($value):bool{ - return is_string($value); + + if(!is_string($value)){ + return false; + } + + $value = trim($value); + + // hex notation + // #rgb(a) + // #rrggbb(aa) + // #rrrrggggbbbb(aaaa) + // ... + if(preg_match('/^#[a-f]+$/i', $value) && in_array((strlen($value) - 1), [3, 4, 6, 8, 9, 12, 16, 24, 32], true)){ + return true; + } + + // css (-like) func(...values) + if(preg_match('#^(graya?|hs(b|la?)|rgba?)\([\d .,%]+\)$#i', $value)){ + return true; + } + + // predefined css color + if(preg_match('/^[a-z]+$/i', $value)){ + return true; + } + + return false; } /** * @inheritDoc + * @throws \ImagickPixelException */ protected function getModuleValue($value):ImagickPixel{ return new ImagickPixel($value); diff --git a/src/Output/QRMarkup.php b/src/Output/QRMarkup.php index c51a4448e..b49014b1e 100644 --- a/src/Output/QRMarkup.php +++ b/src/Output/QRMarkup.php @@ -10,7 +10,7 @@ namespace chillerlan\QRCode\Output; -use function is_string, strip_tags, trim; +use function is_string, preg_match, strip_tags, trim; /** * Abstract for markup types: HTML, SVG, ... XML anyone? @@ -18,10 +18,38 @@ use function is_string, strip_tags, trim; abstract class QRMarkup extends QROutputAbstract{ /** + * note: we're not necessarily validating the several values, just checking the general syntax + * note: css4 colors are not included + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value * @inheritDoc */ public static function moduleValueIsValid($value):bool{ - return is_string($value); + + if(!is_string($value)){ + return false; + } + + $value = trim(strip_tags($value), " '\"\r\n\t"); + + // hex notation + // #rgb(a) + // #rrggbb(aa) + if(preg_match('/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i', $value)){ + return true; + } + + // css: hsla/rgba(...values) + if(preg_match('#^(hsla?|rgba?)\([\d .,%/]+\)$#i', $value)){ + return true; + } + + // predefined css color + if(preg_match('/^[a-z]+$/i', $value)){ + return true; + } + + return false; } /** diff --git a/src/Output/QRMarkupSVG.php b/src/Output/QRMarkupSVG.php index b7330f1b8..3c744b1ad 100644 --- a/src/Output/QRMarkupSVG.php +++ b/src/Output/QRMarkupSVG.php @@ -11,7 +11,7 @@ namespace chillerlan\QRCode\Output; use chillerlan\QRCode\Data\QRMatrix; -use function array_chunk, implode, sprintf; +use function array_chunk, implode, is_string, preg_match, sprintf, trim; /** * SVG output @@ -23,6 +23,27 @@ use function array_chunk, implode, sprintf; */ class QRMarkupSVG extends QRMarkup{ + /** + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill + * @inheritDoc + */ + public static function moduleValueIsValid($value):bool{ + + if(!is_string($value)){ + return false; + } + + $value = trim($value); + + // url(...) + if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){ + return true; + } + + // otherwise check for standard css notation + return parent::moduleValueIsValid($value); + } + /** * @inheritDoc */ diff --git a/src/Output/QROutputInterface.php b/src/Output/QROutputInterface.php index 864cab3c8..ee8af95c9 100644 --- a/src/Output/QROutputInterface.php +++ b/src/Output/QROutputInterface.php @@ -89,7 +89,7 @@ interface QROutputInterface{ /** * Determines whether the given value is valid * - * @param mixed|null $value + * @param mixed $value */ public static function moduleValueIsValid($value):bool; diff --git a/tests/Output/QREpsTest.php b/tests/Output/QREpsTest.php index 4182167cf..96b518253 100644 --- a/tests/Output/QREpsTest.php +++ b/tests/Output/QREpsTest.php @@ -19,6 +19,18 @@ class QREpsTest extends QROutputTestAbstract{ protected string $FQN = QREps::class; protected string $type = QROutputInterface::EPS; + public static function moduleValueProvider():array{ + return [ + 'valid: 3 int' => [[123, 123, 123], true], + 'valid: 4 int' => [[123, 123, 123, 123], true], + 'valid: w/invalid extra element' => [[123, 123, 123, 123, 'abc'], true], + 'valid: numeric string' => [['123', '123', '123'], true], + 'invalid: wrong type' => ['foo', false], + 'invalid: array too short' => [[1, 2], false], + 'invalid: contains non-number' => [[1, 'b', 3], false], + ]; + } + /** * @inheritDoc */ diff --git a/tests/Output/QRFpdfTest.php b/tests/Output/QRFpdfTest.php index c55d9bb8c..a117a6259 100644 --- a/tests/Output/QRFpdfTest.php +++ b/tests/Output/QRFpdfTest.php @@ -36,6 +36,17 @@ final class QRFpdfTest extends QROutputTestAbstract{ parent::setUp(); } + public static function moduleValueProvider():array{ + return [ + 'valid: int' => [[123, 123, 123], true], + 'valid: w/invalid extra element' => [[123, 123, 123, 'abc'], true], + 'valid: numeric string' => [['123', '123', '123'], true], + 'invalid: wrong type' => ['foo', false], + 'invalid: array too short' => [[1, 2], false], + 'invalid: contains non-number' => [[1, 'b', 3], false], + ]; + } + /** * @inheritDoc */ diff --git a/tests/Output/QRGdImageTestAbstract.php b/tests/Output/QRGdImageTestAbstract.php index b7c6434e9..69be0239d 100644 --- a/tests/Output/QRGdImageTestAbstract.php +++ b/tests/Output/QRGdImageTestAbstract.php @@ -33,6 +33,17 @@ abstract class QRGdImageTestAbstract extends QROutputTestAbstract{ parent::setUp(); } + public static function moduleValueProvider():array{ + return [ + 'valid: int' => [[123, 123, 123], true], + 'valid: w/invalid extra element' => [[123, 123, 123, 'abc'], true], + 'valid: numeric string' => [['123', '123', '123'], true], + 'invalid: wrong type' => ['foo', false], + 'invalid: array too short' => [[1, 2], false], + 'invalid: contains non-number' => [[1, 'b', 3], false], + ]; + } + /** * @inheritDoc */ diff --git a/tests/Output/QRImagickTest.php b/tests/Output/QRImagickTest.php index 050f98012..9f2fb7fb4 100644 --- a/tests/Output/QRImagickTest.php +++ b/tests/Output/QRImagickTest.php @@ -37,6 +37,30 @@ final class QRImagickTest extends QROutputTestAbstract{ parent::setUp(); } + public static function moduleValueProvider():array{ + return [ + 'invalid: wrong type' => [[], false], + 'valid: hex color (3)' => ['#abc', true], + 'valid: hex color (4)' => ['#abcd', true], + 'valid: hex color (6)' => ['#aabbcc', true], + 'valid: hex color (8)' => ['#aabbccdd', true], + 'valid: hex color (32)' => ['#aaaaaaaabbbbbbbbccccccccdddddddd', true], + 'invalid: hex color (non-hex)' => ['#aabbcxyz', false], + 'invalid: hex color (too short)' => ['#aa', false], + 'invalid: hex color (too long)' => ['#aaaaaaaabbbbbbbbccccccccdddddddd00', false], + 'invalid: hex color (5)' => ['#aabbc', false], + 'invalid: hex color (7)' => ['#aabbccd', false], + 'valid: rgb(...%)' => ['rgb(100.0%, 0.0%, 0.0%)', true], + 'valid: rgba(...)' => [' rgba(255, 0, 0, 1.0) ', true], + 'valid: hsb(...)' => ['hsb(33.3333%, 100%, 75%)', true], + 'valid: hsla(...)' => ['hsla(120, 255, 191.25, 1.0)', true], + 'invalid: rgba(non-numeric)' => ['rgba(255, 0, whatever, 0, 1.0)', false], + 'invalid: rgba(extra-char)' => ['rgba(255, 0, 0, 1.0);', false], + 'valid: csscolor' => ['purple', true], + 'invalid: c5s c0lor' => ['c5sc0lor', false], + ]; + } + /** * @inheritDoc */ diff --git a/tests/Output/QRMarkupSVGTest.php b/tests/Output/QRMarkupSVGTest.php index b8fe7d494..7f15ac6c6 100644 --- a/tests/Output/QRMarkupSVGTest.php +++ b/tests/Output/QRMarkupSVGTest.php @@ -20,4 +20,30 @@ final class QRMarkupSVGTest extends QRMarkupTestAbstract{ protected string $FQN = QRMarkupSVG::class; protected string $type = QROutputInterface::MARKUP_SVG; + public static function moduleValueProvider():array{ + return [ + // css colors from parent + 'valid: hex color (3)' => ['#abc', true], + 'valid: hex color (4)' => ['#abcd', true], + 'valid: hex color (6)' => ['#aabbcc', true], + 'valid: hex color (8)' => ['#aabbccdd', true], + 'invalid: hex color (non-hex)' => ['#aabbcxyz', false], + 'invalid: hex color (too short)' => ['#aa', false], + 'invalid: hex color (5)' => ['#aabbc', false], + 'invalid: hex color (7)' => ['#aabbccd', false], + 'valid: rgb(...%)' => ['rgb(100.0%, 0.0%, 0.0%)', true], + 'valid: rgba(...)' => [' rgba(255, 0, 0, 1.0) ', true], + 'valid: hsl(...)' => ['hsl(120, 60%, 50%)', true], + 'valid: hsla(...)' => ['hsla(120, 255, 191.25, 1.0)', true], + 'invalid: rgba(non-numeric)' => ['rgba(255, 0, whatever, 0, 1.0)', false], + 'invalid: rgba(extra-char)' => ['rgba(255, 0, 0, 1.0);', false], + 'valid: csscolor' => ['purple', true], + 'invalid: c5sc0lor' => ['c5sc0lor', false], + + // SVG + 'valid: url(#id)' => ['url(#fillGradient)', true], + 'invalid: url(link)' => ['url(https://example.com/noop)', false], + ]; + } + } diff --git a/tests/Output/QRMarkupTestAbstract.php b/tests/Output/QRMarkupTestAbstract.php index 81293de6f..abe98301c 100644 --- a/tests/Output/QRMarkupTestAbstract.php +++ b/tests/Output/QRMarkupTestAbstract.php @@ -17,6 +17,28 @@ use chillerlan\QRCode\Data\QRMatrix; */ abstract class QRMarkupTestAbstract extends QROutputTestAbstract{ + public static function moduleValueProvider():array{ + return [ + 'invalid: wrong type' => [[], false], + 'valid: hex color (3)' => ['#abc', true], + 'valid: hex color (4)' => ['#abcd', true], + 'valid: hex color (6)' => ['#aabbcc', true], + 'valid: hex color (8)' => ['#aabbccdd', true], + 'invalid: hex color (non-hex)' => ['#aabbcxyz', false], + 'invalid: hex color (too short)' => ['#aa', false], + 'invalid: hex color (5)' => ['#aabbc', false], + 'invalid: hex color (7)' => ['#aabbccd', false], + 'valid: rgb(...%)' => ['rgb(100.0%, 0.0%, 0.0%)', true], + 'valid: rgba(...)' => [' rgba(255, 0, 0, 1.0) ', true], + 'valid: hsl(...)' => ['hsl(120, 60%, 50%)', true], + 'valid: hsla(...)' => ['hsla(120, 255, 191.25, 1.0)', true], + 'invalid: rgba(non-numeric)' => ['rgba(255, 0, whatever, 0, 1.0)', false], + 'invalid: rgba(extra-char)' => ['rgba(255, 0, 0, 1.0);', false], + 'valid: csscolor' => ['purple', true], + 'invalid: c5sc0lor' => ['c5sc0lor', false], + ]; + } + /** * @inheritDoc */ diff --git a/tests/Output/QROutputTestAbstract.php b/tests/Output/QROutputTestAbstract.php index a8c3b6b7a..97487078b 100644 --- a/tests/Output/QROutputTestAbstract.php +++ b/tests/Output/QROutputTestAbstract.php @@ -67,15 +67,28 @@ abstract class QROutputTestAbstract extends TestCase{ $this->outputInterface->dump('/foo/bar.test'); } + abstract public static function moduleValueProvider():array; + /** - * covers the module values settings + * @param mixed $value + * @param bool $expected + * + * @dataProvider moduleValueProvider */ - abstract public function testSetModuleValues():void; + public function testValidateModuleValues($value, bool $expected):void{ + /** @noinspection PhpUndefinedMethodInspection */ + $this::assertSame($expected, $this->FQN::moduleValueIsValid($value)); + } /* * additional, non-essential, potentially inaccurate coverage tests */ + /** + * covers the module values settings + */ + abstract public function testSetModuleValues():void; + /** * coverage of the built-in output modules */ diff --git a/tests/Output/QRStringTestAbstract.php b/tests/Output/QRStringTestAbstract.php index 18e13e52d..db29d667e 100644 --- a/tests/Output/QRStringTestAbstract.php +++ b/tests/Output/QRStringTestAbstract.php @@ -19,4 +19,13 @@ abstract class QRStringTestAbstract extends QROutputTestAbstract{ protected string $FQN = QRString::class; + public static function moduleValueProvider():array{ + return [ + 'invalid: wrong type' => [[], false], + 'valid: string' => ['abc', true], + 'valid: zero length string' => ['', true], + 'valid: empty string' => [' ', true], + ]; + } + }