From ec35036aef6cc6c676917d5d8adee57b1cc2e05d Mon Sep 17 00:00:00 2001 From: Tomasz Hanc Date: Sat, 6 Jun 2020 11:31:40 +0200 Subject: [PATCH] Adds assertion for URLs --- README.md | 1 + src/Assert.php | 40 +++++++++++++ tests/AssertTest.php | 85 ++++++++++++++++++++++++++++ tests/static-analysis/assert-url.php | 18 ++++++ 4 files changed, 144 insertions(+) create mode 100644 tests/static-analysis/assert-url.php diff --git a/README.md b/README.md index 1407a9a1..341dd8f8 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ Method | Description `ip($value, $message = '')` | Check that a string is a valid IP (either IPv4 or IPv6) `ipv4($value, $message = '')` | Check that a string is a valid IPv4 `ipv6($value, $message = '')` | Check that a string is a valid IPv6 +`url($value, $message = '')` | Check that a string is a valid URL `email($value, $message = '')` | Check that a string is a valid e-mail address `notWhitespaceOnly($value, $message = '')` | Check that a string contains at least one non-whitespace character diff --git a/src/Assert.php b/src/Assert.php index 175d1f3b..ed15ae4f 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -58,6 +58,7 @@ * @method static void nullOrIp($value, $message = '') * @method static void nullOrIpv4($value, $message = '') * @method static void nullOrIpv6($value, $message = '') + * @method static void nullOrUrl($value, $message = '') * @method static void nullOrEmail($value, $message = '') * @method static void nullOrUniqueValues($values, $message = '') * @method static void nullOrEq($value, $expect, $message = '') @@ -150,6 +151,7 @@ * @method static void allIp($values, $message = '') * @method static void allIpv4($values, $message = '') * @method static void allIpv6($values, $message = '') + * @method static void allUrl($values, $message = '') * @method static void allEmail($values, $message = '') * @method static void allUniqueValues($values, $message = '') * @method static void allEq($values, $expect, $message = '') @@ -887,6 +889,44 @@ public static function ipv6($value, $message = '') } } + /** + * @psalm-pure + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + * + * The URL pattern is taken from Symfony: @see https://github.com/symfony/Validator/blob/master/Constraints/UrlValidator.php + */ + public static function url($value, $message = '') + { + $pattern = '~^ + (http|https):// # protocol + (((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth + ( + ([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name + | # or + \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address + | # or + \[ + (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::)))) + \] # an IPv6 address + ) + (:[0-9]+)? # a port (optional) + (?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path + (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional) + (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional) + $~ixu'; + + if (!\preg_match($pattern, $value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be a valid URL. Got %s', + static::valueToString($value) + )); + } + } + /** * @param mixed $value * @param string $message diff --git a/tests/AssertTest.php b/tests/AssertTest.php index c1f708b9..c1694577 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -558,6 +558,91 @@ public function getTests() array('ipv6', array(array()), false), array('ipv6', array(null), false), array('ipv6', array(false), false), + array('url', array('example.com'), false), + array('url', array('://example.com'), false), + array('url', array('http ://example.com'), false), + array('url', array('http:/example.com'), false), + array('url', array('http://example.com::aa'), false), + array('url', array('http://example.com:aa'), false), + array('url', array('ftp://example.fr'), false), + array('url', array('faked://example.fr'), false), + array('url', array('http://127.0.0.1:aa/'), false), + array('url', array('ftp://[::1]/'), false), + array('url', array('http://[::1'), false), + array('url', array('http://hello.☎/'), false), + array('url', array('http://:password@symfony.com'), false), + array('url', array('http://:password@@symfony.com'), false), + array('url', array('http://username:passwordsymfony.com'), false), + array('url', array('http://usern@me:password@symfony.com'), false), + array('url', array('http://nota%hex:password@symfony.com'), false), + array('url', array('http://username:nota%hex@symfony.com'), false), + array('url', array('http://example.com/exploit.html?'), false), + array('url', array('http://example.com/exploit.html?hel lo'), false), + array('url', array('http://example.com/exploit.html?not_a%hex'), false), + array('url', array('http://'), false), + array('url', array('http://a.pl'), true), + array('url', array('http://www.example.com'), true), + array('url', array('http://www.example.com.'), true), + array('url', array('http://www.example.museum'), true), + array('url', array('https://example.com/'), true), + array('url', array('https://example.com:80/'), true), + array('url', array('http://examp_le.com'), true), + array('url', array('http://www.sub_domain.examp_le.com'), true), + array('url', array('http://www.example.coop/'), true), + array('url', array('http://www.test-example.com/'), true), + array('url', array('http://www.symfony.com/'), true), + array('url', array('http://symfony.fake/blog/'), true), + array('url', array('http://symfony.com/?'), true), + array('url', array('http://symfony.com/search?type=&q=url+validator'), true), + array('url', array('http://symfony.com/#'), true), + array('url', array('http://symfony.com/#?'), true), + array('url', array('http://www.symfony.com/doc/current/book/validation.html#supported-constraints'), true), + array('url', array('http://very.long.domain.name.com/'), true), + array('url', array('http://localhost/'), true), + array('url', array('http://myhost123/'), true), + array('url', array('http://127.0.0.1/'), true), + array('url', array('http://127.0.0.1:80/'), true), + array('url', array('http://[::1]/'), true), + array('url', array('http://[::1]:80/'), true), + array('url', array('http://[1:2:3::4:5:6:7]/'), true), + array('url', array('http://sãopaulo.com/'), true), + array('url', array('http://xn--sopaulo-xwa.com/'), true), + array('url', array('http://sãopaulo.com.br/'), true), + array('url', array('http://xn--sopaulo-xwa.com.br/'), true), + array('url', array('http://пример.испытание/'), true), + array('url', array('http://xn--e1afmkfd.xn--80akhbyknj4f/'), true), + array('url', array('http://مثال.إختبار/'), true), + array('url', array('http://xn--mgbh0fb.xn--kgbechtv/'), true), + array('url', array('http://例子.测试/'), true), + array('url', array('http://xn--fsqu00a.xn--0zwm56d/'), true), + array('url', array('http://例子.測試/'), true), + array('url', array('http://xn--fsqu00a.xn--g6w251d/'), true), + array('url', array('http://例え.テスト/'), true), + array('url', array('http://xn--r8jz45g.xn--zckzah/'), true), + array('url', array('http://مثال.آزمایشی/'), true), + array('url', array('http://xn--mgbh0fb.xn--hgbk6aj7f53bba/'), true), + array('url', array('http://실례.테스트/'), true), + array('url', array('http://xn--9n2bp8q.xn--9t4b11yi5a/'), true), + array('url', array('http://العربية.idn.icann.org/'), true), + array('url', array('http://xn--ogb.idn.icann.org/'), true), + array('url', array('http://xn--e1afmkfd.xn--80akhbyknj4f.xn--e1afmkfd/'), true), + array('url', array('http://xn--espaa-rta.xn--ca-ol-fsay5a/'), true), + array('url', array('http://xn--d1abbgf6aiiy.xn--p1ai/'), true), + array('url', array('http://☎.com/'), true), + array('url', array('http://username:password@symfony.com'), true), + array('url', array('http://user.name:password@symfony.com'), true), + array('url', array('http://user_name:pass_word@symfony.com'), true), + array('url', array('http://username:pass.word@symfony.com'), true), + array('url', array('http://user.name:pass.word@symfony.com'), true), + array('url', array('http://user-name@symfony.com'), true), + array('url', array('http://user_name@symfony.com'), true), + array('url', array('http://symfony.com?'), true), + array('url', array('http://symfony.com?query=1'), true), + array('url', array('http://symfony.com/?query=1'), true), + array('url', array('http://symfony.com#'), true), + array('url', array('http://symfony.com#fragment'), true), + array('url', array('http://symfony.com/#fragment'), true), + array('url', array('http://example.com/exploit.html?hello[0]=test'), true), array('email', array('foo'), false), array('email', array(123), false), array('email', array('foo.com'), false), diff --git a/tests/static-analysis/assert-url.php b/tests/static-analysis/assert-url.php new file mode 100644 index 00000000..331aa302 --- /dev/null +++ b/tests/static-analysis/assert-url.php @@ -0,0 +1,18 @@ +