diff --git a/4dev/tests/CoreLibsCheckColorsTest.php b/4dev/tests/CoreLibsCheckColorsTest.php new file mode 100644 index 00000000..7386bf76 --- /dev/null +++ b/4dev/tests/CoreLibsCheckColorsTest.php @@ -0,0 +1,329 @@ + [ + '#ab12cd', + null, + true, + ], + 'valid hex rgb, flag ALL' => [ + '#ab12cd', + \CoreLibs\Check\Colors::ALL, + true, + ], + 'valid hex rgb, flag HEX_RGB' => [ + '#ab12cd', + \CoreLibs\Check\Colors::HEX_RGB, + true, + ], + 'valid hex rgb, wrong flag' => [ + '#ab12cd', + \CoreLibs\Check\Colors::RGB, + false, + ], + // error + 'invalid hex rgb A' => [ + '#ab12zz', + null, + false, + ], + 'invalid hex rgb B' => [ + '#ZyQfo', + null, + false, + ], + // other valid hex checks + 'valid hex rgb, alt A' => [ + '#AB12cd', + null, + true, + ], + // * hax alpha + 'valid hex rgb alpha, flag ALL (default)' => [ + '#ab12cd12', + null, + true, + ], + 'valid hex rgb alpha, flag ALL' => [ + '#ab12cd12', + \CoreLibs\Check\Colors::ALL, + true, + ], + 'valid hex rgb alpha, flag HEX_RGBA' => [ + '#ab12cd12', + \CoreLibs\Check\Colors::HEX_RGBA, + true, + ], + 'valid hex rgb alpha, wrong flag' => [ + '#ab12cd12', + \CoreLibs\Check\Colors::RGB, + false, + ], + // error + 'invalid hex rgb alpha A' => [ + '#ab12dd1', + null, + false, + ], + 'invalid hex rgb alpha B' => [ + '#ab12ddzz', + null, + false, + ], + 'valid hex rgb alpha, alt A' => [ + '#ab12cdEE', + null, + true, + ], + // * rgb + 'valid rgb, flag ALL (default)' => [ + 'rgb(255, 10, 20)', + null, + true, + ], + 'valid rgb, flag ALL' => [ + 'rgb(255, 10, 20)', + \CoreLibs\Check\Colors::ALL, + true, + ], + 'valid rgb, flag RGB' => [ + 'rgb(255, 10, 20)', + \CoreLibs\Check\Colors::RGB, + true, + ], + 'valid rgb, wrong flag' => [ + 'rgb(255, 10, 20)', + \CoreLibs\Check\Colors::HEX_RGB, + false, + ], + // error + 'invalid rgb A' => [ + 'rgb(356, 10, 20)', + null, + false, + ], + // other valid rgb conbinations + 'valid rgb, alt A (percent)' => [ + 'rgb(100%, 10%, 20%)', + null, + true, + ], + // TODO check all % and non percent combinations + 'valid rgb, alt B (percent, mix)' => [ + 'rgb(100%, 10, 40)', + null, + true, + ], + // * rgb alpha + 'valid rgba, flag ALL (default)' => [ + 'rgba(255, 10, 20, 0.5)', + null, + true, + ], + 'valid rgba, flag ALL' => [ + 'rgba(255, 10, 20, 0.5)', + \CoreLibs\Check\Colors::ALL, + true, + ], + 'valid rgba, flag RGB' => [ + 'rgba(255, 10, 20, 0.5)', + \CoreLibs\Check\Colors::RGBA, + true, + ], + 'valid rgba, wrong flag' => [ + 'rgba(255, 10, 20, 0.5)', + \CoreLibs\Check\Colors::HEX_RGB, + false, + ], + // error + 'invalid rgba A' => [ + 'rgba(356, 10, 20, 0.5)', + null, + false, + ], + // other valid rgba combinations + 'valid rgba, alt A (percent)' => [ + 'rgba(100%, 10%, 20%, 0.5)', + null, + true, + ], + // TODO check all % and non percent combinations + 'valid rgba, alt B (percent, mix)' => [ + 'rgba(100%, 10, 40, 0.5)', + null, + true, + ], + // TODO check all % and non percent combinations with percent transparent + 'valid rgba, alt C (percent transparent)' => [ + 'rgba(100%, 10%, 20%, 50%)', + null, + true, + ], + /* + // hsl + 'hsl(100, 50%, 60%)', + 'hsl(100, 50.5%, 60.5%)', + 'hsla(100, 50%, 60%)', + 'hsla(100, 50.5%, 60.5%)', + 'hsla(100, 50%, 60%, 0.5)', + 'hsla(100, 50.5%, 60.5%, 0.5)', + 'hsla(100, 50%, 60%, 50%)', + 'hsla(100, 50.5%, 60.5%, 50%)', + */ + // * hsl + 'valid hsl, flag ALL (default)' => [ + 'hsl(100, 50%, 60%)', + null, + true, + ], + 'valid hsl, flag ALL' => [ + 'hsl(100, 50%, 60%)', + \CoreLibs\Check\Colors::ALL, + true, + ], + 'valid hsl, flag RGB' => [ + 'hsl(100, 50%, 60%)', + \CoreLibs\Check\Colors::HSL, + true, + ], + 'valid hsl, wrong flag' => [ + 'hsl(100, 50%, 60%)', + \CoreLibs\Check\Colors::HEX_RGB, + false, + ], + 'invalid hsl A' => [ + 'hsl(500, 50%, 60%)', + null, + false, + ], + 'valid hsl, alt A' => [ + 'hsl(100, 50.5%, 60.5%)', + null, + true, + ], + // * hsl alpha + 'valid hsla, flag ALL (default)' => [ + 'hsla(100, 50%, 60%, 0.5)', + null, + true, + ], + 'valid hsla, flag ALL' => [ + 'hsla(100, 50%, 60%, 0.5)', + \CoreLibs\Check\Colors::ALL, + true, + ], + 'valid hsla, flag RGB' => [ + 'hsla(100, 50%, 60%, 0.5)', + \CoreLibs\Check\Colors::HSLA, + true, + ], + 'valid hsla, wrong flag' => [ + 'hsla(100, 50%, 60%, 0.5)', + \CoreLibs\Check\Colors::HEX_RGB, + false, + ], + 'invalid hsla A' => [ + 'hsla(500, 50%, 60%, 0.5)', + null, + false, + ], + 'valid hsla, alt A (percent alpha' => [ + 'hsla(100, 50%, 60%, 50%)', + null, + true, + ], + 'valid hsla, alt A (percent alpha' => [ + 'hsla(100, 50.5%, 60.5%, 50%)', + null, + true, + ], + // * combined flag checks + 'valid rgb, flag RGB|RGBA' => [ + 'rgb(100%, 10%, 20%)', + \CoreLibs\Check\Colors::RGB | \CoreLibs\Check\Colors::RGBA, + true, + ], + // TODO other combined flag checks all combinations + // * invalid string + 'invalid string A' => [ + 'invalid string', + null, + false, + ], + 'invalid string B' => [ + '(hsla(100, 100, 100))', + null, + false, + ], + 'invalid string C' => [ + 'hsla(100, 100, 100', + null, + false, + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::validateColor + * @dataProvider validateColorProvider + * @testdox validateColor $input with flags $flags be $expected [$_dataName] + * + * @param string $input + * @param int|null $flags + * @param bool $expected + * @return void + */ + public function testValidateColor(string $input, ?int $flags, bool $expected) + { + if ($flags === null) { + $result = \CoreLibs\Check\Colors::validateColor($input); + } else { + $result = \CoreLibs\Check\Colors::validateColor($input, $flags); + } + $this->assertEquals( + $expected, + $result + ); + } + + /** + * Undocumented function + * + * @covers ::validateColor + * @testWith [99] + * @testdox Check Exception throw for $flag + * + * @param int $flag + * @return void + */ + public function testValidateColorException(int $flag): void + { + $this->expectException(\Exception::class); + \CoreLibs\Check\Colors::validateColor('#ffffff', $flag); + } +} + +// __END__ diff --git a/4dev/tests/CoreLibsConvertColorsTest.php b/4dev/tests/CoreLibsConvertColorsTest.php index d58ce903..7b288f0c 100644 --- a/4dev/tests/CoreLibsConvertColorsTest.php +++ b/4dev/tests/CoreLibsConvertColorsTest.php @@ -122,6 +122,8 @@ final class CoreLibsConvertColorsTest extends TestCase */ public function rgb2hslAndhsbList(): array { + // if hsb_from or hsl_from is set, this will be used in hsb/hsl convert + // hsb_rgb is used for adjusted rgb valus due to round error to in return [ 'valid gray' => [ 'rgb' => [12, 12, 12], @@ -137,6 +139,16 @@ final class CoreLibsConvertColorsTest extends TestCase 'hsl' => [211.6, 90.5, 41.2], 'valid' => true, ], + // hsg/hsl with 360 which is seen as 0 + 'valid color hue 360' => [ + 'rgb' => [200, 10, 10], + 'hsb' => [0, 95, 78.0], + 'hsb_from' => [360, 95, 78.0], + 'hsb_rgb' => [199, 10, 10], // should be rgb, but rounding error + 'hsl' => [0.0, 90.5, 41.2], + 'hsl_from' => [360.0, 90.5, 41.2], + 'valid' => true, + ], // invalid values 'invalid color' => [ 'rgb' => [-12, 300, 12], @@ -176,9 +188,9 @@ final class CoreLibsConvertColorsTest extends TestCase $list = []; foreach ($this->rgb2hslAndhsbList() as $name => $values) { $list[$name . ', hsb to rgb'] = [ - 0 => $values['hsb'][0], - 1 => $values['hsb'][1], - 2 => $values['hsb'][2], + 0 => $values['hsb_from'][0] ?? $values['hsb'][0], + 1 => $values['hsb_from'][1] ?? $values['hsb'][1], + 2 => $values['hsb_from'][2] ?? $values['hsb'][2], 3 => $values['valid'] ? $values['hsb_rgb'] : false ]; } @@ -214,9 +226,9 @@ final class CoreLibsConvertColorsTest extends TestCase $list = []; foreach ($this->rgb2hslAndhsbList() as $name => $values) { $list[$name . ', hsl to rgb'] = [ - 0 => $values['hsl'][0], - 1 => $values['hsl'][1], - 2 => $values['hsl'][2], + 0 => $values['hsl_from'][0] ?? $values['hsl'][0], + 1 => $values['hsl_from'][1] ?? $values['hsl'][1], + 2 => $values['hsl_from'][2] ?? $values['hsl'][2], 3 => $values['valid'] ? $values['rgb'] : false ]; } @@ -382,6 +394,27 @@ final class CoreLibsConvertColorsTest extends TestCase \CoreLibs\Convert\Colors::hsl2rgb($input_h, $input_s, $input_l) ); } + + /** + * edge case check hsl/hsb and hue 360 (= 0) + * + * @covers ::hsl2rgb + * @covers ::hsb2rgb + * @testdox hsl2rgb/hsb2rgb hue 360 valid check + * + * @return void + */ + public function testHslHsb360hue(): void + { + $this->assertNotFalse( + \CoreLibs\Convert\Colors::hsl2rgb(360.0, 90.5, 41.2), + 'HSL to RGB with 360 hue' + ); + $this->assertNotFalse( + \CoreLibs\Convert\Colors::hsb2rgb(360, 95, 78.0), + 'HSB to RGB with 360 hue' + ); + } } // __END__ diff --git a/www/admin/class_test.check.colors.php b/www/admin/class_test.check.colors.php new file mode 100644 index 00000000..9ca898ad --- /dev/null +++ b/www/admin/class_test.check.colors.php @@ -0,0 +1,123 @@ + BASE . LOG, + 'file_id' => $LOG_FILE_ID, + // add file date + 'print_file_date' => true, + // set debug and print flags + 'debug_all' => $DEBUG_ALL ?? false, + 'echo_all' => $ECHO_ALL ?? false, + 'print_all' => $PRINT_ALL ?? false, +]); + +$PAGE_NAME = 'TEST CLASS: CHECK COLORS'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print '
Class Test Master
'; +print '

' . $PAGE_NAME . '

'; + +// list of colors to check +$css_colors = [ + // base hex + '#ab12cd', + '#ab12cd12', + // rgb + 'rgb(255, 10, 20)', + 'rgb(100%, 10%, 20%)', + 'rgba(255, 10, 20)', + 'rgba(100%, 10%, 20%)', + 'rgba(255, 10, 20, 0.5)', + 'rgba(100%, 10%, 20%, 0.5)', + 'rgba(255, 10, 20, 50%)', + 'rgba(100%, 10%, 20%, 50%)', + // hsl + 'hsl(100, 50%, 60%)', + 'hsl(100, 50.5%, 60.5%)', + 'hsla(100, 50%, 60%)', + 'hsla(100, 50.5%, 60.5%)', + 'hsla(100, 50%, 60%, 0.5)', + 'hsla(100, 50.5%, 60.5%, 0.5)', + 'hsla(100, 50%, 60%, 50%)', + 'hsla(100, 50.5%, 60.5%, 50%)', + // invalid here + 'invalid string', + '(hsla(100, 100, 100))', + 'hsla(100, 100, 100', + // invalid numbers + '#zzab99', + '#abcdef0', + 'rgb(255%, 100, 100)', + 'rgb(255%, 100, -10)', + 'rgb(100%, 100, -10)', + 'hsl(370, 100, 10)', + 'hsl(200, 100%, 160%)', +]; + +foreach ($css_colors as $color) { + $check = Colors::validateColor($color); + print "Color check: $color with (" . Colors::ALL . "): "; + if ($check) { + print 'OK'; + } else { + print 'ERROR'; + } + print "
"; +} + +echo "
"; + +// valid rgb/hsl checks +$color = 'hsla(360, 100%, 60%, 0.556)'; +$check = Colors::validateColor($color); +print "Color check: $color with (" . Colors::ALL . "): "; +if ($check) { + print 'OK'; +} else { + print 'ERROR'; +} + +// invalid flag +echo "
"; +try { + $check = Colors::validateColor('#ab12cd', 99); + print "No Exception"; +} catch (\Exception $e) { + print "ERROR: " . $e->getCode() . ": " . $e->getMessage() . "
"; +} + +// error message +print $log->printErrorMsg(); + +print ""; + +// __END__ diff --git a/www/admin/class_test.colors.php b/www/admin/class_test.convert.colors.php similarity index 97% rename from www/admin/class_test.colors.php rename to www/admin/class_test.convert.colors.php index a66a0783..9c0312a4 100644 --- a/www/admin/class_test.colors.php +++ b/www/admin/class_test.convert.colors.php @@ -22,7 +22,7 @@ define('USE_DATABASE', false); // sample config require 'config.php'; // define log file id -$LOG_FILE_ID = 'classTest-colors'; +$LOG_FILE_ID = 'classTest-convert-colors'; ob_end_flush(); use CoreLibs\Convert\Colors; @@ -40,7 +40,7 @@ $log = new CoreLibs\Debug\Logging([ ]); $color_class = 'CoreLibs\Convert\Colors'; -$PAGE_NAME = 'TEST CLASS: COLORS'; +$PAGE_NAME = 'TEST CLASS: CONVERT COLORS'; print ""; print "" . $PAGE_NAME . ""; print ""; diff --git a/www/admin/class_test.php b/www/admin/class_test.php index bb17fbd6..54ea5dd6 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -55,7 +55,8 @@ print ""; print '
Class Test: DB
'; print '
Class Test: DB dbReturn
'; -print '
Class Test: COLORS
'; +print '
Class Test: CONVERT COLORS
'; +print '
Class Test: CHECK COLORS
'; print '
Class Test: MIME
'; print '
Class Test: JSON
'; print '
Class Test: FORM TOKEN
'; diff --git a/www/lib/CoreLibs/ACL/Login.php b/www/lib/CoreLibs/ACL/Login.php index c46671d2..7e4ccf79 100644 --- a/www/lib/CoreLibs/ACL/Login.php +++ b/www/lib/CoreLibs/ACL/Login.php @@ -690,6 +690,7 @@ class Login // rgb(), rgba(), hsl(), hsla() // rgb: nnn.n for each // hsl: nnn.n for first, nnn.n% for 2nd, 3rd + // Check\Colors::validateColor() $_SESSION['LANG'] = $res['locale'] ?? 'en'; $_SESSION['DEFAULT_CHARSET'] = $res['encoding'] ?? 'UTF-8'; $_SESSION['DEFAULT_LOCALE'] = $_SESSION['LANG'] diff --git a/www/lib/CoreLibs/Check/Colors.php b/www/lib/CoreLibs/Check/Colors.php new file mode 100644 index 00000000..2d1d3ae3 --- /dev/null +++ b/www/lib/CoreLibs/Check/Colors.php @@ -0,0 +1,187 @@ + $color_check) { + if (empty($color_check)) { + return false; + } + $percent_check = false; + if (strrpos($color_check, '%', -1) !== false) { + $percent_check = true; + $color_check = str_replace('%', '', $color_check); + } + // first three normal percent or valid number + if ($rgb_flag !== false) { + if ($percent_check === true) { + // for ALL pos + if ($color_check < 0 || $color_check > 100) { + return false; + } + } elseif ( + $pos < 3 && + ($color_check < 0 || $color_check > 255) + ) { + return false; + } elseif ( + // RGBA set pos 3 if not percent + $pos == 3 && + ($color_check < 0 || $color_check > 1) + ) { + return false; + } + } elseif ($hsl_flag !== false) { + // pos 0: 0-360 + // pos 1,2: % + // pos 3: % or 0-1 (float) + if ( + $pos == 0 && + ($color_check < 0 || $color_check > 360) + ) { + return false; + } elseif ( + // if pos 1/2 are not percent + ($pos == 1 || $pos == 2) && + ($percent_check != true || + ($color_check < 0 || $color_check > 100)) + ) { + return false; + } elseif ( + // 3 is either percent or 0~1 + $pos == 3 && + ( + ($percent_check == false && + ($color_check < 0 || $color_check > 1)) || + ($percent_check === true && + ($color_check < 0 || $color_check > 100)) + ) + ) { + return false; + } + } + } + return true; + } + + /** + * check if html/css color string is valid + * @param string $color A color string of any format + * @param int $flags defaults to ALL, else use | to combined from + * HEX_RGB, HEX_RGBA, RGB, RGBA, HSL, HSLA + * @return bool True if valid, False if not + * @throws Exception 1: no valid flag set + */ + public static function validateColor(string $color, int $flags = self::ALL): bool + { + // blocks for each check + $regex_blocks = []; + // set what to check + if ($flags & self::HEX_RGB) { + $regex_blocks[] = '#[\dA-Fa-f]{6}'; + } + if ($flags & self::HEX_RGBA) { + $regex_blocks[] = '#[\dA-Fa-f]{8}'; + } + if ($flags & self::RGB) { + $regex_blocks[] = 'rgb\(\d{1,3}%?,\s*\d{1,3}%?,\s*\d{1,3}%?\)'; + } + if ($flags & self::RGBA) { + $regex_blocks[] = 'rgba\(\d{1,3}%?,\s*\d{1,3}%?,\s*\d{1,3}%?(,\s*(0\.\d{1,2}|1(\.0)?|\d{1,3}%))?\)'; + } + if ($flags & self::HSL) { + $regex_blocks[] = 'hsl\(\d{1,3},\s*\d{1,3}(\.\d{1})?%,\s*\d{1,3}(\.\d{1})?%\)'; + } + if ($flags & self::HSLA) { + $regex_blocks[] = 'hsla\(\d{1,3},\s*\d{1,3}(\.\d{1})?%,\s*\d{1,3}' + . '(\.\d{1})?%(,\s*(0\.\d{1,2}|1(\.0)?|\d{1,3}%))?\)'; + } + // wrong flag set + if ($flags > self::ALL) { + throw new \Exception("Invalid flags parameter: $flags", 1); + } + if (!count($regex_blocks)) { + throw new \Exception("No regex blocks set: $flags", 2); + } + + // build regex + $regex = '^(' + . join('|', $regex_blocks) + // close regex + . ')$'; + // print "C: $color, F: $flags, R: $regex\n"; + + if (preg_match("/$regex/", $color)) { + // if valid regex, we now need to check if the content is actually valid + // only for rgb/hsl type + /** @var int|false */ + $rgb_flag = strpos($color, 'rgb'); + /** @var int|false */ + $hsl_flag = strpos($color, 'hsl'); + // if both not match, return true + if ( + $rgb_flag === false && + $hsl_flag === false + ) { + return true; + } + // run detaul rgb/hsl content check + return self::rgbHslContentCheck($color, $rgb_flag, $hsl_flag); + } else { + return false; + } + } +} + +// __END__ diff --git a/www/vendor/composer/autoload_classmap.php b/www/vendor/composer/autoload_classmap.php index 7a5b8e8e..ad05dece 100644 --- a/www/vendor/composer/autoload_classmap.php +++ b/www/vendor/composer/autoload_classmap.php @@ -12,6 +12,7 @@ return array( 'CoreLibs\\Admin\\Backend' => $baseDir . '/lib/CoreLibs/Admin/Backend.php', 'CoreLibs\\Admin\\EditBase' => $baseDir . '/lib/CoreLibs/Admin/EditBase.php', 'CoreLibs\\Basic' => $baseDir . '/lib/CoreLibs/Basic.php', + 'CoreLibs\\Check\\Colors' => $baseDir . '/lib/CoreLibs/Check/Colors.php', 'CoreLibs\\Check\\Email' => $baseDir . '/lib/CoreLibs/Check/Email.php', 'CoreLibs\\Check\\Encoding' => $baseDir . '/lib/CoreLibs/Check/Encoding.php', 'CoreLibs\\Check\\File' => $baseDir . '/lib/CoreLibs/Check/File.php', diff --git a/www/vendor/composer/autoload_static.php b/www/vendor/composer/autoload_static.php index 5a00df1a..5a5ae608 100644 --- a/www/vendor/composer/autoload_static.php +++ b/www/vendor/composer/autoload_static.php @@ -45,6 +45,7 @@ class ComposerStaticInit10fe8fe2ec4017b8644d2b64bcf398b9 'CoreLibs\\Admin\\Backend' => __DIR__ . '/../..' . '/lib/CoreLibs/Admin/Backend.php', 'CoreLibs\\Admin\\EditBase' => __DIR__ . '/../..' . '/lib/CoreLibs/Admin/EditBase.php', 'CoreLibs\\Basic' => __DIR__ . '/../..' . '/lib/CoreLibs/Basic.php', + 'CoreLibs\\Check\\Colors' => __DIR__ . '/../..' . '/lib/CoreLibs/Check/Colors.php', 'CoreLibs\\Check\\Email' => __DIR__ . '/../..' . '/lib/CoreLibs/Check/Email.php', 'CoreLibs\\Check\\Encoding' => __DIR__ . '/../..' . '/lib/CoreLibs/Check/Encoding.php', 'CoreLibs\\Check\\File' => __DIR__ . '/../..' . '/lib/CoreLibs/Check/File.php',