Completed
Push — master ( 345ae0...ff8392 )
by Dmitry
03:40
created

DomainValidator::getErrorMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
crap 2
1
<?php
2
3
namespace kdn\yii2\validators;
4
5
use Yii;
6
use yii\base\InvalidConfigException;
7
use yii\validators\Validator;
8
9
/**
10
 * Class DomainValidator.
11
 * @package kdn\yii2\validators
12
 */
13
class DomainValidator extends Validator
14
{
15
    /**
16
     * @var boolean whether to allow underscores in domain name;
17
     * defaults to false
18
     */
19
    public $allowUnderscore = false;
20
21
    /**
22
     * @var boolean whether to allow URL address along with domain name;
23
     * defaults to true, meaning that validator should try to parse URL address and then validate domain name
24
     */
25
    public $allowURL = true;
26
27
    /**
28
     * @var boolean whether to check whether domain name exists;
29
     * be aware that this check can fail due to temporary DNS problems even if domain name exists;
30
     * do not use it to check domain name availability;
31
     * defaults to false
32
     */
33
    public $checkDNS = false;
34
35
    /**
36
     * @var boolean whether validation process should take into account IDN (internationalized domain names);
37
     * defaults to false, meaning that validation of domain names containing IDN will always fail;
38
     * note that in order to use IDN validation you have to install and enable `intl` PHP extension,
39
     * otherwise an exception would be thrown
40
     */
41
    public $enableIDN = false;
42
43
    /**
44
     * @var string the encoding of the string value to be validated (e.g. 'UTF-8');
45
     * if this property is not set, [[\yii\base\Application::charset]] will be used
46
     */
47
    public $encoding;
48
49
    /**
50
     * @var integer minimum number of domain name labels;
51
     * defaults to 1, meaning that domain name should contain at least 1 label
52
     * @see messageLabelNumberMin for the customized message for domain name with too small number of labels
53
     */
54
    public $labelNumberMin = 1;
55
56
    /**
57
     * @var string user-defined error message used when DNS record corresponding to domain name not found
58
     */
59
    public $messageDNS;
60
61
    /**
62
     * @var string user-defined error message used when domain name contains a character which 'intl' extension
63
     * failed to convert to ASCII
64
     */
65
    public $messageIdnToAscii;
66
67
    /**
68
     * @var string user-defined error message used when domain name contains an invalid character
69
     */
70
    public $messageInvalidCharacter;
71
72
    /**
73
     * @var string user-defined error message used when number of domain name labels is smaller than [[labelNumberMin]]
74
     */
75
    public $messageLabelNumberMin;
76
77
    /**
78
     * @var string user-defined error message used when domain name label starts or ends with an invalid character
79
     */
80
    public $messageLabelStartEnd;
81
82
    /**
83
     * @var string user-defined error message used when domain name label is too long
84
     */
85
    public $messageLabelTooLong;
86
87
    /**
88
     * @var string user-defined error message used when domain name label is too short
89
     */
90
    public $messageLabelTooShort;
91
92
    /**
93
     * @var string user-defined error message used when domain name is not a string
94
     */
95
    public $messageNotString;
96
97
    /**
98
     * @var string user-defined error message used when domain name is too long
99
     */
100
    public $messageTooLong;
101
102
    /**
103
     * @var string user-defined error message used when domain name is too short
104
     */
105
    public $messageTooShort;
106
107
    /**
108
     * @inheritdoc
109
     */
110 148
    public function init()
111
    {
112 148
        parent::init();
113 148
        if ($this->enableIDN && !function_exists('idn_to_ascii')) {
114 1
            throw new InvalidConfigException(
115
                'In order to use IDN validation intl extension must be installed and enabled.'
116 1
            );
117
        }
118 148
        if (!isset($this->encoding)) {
119 148
            $this->encoding = Yii::$app->charset;
120 148
        }
121 148
    }
122
123
    /**
124
     * @inheritdoc
125
     */
126 147
    protected function validateValue($value)
127
    {
128 147
        if (!is_string($value)) {
129 13
            return $this->getErrorMessage('messageNotString');
130
        }
131
132 134
        if ($this->allowURL) {
133 120
            $host = parse_url($value, PHP_URL_HOST);
134 120
            if (isset($host) && $host !== false) {
135 43
                $value = $host;
136 43
            }
137 120
        }
138
139 134
        if ($this->enableIDN) {
140 59
            $idnaInfo = null;
141 59
            $asciiValue = idn_to_ascii($value, 0, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
142 59
            if ($asciiValue !== false) {
143 39
                $value = $asciiValue;
144 39
            } else {
145 20
                $idnaErrors = null;
146 20
                if (is_array($idnaInfo) && array_key_exists('errors', $idnaInfo)) {
147 18
                    $idnaErrors = $idnaInfo['errors'];
148 18
                }
149 20
                if ($idnaErrors & IDNA_ERROR_EMPTY_LABEL) {
150 2
                    $errorName = 'messageLabelTooShort';
151 20
                } elseif ($idnaErrors & IDNA_ERROR_LABEL_TOO_LONG) {
152 2
                    $errorName = 'messageLabelTooLong';
153 18
                } elseif ($idnaErrors & IDNA_ERROR_DOMAIN_NAME_TOO_LONG) {
154 1
                    $errorName = 'messageTooLong';
155 16
                } elseif ($idnaErrors & IDNA_ERROR_LEADING_HYPHEN) {
156 6
                    $errorName = 'messageLabelStartEnd';
157 15
                } elseif ($idnaErrors & IDNA_ERROR_TRAILING_HYPHEN) {
158 6
                    $errorName = 'messageLabelStartEnd';
159 9
                } elseif ($idnaErrors & IDNA_ERROR_DISALLOWED) {
160 1
                    $errorName = 'messageIdnToAscii';
161 1
                } else {
162 2
                    if (empty($value)) {
163 1
                        $errorName = 'messageTooShort';
164 1
                    } else {
165 1
                        $errorName = 'messageTooLong';
166
                    }
167
                }
168 20
                return $this->getErrorMessage($errorName);
169
            }
170 39
        }
171
172 114
        if (empty($value)) {
173 2
            return $this->getErrorMessage('messageTooShort');
174
        }
175
176
        // ignore trailing dot
177 113
        if ($value[mb_strlen($value, $this->encoding) - 1] == '.') {
178 20
            $value = substr_replace($value, '', -1);
179 20
        }
180
181
        // 253 characters limit is same as 127 levels,
182
        // domain name with 127 levels with 1 character per label will be 253 characters long
183 113
        if (mb_strlen($value, $this->encoding) > 253) {
184 2
            return $this->getErrorMessage('messageTooLong');
185
        }
186
187 111
        $labels = explode('.', $value);
188 111
        $labelsCount = count($labels);
189
190 111
        if ($labelsCount < $this->labelNumberMin) {
191 2
            return $this->getErrorMessage(
192 2
                'messageLabelNumberMin',
193 2
                ['labelNumberMin' => $this->labelNumberMin, 'dotsNumberMin' => $this->labelNumberMin - 1]
194 2
            );
195
        }
196
197 111
        for ($i = 0; $i < $labelsCount; $i++) {
198 111
            $label = $labels[$i];
199 111
            $labelLength = mb_strlen($label, $this->encoding);
200
201 111
            if (empty($label)) {
202 6
                return $this->getErrorMessage('messageLabelTooShort');
203
            }
204
205 108
            if ($this->allowUnderscore) {
206 3
                $pattern = '/^[a-z\d-_]+$/i';
207 3
            } else {
208 105
                $pattern = '/^[a-z\d-]+$/i';
209
            }
210 108
            if (!preg_match($pattern, $label)) {
211 39
                return $this->getErrorMessage('messageInvalidCharacter');
212
            }
213
214 71
            if ($i == $labelsCount - 1) {
215
                // last domain name label
216 64
                if (!ctype_alpha($label[0])) {
217 3
                    return $this->getErrorMessage('messageLabelStartEnd');
218
                }
219 61
            } else {
220 63
                if (!ctype_alnum($label[0])) {
221 1
                    return $this->getErrorMessage('messageLabelStartEnd');
222
                }
223
            }
224 70
            if (!ctype_alnum($label[$labelLength - 1])) {
225 2
                return $this->getErrorMessage('messageLabelStartEnd');
226
            }
227
228 69
            if ($labelLength > 63) {
229 1
                return $this->getErrorMessage('messageLabelTooLong');
230
            }
231 68
        }
232
233 59
        if ($this->checkDNS) {
234 2
            if (!checkdnsrr($value, 'ANY')) {
235 2
                return $this->getErrorMessage('messageDNS');
236
            }
237 2
        }
238
239 59
        return null;
240
    }
241
242
    /**
243
     * Get error message by name.
244
     * @param string $name error message name
245
     * @param array $params parameters to be inserted into the error message
246
     * @return string error message.
247
     */
248 67
    protected function getErrorMessage($name, $params = [])
249
    {
250 67
        if (isset($this->$name)) {
251 3
            return [$this->$name, $params];
252
        }
253 66
        $this->$name = Yii::t('app', $this->getDefaultErrorMessages()[$name]); // todo app -> kdn-yii2
254 66
        return [$this->$name, $params];
255
    }
256
257
    /**
258
     * Get default error messages.
259
     * @return array default error messages.
260
     */
261 66
    protected function getDefaultErrorMessages()
262
    {
263
        $messages = [
264 66
            'messageDNS' => 'DNS record corresponding to {attribute} not found.',
265 66
            'messageIdnToAscii' => '{attribute} contains invalid characters.',
266
            'messageLabelNumberMin' =>
267
                '{attribute} should consist of at least {labelNumberMin} labels separated by ' .
268 66
                '{dotsNumberMin, plural, one{dot} other{dots}}.',
269 66
            'messageLabelTooShort' => 'Each label of {attribute} should contain at least 1 character.',
270 66
            'messageNotString' => '{attribute} must be a string.',
271 66
            'messageTooShort' => '{attribute} should contain at least 1 character.',
272 66
        ];
273 66
        if ($this->enableIDN) {
274 33
            $messages['messageLabelStartEnd'] =
275
                'Each label of {attribute} should start and end with letter or number.' .
276 33
                ' The rightmost label of {attribute} should start with letter.';
277 33
            $messages['messageLabelTooLong'] = 'Label of {attribute} is too long.';
278 33
            $messages['messageTooLong'] = '{attribute} is too long.';
279 33
            if ($this->allowUnderscore) {
280 1
                $messages['messageInvalidCharacter'] =
281
                    'Each label of {attribute} can consist of only letters, numbers, hyphens and underscores.';
282 1
            } else {
283 32
                $messages['messageInvalidCharacter'] =
284
                    'Each label of {attribute} can consist of only letters, numbers and hyphens.';
285
            }
286 33
        } else {
287 33
            $messages['messageLabelStartEnd'] =
288
                'Each label of {attribute} should start and end with latin letter or number.' .
289 33
                ' The rightmost label of {attribute} should start with latin letter.';
290 33
            $messages['messageLabelTooLong'] = 'Each label of {attribute} should contain at most 63 characters.';
291 33
            $messages['messageTooLong'] = '{attribute} should contain at most 253 characters.';
292 33
            if ($this->allowUnderscore) {
293 1
                $messages['messageInvalidCharacter'] =
294
                    'Each label of {attribute} can consist of only latin letters, numbers, hyphens and underscores.';
295 1
            } else {
296 32
                $messages['messageInvalidCharacter'] =
297
                    'Each label of {attribute} can consist of only latin letters, numbers and hyphens.';
298
            }
299
        }
300 66
        return $messages;
301
    }
302
}
303