Skip to content

Commit

Permalink
Adds assertion for URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaszhanc committed Jun 6, 2020
1 parent e692710 commit ec35036
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions src/Assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '')
Expand Down Expand Up @@ -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 = '')
Expand Down Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions tests/AssertTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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?<script>alert(1);</script>'), 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),
Expand Down
18 changes: 18 additions & 0 deletions tests/static-analysis/assert-url.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Webmozart\Assert\Tests\StaticAnalysis;

use Webmozart\Assert\Assert;

/**
* @psalm-pure
* @psalm-param non-empty-string $value
*
* @psalm-return non-empty-string
*/
function consume(string $value): string
{
Assert::url($value);

return $value;
}

0 comments on commit ec35036

Please sign in to comment.