Completed
Pull Request — master (#175)
by Alexander
02:53 queued 02:53
created

Url::validateValue()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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