Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
53 / 53 |
Url | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
26 | |
100.00% |
53 / 53 |
__construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
validate | |
100.00% |
1 / 1 |
15 | |
100.00% |
42 / 42 |
|||
testSocket | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
testDns | |
100.00% |
1 / 1 |
4 | |
100.00% |
2 / 2 |
|||
setSocketTester | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
setDnsTester | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
<?php | |
namespace Verja\Validator; | |
use Verja\Error; | |
use Verja\Validator; | |
class Url extends Validator | |
{ | |
const COVERAGE_VALID = 'valid'; // check only that the url is valid (parse_url !== false) | |
const COVERAGE_COMPLETE = 'complete'; // check that host and scheme is given | |
const COVERAGE_ACTIVE = 'active'; // check that the host exists (active dns record) | |
const COVERAGE_LISTENING = 'listening'; // check for an open http(s) or ftp port | |
/** @var int[] */ | |
protected static $ports = [ | |
'https' => 443, | |
'http' => 80, | |
'ftp' => 21, | |
'ssh' => 22, | |
]; | |
/** @var callable */ | |
protected static $socketTester; | |
/** @var callable */ | |
protected static $dnsTester; | |
/** @var string */ | |
protected $mode; | |
/** @var array */ | |
protected $schemes = ['https', 'http', 'ftp']; | |
/** | |
* Url constructor. | |
* | |
* @param string $coverage | |
* @param array|string $schemes | |
*/ | |
public function __construct(string $coverage = self::COVERAGE_COMPLETE, $schemes = ['https', 'http', 'ftp']) | |
{ | |
$this->mode = $coverage; | |
$this->schemes = is_string($schemes) ? array_slice(func_get_args(), 1) : $schemes; | |
} | |
/** | |
* Validate $value | |
* | |
* @param mixed $value | |
* @param array $context | |
* @return bool | |
*/ | |
public function validate($value, array $context = []): bool | |
{ | |
if (!is_string($value) || !$url = parse_url($value)) { | |
// not a string or not a url | |
$this->error = new Error( | |
'NO_URL', | |
$value, | |
'value should be a valid url' | |
); | |
return false; | |
} | |
if ($this->mode !== self::COVERAGE_VALID) { | |
// for scheme file we don't need a host | |
if (!isset($url['scheme']) || !isset($url['host']) && $url['scheme'] !== 'file') { | |
// no full url | |
$this->error = new Error( | |
'NOT_FULL_URL', | |
$value, | |
'value should be a full url including host and scheme', | |
['schemes' => $this->schemes] | |
); | |
return false; | |
} | |
if (!in_array($url['scheme'], $this->schemes)) { | |
// scheme not allowed | |
$this->error = new Error( | |
'SCHEME_NOT_ALLOWED', | |
$value, | |
'value should contain an allowed scheme', | |
['schemes' => $this->schemes] | |
); | |
return false; | |
} | |
if (($this->mode === self::COVERAGE_ACTIVE || $this->mode === self::COVERAGE_LISTENING) && | |
!static::testDns($url['host']) | |
) { | |
// no dns entry | |
$this->error = new Error( | |
'NO_DNS_RECORD', | |
$value, | |
'value should contain a hostname with an active dns record' | |
); | |
return false; | |
} | |
if ($this->mode === self::COVERAGE_LISTENING) { | |
if (!isset($url['port']) && !isset(self::$ports[$url['scheme']])) { | |
// no port defined | |
$this->error = new Error( | |
'NO_PORT', | |
$value, | |
'value should contain a port' | |
); | |
return false; | |
} | |
if (!static::testSocket($url['host'], $url['port'] ?? self::$ports[$url['scheme']])) { | |
// not listening to connections | |
$this->error = new Error( | |
'NOT_LISTENING', | |
$value, | |
'value should point to a server that is currently listening' | |
); | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
protected static function testSocket(string $host, int $port) | |
{ | |
if (static::$socketTester !== null) { | |
return call_user_func(static::$socketTester, $host, $port); | |
} | |
// @codeCoverageIgnoreStart | |
// we can not test this during unit tests - so we mock this through $socketTester | |
try { | |
$socket = fsockopen($host, $port, $errno, $errstr, 0.5); | |
fclose($socket); | |
return true; | |
} catch (\Throwable $e) { | |
// ignore any errors (socket timeout for example) | |
} | |
return false; | |
// @codeCoverageIgnoreEnd | |
} | |
protected static function testDns(string $host) | |
{ | |
if (static::$dnsTester !== null) { | |
return call_user_func(static::$dnsTester, $host); | |
} | |
// @codeCoverageIgnoreStart | |
// we can not test this during unit tests - so we mock this through $dnsTester | |
return checkdnsrr($host, 'A') || | |
checkdnsrr($host, 'AAAA') || | |
checkdnsrr($host, 'CNAME'); | |
// @codeCoverageIgnoreEnd | |
} | |
public static function setSocketTester(callable $socketTester) | |
{ | |
static::$socketTester = $socketTester; | |
} | |
public static function setDnsTester(callable $dnsTester) | |
{ | |
static::$dnsTester = $dnsTester; | |
} | |
} |