diff --git a/README.md b/README.md index 5010ea0..153247f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ Security::escHTML('string'); Security::escJS('string'); +Security::escURL('string'); + +Security::escCSS('string'); + Security::isSupportedCharset('string'); ``` @@ -53,6 +57,8 @@ Charsets shortlisted: * escHTML(text: mixed, [charset: string = 'UTF-8']): string * escAttr(text: mixed, [charset: string = 'UTF-8']): string * escJS(text: mixed, [charset: string = 'UTF-8']): string +* escURL(text: mixed, [charset: string = 'UTF-8']): string +* escCSS(text: mixed, [charset: string = 'UTF-8']): string ## How to Dev `composer ci` for php-cs-fixer and phpunit and coverage diff --git a/src/Security.php b/src/Security.php index 57a2e16..a49e084 100644 --- a/src/Security.php +++ b/src/Security.php @@ -233,4 +233,49 @@ public static function escJS($text, string $charset = 'UTF-8'): string return static::convertStringFromUTF8($text, $charset); } + + /** + * @param mixed $text + * @param string $charset + * + * @throws SecurityException + * + * @return string + */ + public static function escURL($text, string $charset = 'UTF-8'): string + { + $text = static::convertStringToUTF8($text, $charset); + + $text = \rawurlencode($text); + + return static::convertStringFromUTF8($text, $charset); + } + + /** + * @param mixed $text + * @param string $charset + * + * @throws SecurityException + * + * @return string + */ + public static function escCSS($text, string $charset = 'UTF-8'): string + { + $text = static::convertStringToUTF8($text, $charset); + + $text = \preg_replace_callback('/[^a-z0-9]/iSu', static function ($matches) { + $chr = $matches[0]; + + if (\strlen($chr) === 1) { + $ord = \ord($chr); + } else { + $chr = \mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); + $ord = \hexdec(\bin2hex($chr)); + } + + return \sprintf('\\%X ', $ord); + }, $text); + + return static::convertStringFromUTF8($text, $charset); + } } diff --git a/tests/SecurityTest.php b/tests/SecurityTest.php index d738cde..6ba1e55 100644 --- a/tests/SecurityTest.php +++ b/tests/SecurityTest.php @@ -143,6 +143,65 @@ public function dataJS(): array ]; } + public function dataURL(): array + { + return [ + '<' => ['<', '%3C'], + '>' => ['>', '%3E'], + '\'' => ['\'', '%27'], + '"' => ['"', '%22'], + '&' => ['&', '%26'], + 'Ā' => ['Ā', '%C4%80'], + ',' => [',', '%2C'], + '.' => ['.', '.'], + '_' => ['_', '_'], + '-' => ['-', '-'], + ':' => [':', '%3A'], + ';' => [';', '%3B'], + '!' => ['!', '%21'], + 'a' => ['a', 'a'], + 'A' => ['A', 'A'], + 'z' => ['z', 'z'], + 'Z' => ['Z', 'Z'], + '0' => ['0', '0'], + '9' => ['9', '9'], + "\r" => ["\r", '%0D'], + "\n" => ["\n", '%0A'], + "\t" => ["\t", '%09'], + "\0" => ["\0", '%00'], + ' ' => [' ', '%20'], + '~' => ['~', '~'], + '+' => ['+', '%2B'] + ]; + } + + public function dataCSS(): array + { + return [ + '<' => ['<', '\\3C '], + '>' => ['>', '\\3E '], + '\'' => ['\'', '\\27 '], + '"' => ['"', '\\22 '], + '&' => ['&', '\\26 '], + 'Ā' => ['Ā', '\\100 '], + "\xF0\x90\x80\x80" => ["\xF0\x90\x80\x80", '\\10000 '], + ',' => [',', '\\2C '], + '.' => ['.', '\\2E '], + '_' => ['_', '\\5F '], + 'a' => ['a', 'a'], + 'A' => ['A', 'A'], + 'z' => ['z', 'z'], + 'Z' => ['Z', 'Z'], + '0' => ['0', '0'], + '9' => ['9', '9'], + "\r" => ["\r", '\\D '], + "\n" => ["\n", '\\A '], + "\t" => ["\t", '\\9 '], + "\0" => ["\0", '\\0 '], + ' ' => [' ', '\\20 '], + ]; + } + /** * @dataProvider dataHTML * @@ -182,6 +241,32 @@ public function testEscJS(string $input, string $expected): void self::assertSame($expected, Security::escJS($input)); } + /** + * @dataProvider dataURL + * + * @param string $input + * @param string $expected + * + * @throws SecurityException + */ + public function testEscURL(string $input, string $expected): void + { + self::assertSame($expected, Security::escURL($input)); + } + + /** + * @dataProvider dataCSS + * + * @param string $input + * @param string $expected + * + * @throws SecurityException + */ + public function testEscCSS(string $input, string $expected): void + { + self::assertSame($expected, Security::escCSS($input)); + } + public function testCharsetNotSupportedException(): void { $countThrownExceptions = 0; @@ -207,7 +292,21 @@ public function testCharsetNotSupportedException(): void ++$countThrownExceptions; } - self::assertSame(3, $countThrownExceptions); + try { + Security::escURL('a', 'nope'); + } catch (SecurityException $e) { + self::assertSame("Charset 'nope' is not supported", $e->getMessage()); + ++$countThrownExceptions; + } + + try { + Security::escCSS('a', 'nope'); + } catch (SecurityException $e) { + self::assertSame("Charset 'nope' is not supported", $e->getMessage()); + ++$countThrownExceptions; + } + + self::assertSame(5, $countThrownExceptions); } public function testInvalidCharacter(): void @@ -236,7 +335,21 @@ public function testInvalidCharacter(): void ++$countThrownExceptions; } - self::assertSame(3, $countThrownExceptions); + try { + Security::escURL($invalidChar); + } catch (SecurityException $e) { + self::assertSame('String to convert is not valid for the specified charset', $e->getMessage()); + ++$countThrownExceptions; + } + + try { + Security::escCSS($invalidChar); + } catch (SecurityException $e) { + self::assertSame('String to convert is not valid for the specified charset', $e->getMessage()); + ++$countThrownExceptions; + } + + self::assertSame(5, $countThrownExceptions); } /**