There’s a lot of code out there that deals with URL validation. Basically all of it is concerned with “does this URL meet the RFC spec?” That’s actually not that interesting a question if you are validating user input. You don’t want ‘gopher://whatever/’ or ‘http://10.1.2.3’ as valid URLs in your system if you have asked the user for the address of a web page.
What’s more, because of punycode-powered internationalized URLs most URL validating code will tell you that real URLs that people can use today are invalid (PHP’s parse_url is not even utf8 safe).
Here’s some PHP code that validates URLs in a more practical way. It uses the list of TLDs in static::$validTlds from the IANA list of valid TLDs and assumes the presence of a utf8-safe $this->parseUrl such as Joomla’s version.
(c) 2013 Thomas David Baker, MIT License
/**
* Return true if url is valid, false otherwise.
*
* Note that this is not the RFC definiton of a valid URL. For example we
* differ from the RFC in only accepting http and https URLs, not accepting
* single word hosts, and accepting any characters in hostnames (as modern
* browsers will punycode translate them to ASCII automatically).
*
* @param string $url Url to validate. Must include 'scheme://' to have any
* chance of validating.
*
* @return boolean
*/
public function validUrl($url) {
$parts = $this->parseUrl($url);
// We must be able to recognize this as some form of URL.
if (!$parts) {
return false;
}
// SCHEME.
// Must be qualified with a scheme.
if (!isset($parts['scheme']) || !$parts['scheme']) {
return false;
}
// Only http and https are acceptable. No ftp or similar.
if (!in_array($parts['scheme'], ['http', 'https'])) {
return false;
}
// CHECK FOR 'EXTRA PARTS'.
// If a URL has unrecognized bits then it is not valid - for example the
// 'z' in 'www.google.com:80z'.
// This check invalidates URLs that use a user - we don't allow those.
$partsCheck = $parts;
$partsCheck['scheme'] .= '://';
if (isset($partsCheck['port'])) {
$partsCheck['port'] = ':' . $partsCheck['port'];
}
if (isset($partsCheck['query'])) {
$partsCheck['query'] = '?' . $partsCheck['query'];
}
if (isset($partsCheck['fragment'])) {
$partsCheck['fragment'] = '#' . $partsCheck['fragment'];
}
if (implode('', $partsCheck) !== $url) {
return false;
}
// HOST.
if (!isset($parts['host']) || !$parts['host']) {
return false;
}
// Single word hosts are not acceptable.
if (strpos($parts['host'], '.') === false) {
return false;
}
if (strpos($parts['host'], ' ') !== false) {
return false;
}
if (strpos($parts['host'], '--') !== false) {
return false;
}
if (strpos($parts['host'], '-') === 0) {
return false;
}
// Cope with internationalized domain names.
$host = idn_to_ascii($parts['host']);
$hostSegments = explode('.', $host);
// The IANA lists TLDs in uppercase, so we do too.
$tld = mb_strtoupper(array_pop($hostSegments));
if (!$tld) {
return false;
}
if (!in_array(mb_strtoupper($tld), static::$validTlds)) {
return false;
}
$domain = array_pop($hostSegments);
if (!$domain) {
return false;
}
// PATH.
if (isset($parts['path']) && substr($parts['path'], 0, 1) !== '/') {
return false;
}
// If you made it this far you're golden.
return true;
}
Looking at the list of interesting URLs from http://mathiasbynens.be/demo/url-regex and elsewhere it allows all of the following:
http://foo.com/blah_blah
http://foo.com/blah_blah/
http://foo.com/blah_blah_(wikipedia)
http://foo.com/blah_blah_(wikipedia)_(again)
http://www.example.com/wpstyle/?p=364
https://www.example.com/foo/?bar=baz&inga=42&quux
http://✪df.ws/123
http://➡.ws/䨹
http://⌘.ws
http://⌘.ws/
http://foo.com/blah_(wikipedia)#cite-1
http://foo.com/blah_(wikipedia)_blah#cite-1
http://foo.com/unicode_(✪)_in_parens
http://foo.com/(something)?after=parens
http://☺.damowmow.com/
http://code.google.com/events/#&product=browser
http://j.mp
http://foo.com/?q=Test%20URL-encoded%20stuff
http://مثال.إختبار
http://例子.测试
http://उदाहरण.परीक्षा
http://1337.net
http://a.b-c.de
And disallows all of these:
# Invalid URLs
http://
http://.
http://..
http://../
http://?
http://??
http://??/
http://#
http://##
http://##/
http://foo.bar?q=Spaces should be encoded
//
//a
///a
///
http:///a
foo.com
rdar://1234
h://test
http:// shouldfail.com
:// should fail
http://foo.bar/foo(bar)baz quux
ftps://foo.bar/
http://-error-.invalid/
http://a.b--c.de/
http://-a.b.co
http://0.0.0.0
http://10.1.1.0
http://10.1.1.255
http://224.1.1.1
http://1.1.1.1.1
http://123.123.123
http://3628126748
http://.www.foo.bar/
http://www.foo.bar./
http://.www.foo.bar./
http://10.1.1.1
http://10.1.1.254
# The following URLs are valid by the letter of the law but we don't want to allow them.
http://userid:password@example.com:8080
http://userid:password@example.com:8080/
http://userid@example.com
http://userid@example.com/
http://userid@example.com:8080
http://userid@example.com:8080/
http://userid:password@example.com
http://userid:password@example.com/
http://-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com
http://142.42.1.1/
http://142.42.1.1:8080/
http://223.255.255.254
ftp://foo.bar/baz