Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
53 / 53
Url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
6 / 6
26
100.00% covered (success)
100.00%
53 / 53
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 validate
100.00% covered (success)
100.00%
1 / 1
15
100.00% covered (success)
100.00%
42 / 42
 testSocket
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
 testDns
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
2 / 2
 setSocketTester
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setDnsTester
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
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;
    }
}