diff --git a/src/MD_STD_IN.php b/src/MD_STD_IN.php index 0c004e1..829ec56 100644 --- a/src/MD_STD_IN.php +++ b/src/MD_STD_IN.php @@ -43,7 +43,7 @@ final class MD_STD_IN { */ public static function sanitize_id_or_zero(mixed $input):int { - if ($input === "") { + if ($input === "" || $input === 0) { return 0; } @@ -96,11 +96,14 @@ final class MD_STD_IN { */ public static function sanitize_rgb_color(mixed $input):string { - $output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH); + if (($output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)) === false) { + throw new MDInvalidColorCode("Invalid color code provided: " . $output); + } - if ($output === false - || !in_array(strlen($output), [3, 6], true) - || (preg_match('/^[a-fA-F0-9]{3}$/', $output) === false && preg_match('/^[a-fA-F0-9]{6}$/', $output) === false) + $output = \strtoupper($output); + + if (!in_array(strlen($output), [3, 6], true) + || (MD_STD::preg_replace_str('/[A-F0-9]/', '', $output) !== '') ) { throw new MDInvalidColorCode("Invalid color code provided: " . $output); } @@ -214,19 +217,30 @@ final class MD_STD_IN { $rewritten .= $parsed['host']; if (!empty($parsed['port'])) $rewritten .= ':' . $parsed['port']; $rewritten .= str_replace('%2F' , '/', urlencode($parsed['path'])); - if (!empty($parsed['query'])) $rewritten .= '?' . urlencode($parsed['query']); + if (!empty($parsed['query'])) { + $rewritten .= '?' . str_replace('%3D', '=', urlencode($parsed['query'])); + } if (($output = \filter_var($rewritten, FILTER_VALIDATE_URL)) === false) { throw new MDInvalidUrl("Invalid input URL" . \urlencode($input)); } } if (empty($output)) return ''; + // As per the RFC, URLs should not exceed 2048. Enough real-world ones + // do. But they certainly should not exceed 10000 characters. + if (\strlen($output) > 10000) { + throw new MDInvalidUrl("The entered URL seems to be valid otherwise, but is overly long."); + } // Check for valid schemes if (MD_STD::startsWithAny($output, ['https://', 'http://', 'ftp://']) === false) { throw new MDInvalidUrl("Invalid input URL"); } + if (\str_contains($output, '.') === false) { + throw new MDInvalidUrl("Invalid input URL"); + } + return $output; } @@ -245,7 +259,7 @@ final class MD_STD_IN { } if (($output = \filter_var($input, FILTER_VALIDATE_EMAIL)) === false) { - throw new MDInvalidEmail("Invalid input email address"); + throw new MDInvalidEmail("Invalid input email address" . ' '. $input); } return $output; diff --git a/src/testing/MD_STD_TEST_PROVIDERS.php b/src/testing/MD_STD_TEST_PROVIDERS.php new file mode 100644 index 0000000..4d3b2b2 --- /dev/null +++ b/src/testing/MD_STD_TEST_PROVIDERS.php @@ -0,0 +1,146 @@ + + */ +declare(strict_types = 1); + +use PHPUnit\Framework\TestCase; + + +/** + * Tests for the manifest. + */ +final class MD_STD_TEST_PROVIDERS { + + /** + * Data provider for returning invalid URLs. + * + * @return array + */ + public static function invalid_url_provider():array { + + $output = [ + 'Space in protocol name' => ["h ttps://www.museum-digital.org"], + 'Unwanted protocol' => ["telegram://www.museum-digital.org"], + 'String without protocol' => ["www.museum-digital.org"], + 'Localhost' => ["http://localhost"], + + // As per the RFC, URLs should not exceed 2048. Enough real-world ones + // do. But they certainly should not exceed 10000 characters. + 'Overly long URL (> 10000 chars)' => ["https://www.museum-digital.org/" . str_repeat('a', 10000)], + ]; + + return $output; + + } + + /** + * Data provider for working URLs. + * + * @return array + */ + public static function valid_url_provider():array { + + return [ + 'Regular URL without path or query' => ['https://www.museum-digital.org', 'https://www.museum-digital.org'], + 'URL with cyrillic characters, HTML-encoded ' => [ + 'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4', + 'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4', + ], + 'URL with cyrillic characters, not HTML-encoded ' => [ + 'https://sr.wikipedia.org/wiki/Београд', + 'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4', + ], + 'URL with: scheme, user, pass, host, path, query' => [ + 'https://username:password@sr.wikipedia.org:9000/wiki/Београд?test=hi', + 'https://username:password@sr.wikipedia.org:9000/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4?test=hi', + ], + ]; + + } + + /** + * Data provider for working mail addresses. + * + * @return array + */ + public static function invalid_email_provider():array { + + // Invalid addresses as per https://codefool.tumblr.com/post/15288874550/list-of-valid-and-invalid-email-addresses + $invalid = [ + 'plainaddress', + '#@%^%#$@#$@#.com', + '@example.com', + 'Joe Smith ', + 'email.example.com', + 'email@example@example.com', + '.email@example.com', + 'email.@example.com', + 'email..email@example.com', + 'あいうえお@example.com', + 'email@example.com (Joe Smith)', + 'email@example', + 'email@-example.com', + 'email@111.222.333.44444', + 'email@example..com', + 'Abc..123@example.com', + '“(),:;<>[\]@example.com', + 'just"not"right@example.com', + 'this\ is"really"not\allowed@example.com', + ]; + + $output = []; + foreach ($invalid as $addr) { + $output[$addr] = [ + $addr, + ]; + } + + return $output; + + } + + /** + * Data provider for working mail addresses. + * + * @return array + */ + public static function valid_email_provider():array { + + // Valid addresses as per https://codefool.tumblr.com/post/15288874550/list-of-valid-and-invalid-email-addresses + // Excluding: + // + // 'email@123.123.123.123', + // 'email@[123.123.123.123]', + // '“email”@example.com', + // + // as per PHP's FILTER_VALIDATE_EMAIL + $valid = [ + 'email@example.com', + 'firstname.lastname@example.com', + 'email@subdomain.example.com', + 'firstname+lastname@example.com', + '1234567890@example.com', + 'email@example-one.com', + '_______@example.com', + 'email@example.name', + 'email@example.museum', + 'email@example.co.jp', + 'firstname-lastname@example.com', + ]; + + $output = []; + foreach ($valid as $addr) { + $output[$addr] = [ + $addr, + $addr, + ]; + } + + return $output; + + } + +} diff --git a/tests/MD_STD_IN_Test.php b/tests/MD_STD_IN_Test.php index 5425818..92fec09 100644 --- a/tests/MD_STD_IN_Test.php +++ b/tests/MD_STD_IN_Test.php @@ -12,29 +12,84 @@ use PHPUnit\Framework\TestCase; * Tests for MD_STD_IN. */ final class MD_STD_IN_Test extends TestCase { + /** + * Data provider for valid IDs. + * + * @return array + */ + public static function valid_id_provider():array { + + $values = [ + [1, 1], + ["1", 1], + ["1111111", 1111111], + ]; + + $output = []; + foreach ($values as $value) { + $output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value; + } + return $output; + + } + /** * Function for testing sanitize_id(). * * @small * @covers \MD_STD_IN::sanitize_id + * @dataProvider \MD_STD_IN_Test::valid_id_provider + * + * @param mixed $to_validate Input to validate. + * @param integer $expected Expected output. * * @return void */ - public function test_sanitize_id():void { + public function test_sanitize_id_works(mixed $to_validate, int $expected):void { + self::assertEquals($expected, MD_STD_IN::sanitize_id($to_validate)); + } - self::assertEquals(1, MD_STD_IN::sanitize_id(1)); - self::assertEquals(1, MD_STD_IN::sanitize_id("1")); + /** + * Data provider for valid IDs. + * + * @return array + */ + public static function invalid_id_provider():array { + + $output = self::invalid_id_provider_without_zero(); + $output['Number 0'] = [0]; + return $output; + + } + + /** + * Function for testing sanitize_id(). + * + * @small + * @covers \MD_STD_IN::sanitize_id + * @dataProvider \MD_STD_IN_Test::invalid_id_provider + * + * @param mixed $to_validate Input to validate. + * + * @return void + */ + public function test_sanitize_id_fails(mixed $to_validate):void { self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id(0); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id(100000000000000000000000000000); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id("1a2"); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id("12a"); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id("a"); + MD_STD_IN::sanitize_id($to_validate); + + } + /** + * Data provider for valid IDs. + * + * @return array + */ + public static function valid_id_or_zero_provider():array { + + $output = self::valid_id_provider(); + $output['Integer 0'] = [0, 0]; + $output['String 0'] = [0, 0]; + return $output; } @@ -43,25 +98,71 @@ final class MD_STD_IN_Test extends TestCase { * * @small * @covers \MD_STD_IN::sanitize_id_or_zero + * @dataProvider \MD_STD_IN_Test::valid_id_or_zero_provider + * + * @param mixed $to_validate Input to validate. + * @param integer $expected Expected output. * * @return void */ - public function test_sanitize_id_or_zero():void { + public function test_sanitize_id_or_zero_works(mixed $to_validate, int $expected):void { - self::assertEquals(0, MD_STD_IN::sanitize_id_or_zero("")); - self::assertEquals(0, MD_STD_IN::sanitize_id_or_zero(0)); - self::assertEquals(0, MD_STD_IN::sanitize_id_or_zero("0")); - self::assertEquals(1, MD_STD_IN::sanitize_id_or_zero(1)); - self::assertEquals(1, MD_STD_IN::sanitize_id_or_zero("1")); + self::assertEquals($expected, MD_STD_IN::sanitize_id_or_zero($to_validate)); + + } + + /** + * Data provider for valid IDs. + * + * @return array + */ + public static function invalid_id_provider_without_zero():array { + + return [ + 'Number too high' => [1000000000000000000000000000000000000], + 'Number with character in the middle' => ["1a2"], + 'Number with suffixed string' => ["12a"], + 'String character' => ["a"], + ]; + + } + + /** + * Function for testing sanitize_id_or_zero(). + * + * @small + * @covers \MD_STD_IN::sanitize_id_or_zero + * @dataProvider \MD_STD_IN_Test::invalid_id_provider_without_zero + * + * @param mixed $to_validate Input to validate. + * + * @return void + */ + public function test_sanitize_id_or_zero_fails(mixed $to_validate):void { self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id_or_zero(100000000000000000000000000000); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id_or_zero("1a2"); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id_or_zero("12a"); - self::expectException(MDpageParameterNotNumericException::class); - MD_STD_IN::sanitize_id_or_zero("a"); + MD_STD_IN::sanitize_id_or_zero($to_validate); + + } + + /** + * Data provider for text with its expected cleaned values. + * + * @return array + */ + public static function text_with_expected_return_value_provider():array { + + return [ + 'Empty string' => ['', ''], + 'Integer 0' => [0, '0'], + 'String 0' => ['0', '0'], + 'Regular string' => ['a', 'a'], + 'String with double whitespace in between' => ['a a', 'a a'], + 'String to be trimmed (spaces)' => ['a ', 'a'], + 'String to be trimmed (newline)' => ['a ' . PHP_EOL, 'a'], + 'Empty array' => [[], ''], + 'Array with content' => [['test' => 'test'], ''], + ]; } @@ -70,16 +171,57 @@ final class MD_STD_IN_Test extends TestCase { * * @small * @covers \MD_STD_IN::sanitize_text + * @dataProvider \MD_STD_IN_Test::text_with_expected_return_value_provider + * + * @param mixed $to_validate Input to validate. + * @param string $expected Expected output. * * @return void */ - public function test_sanitize_text():void { + public function test_sanitize_text(mixed $to_validate, string $expected):void { - self::assertEquals("", MD_STD_IN::sanitize_text("")); - self::assertEquals("a", MD_STD_IN::sanitize_text("a")); - self::assertEquals("a a", MD_STD_IN::sanitize_text("a a")); - self::assertEquals("a", MD_STD_IN::sanitize_text("a ")); - self::assertEquals("", MD_STD_IN::sanitize_text([])); + self::assertEquals($expected, MD_STD_IN::sanitize_text($to_validate)); + + } + + /** + * Data provider for working RGB colors. + * + * @return array + */ + public static function valid_rgb_colors_provider():array { + + return [ + 'Three character version' => ['AAA', 'AAA'], + 'Three character version (int)' => ['111', '111'], + 'Three character version (mixed)' => ['1a1', '1A1'], + 'Six character version' => ['AAAAAA', 'AAAAAA'], + 'Six character version (int)' => ['111111', '111111'], + 'Six character version (mixed)' => ['1a1AAA', '1A1AAA'], + ]; + + } + + /** + * Data provider for strings that are not rgb colors. + * + * @return array + */ + public static function invalid_rgb_colors_provider():array { + + $output = [ + 'Array' => [[]], + 'Three characters, but invalid ones' => ['ZZZ'], + 'Six characters, but invalid ones' => ['111ZZZ'], + 'Three characters, but with spaces' => ['ZZZ '], + ]; + + for ($i = 0; $i++; $i < 10) { + if ($i === 3 || $i === 6) continue; + $output['Valid characters repeated ' . $i . ' times'] = [$i]; + } + + return $output; } @@ -88,22 +230,34 @@ final class MD_STD_IN_Test extends TestCase { * * @small * @covers \MD_STD_IN::sanitize_rgb_color + * @dataProvider \MD_STD_IN_Test::valid_rgb_colors_provider + * + * @param mixed $to_validate Input to validate. + * @param string $expected Expected output. * * @return void */ - public function test_sanitize_rgb_color():void { + public function test_sanitize_rgb_color_works(mixed $to_validate, string $expected):void { - self::assertEquals("AAA", MD_STD_IN::sanitize_rgb_color("AAA")); - self::assertEquals("000", MD_STD_IN::sanitize_rgb_color("000")); + self::assertEquals($expected, MD_STD_IN::sanitize_rgb_color($to_validate)); + + } + + /** + * Function for testing sanitize_rgb_color()'s failure modes. + * + * @small + * @covers \MD_STD_IN::sanitize_rgb_color + * @dataProvider \MD_STD_IN_Test::invalid_rgb_colors_provider + * + * @param mixed $to_validate Input to validate. + * + * @return void + */ + public function test_sanitize_rgb_color_fails(mixed $to_validate):void { self::expectException(MDInvalidColorCode::class); - MD_STD_IN::sanitize_rgb_color("ZZZaa2343422342342323"); - - self::expectException(MDInvalidColorCode::class); - MD_STD_IN::sanitize_rgb_color("ZZZaa"); - - self::expectException(MDInvalidColorCode::class); - MD_STD_IN::sanitize_rgb_color("ZZZ"); + MD_STD_IN::sanitize_rgb_color($to_validate); } @@ -199,21 +353,47 @@ final class MD_STD_IN_Test extends TestCase { * * @return void */ - public function test_sanitize_url():void { + public function test_sanitize_url_with_empty_string():void { // Ensure empty inputs return empty output self::assertEquals("", MD_STD_IN::sanitize_url("")); - self::assertEquals("https://www.museum-digital.org", MD_STD_IN::sanitize_url("https://www.museum-digital.org")); - // Ensure that cyrillic characters are accepted - self::assertEquals("https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4", MD_STD_IN::sanitize_url("https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4")); - self::assertEquals("https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4", MD_STD_IN::sanitize_url("https://sr.wikipedia.org/wiki/Београд")); + } - self::assertEquals("https://username:password@sr.wikipedia.org:9000/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4", MD_STD_IN::sanitize_url("https://username:password@sr.wikipedia.org:9000/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4")); - self::assertEquals("https://username:password@sr.wikipedia.org:9000/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4", MD_STD_IN::sanitize_url("https://username:password@sr.wikipedia.org:9000/wiki/Београд")); + /** + * Function for testing sanitize_url(). + * + * @small + * @covers \MD_STD_IN::sanitize_url + * @dataProvider \MD_STD_TEST_PROVIDERS::valid_url_provider + * + * @param string $to_validate Input to validate. + * @param string $expected Expected output. + * + * @return void + */ + public function test_sanitize_url_works(string $to_validate, string $expected):void { + + self::assertEquals($expected, MD_STD_IN::sanitize_url($to_validate)); + + } + + /** + * Function for testing sanitize_url(). + * + * @small + * @covers \MD_STD_IN::sanitize_url + * @dataProvider \MD_STD_TEST_PROVIDERS::invalid_url_provider + * + * @param string $to_validate Input to validate. + * + * + * @return void + */ + public function test_sanitize_url_fails(string $to_validate):void { self::expectException(MDInvalidUrl::class); - MD_STD_IN::sanitize_url("h ttps://www.museum-digital.org"); + MD_STD_IN::sanitize_url($to_validate); } @@ -225,13 +405,43 @@ final class MD_STD_IN_Test extends TestCase { * * @return void */ - public function test_sanitize_email():void { + public function test_sanitize_email_empty():void { self::assertEquals("", MD_STD_IN::sanitize_email("")); - self::assertEquals("test@example.org", MD_STD_IN::sanitize_email("test@example.org")); + + } + + /** + * Function for testing sanitize_email(). + * + * @small + * @covers \MD_STD_IN::sanitize_email + * @dataProvider \MD_STD_TEST_PROVIDERS::valid_email_provider + * + * @param string $to_validate Input to validate. + * @param string $expected Expected output. + * + * @return void + */ + public function test_sanitize_email_works(string $to_validate, string $expected):void { + self::assertEquals($expected, MD_STD_IN::sanitize_email($to_validate)); + } + + /** + * Function for testing sanitize_email() fails when it should. + * + * @small + * @covers \MD_STD_IN::sanitize_email + * @dataProvider \MD_STD_TEST_PROVIDERS::invalid_email_provider + * + * @param string $to_validate Input to validate. + * + * @return void + */ + public function test_sanitize_email_fails(string $to_validate):void { self::expectException(MDInvalidEmail::class); - MD_STD_IN::sanitize_email("test"); + MD_STD_IN::sanitize_email($to_validate); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b38df7d..b938e2e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,7 +12,7 @@ declare(strict_types = 1); // Try using class map as defined through /scripts/buildClassMap.php - foreach (array_merge([__DIR__ . '/../tests', __DIR__ . '/../src', __DIR__ . '/../../MDErrorReporter', __DIR__ . '/../../MDErrorReporter/exceptions', __DIR__ . '/../../MDErrorReporter/exceptions/generic', __DIR__ . '/../../MDErrorReporter/exceptions/updates', __DIR__ . '/../../MDErrorReporter/exceptions/page']) as $classDir) { + foreach (array_merge([__DIR__ . '/../tests', __DIR__ . '/../src', __DIR__ . '/../src/testing', __DIR__ . '/../../MDErrorReporter', __DIR__ . '/../../MDErrorReporter/exceptions', __DIR__ . '/../../MDErrorReporter/exceptions/generic', __DIR__ . '/../../MDErrorReporter/exceptions/updates', __DIR__ . '/../../MDErrorReporter/exceptions/page']) as $classDir) { if (\file_exists("$classDir/$className.php")) { include "$classDir/$className.php";