Selaa lähdekoodia

:octocat: QROutputInterface::moduleValueIsValid() improved checks & tests

smiley 2 vuotta sitten
vanhempi
commit
60a19b3c94

+ 3 - 3
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;

+ 20 - 8
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 = [];
+
+		foreach(array_values($value) as $i => $val){
+
+			if($i > 2){
+				break;
+			}
 
-		for($i = 0; $i < 3; $i++){
-			// clamp value
-			$v[] = (int)max(0, min(255, $value[$i]));
+			$values[] = max(0, min(255, intval($val)));
 		}
 
-		return $v;
+		return $values;
 	}
 
 	/**

+ 22 - 10
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 = [];
+
+		foreach(array_values($value) as $i => $val){
+
+			if($i > 2){
+				break;
+			}
 
-		for($i = 0; $i < 3; $i++){
-			// clamp value
-			$v[] = (int)max(0, min(255, $value[$i]));
+			$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');

+ 31 - 3
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);

+ 30 - 2
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;
 	}
 
 	/**

+ 22 - 1
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
 	 */

+ 1 - 1
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;
 

+ 12 - 0
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
 	 */

+ 11 - 0
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
 	 */

+ 11 - 0
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
 	 */

+ 24 - 0
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
 	 */

+ 26 - 0
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],
+		];
+	}
+
 }

+ 22 - 0
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
 	 */

+ 15 - 2
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
 	 */

+ 9 - 0
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],
+		];
+	}
+
 }