Passed
Push — master ( 99ab46...a20a6f )
by Alexander
02:59
created

Url::rule()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Yiisoft\Validator\HasValidationErrorMessage;
8
use Yiisoft\Validator\Result;
9
use Yiisoft\Validator\Rule;
10
use Yiisoft\Validator\ValidationContext;
11
12
use function is_string;
13
use function strlen;
14
15
/**
16
 * UrlValidator validates that the attribute value is a valid HTTP or HTTPS URL.
17
 *
18
 * Note that this validator only checks if the URL scheme and host part are correct.
19
 * It does not check the remaining parts of a URL.
20
 */
21
class Url extends Rule
22
{
23
    use HasValidationErrorMessage;
24
25
    /**
26
     * @var string the regular expression used to validateValue the attribute value.
27
     * The pattern may contain a `{schemes}` token that will be replaced
28
     * by a regular expression which represents the {@see schemes()}.
29
     */
30
    private string $pattern = '/^{schemes}:\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(?::\d{1,5})?(?:$|[?\/#])/i';
31
    /**
32
     * @var array list of URI schemes which should be considered valid. By default, http and https
33
     * are considered to be valid schemes.
34
     */
35
    private array $validSchemes = ['http', 'https'];
36
    /**
37
     * @var bool whether validation process should take into account IDN (internationalized
38
     * domain names). Defaults to false meaning that validation of URLs containing IDN will always
39
     * fail. Note that in order to use IDN validation you have to install and enable `intl` PHP
40
     * extension, otherwise an exception would be thrown.
41
     */
42
    private bool $enableIDN = false;
43
44
    private string $message = 'This value is not a valid URL.';
45
46 14
    public static function rule(): self
47
    {
48 14
        return new self();
49
    }
50
51 10
    protected function validateValue($value, ValidationContext $context = null): Result
52
    {
53 10
        $result = new Result();
54
55
        // make sure the length is limited to avoid DOS attacks
56 10
        if (is_string($value) && strlen($value) < 2000) {
57 9
            if ($this->enableIDN) {
58 6
                $value = $this->convertIdn($value);
59
            }
60
61 9
            if (preg_match($this->getPattern(), $value)) {
62 6
                return $result;
63
            }
64
        }
65
66 6
        $result->addError($this->formatMessage($this->message));
67
68 6
        return $result;
69
    }
70
71 6
    private function idnToAscii(string $idn): string
72
    {
73 6
        $result = idn_to_ascii($idn, 0, INTL_IDNA_VARIANT_UTS46);
74
75 6
        return $result === false ? '' : $result;
76
    }
77
78 6
    private function convertIdn(string $value): string
79
    {
80 6
        if (strpos($value, '://') === false) {
81 4
            return $this->idnToAscii($value);
82
        }
83
84 2
        return preg_replace_callback(
85 2
            '/:\/\/([^\/]+)/',
86 2
            fn ($matches) => '://' . $this->idnToAscii($matches[1]),
87
            $value
88
        );
89
    }
90
91 9
    private function getPattern(): string
92
    {
93 9
        if (strpos($this->pattern, '{schemes}') !== false) {
94 7
            return str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern);
95
        }
96
97 2
        return $this->pattern;
98
    }
99
100 2
    public function pattern(string $pattern): self
101
    {
102 2
        $new = clone $this;
103 2
        $new->pattern = $pattern;
104 2
        return $new;
105
    }
106
107 9
    public function enableIDN(): self
108
    {
109 9
        if (!function_exists('idn_to_ascii')) {
110 1
            throw new \RuntimeException('In order to use IDN validation intl extension must be installed and enabled.');
111
        }
112
113 8
        $new = clone $this;
114 8
        $new->enableIDN = true;
115 8
        return $new;
116
    }
117
118 1
    public function schemes(array $schemes): self
119
    {
120 1
        $new = clone $this;
121 1
        $new->validSchemes = $schemes;
122 1
        return $new;
123
    }
124
125 6
    public function getOptions(): array
126
    {
127 6
        return array_merge(
128 6
            parent::getOptions(),
129
            [
130 6
                'message' => $this->formatMessage($this->message),
131 6
                'enableIDN' => $this->enableIDN,
132 6
                'validSchemes' => $this->validSchemes,
133 6
                'pattern' => $this->pattern,
134
            ],
135
        );
136
    }
137
}
138