Completed
Push — master ( 719e85...a85d2e )
by Dmitry
02:46
created

DomainValidator::init()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 20
ccs 16
cts 16
cp 1
rs 8.8571
cc 5
eloc 13
nc 5
nop 0
crap 5
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 string the base path for all translated messages; specify it if you want to use custom translated messages
51
     */
52
    public $i18nBasePath;
53
54
    /**
55
     * @var integer minimum number of domain name labels;
56
     * defaults to 2, meaning that domain name should contain at least 2 labels
57
     * @see messageLabelNumberMin for the customized message for domain name with too small number of labels
58
     */
59
    public $labelNumberMin = 2;
60
61
    /**
62
     * @var string user-defined error message used when DNS record corresponding to domain name not found;
63
     * you may use the following placeholders in the message:
64
     * - `{attribute}`: the label of the attribute being validated
65
     * - `{value}`: the value of the attribute being validated
66
     */
67
    public $messageDNS;
68
69
    /**
70
     * @var string user-defined error message used when domain name is invalid but
71
     * reason is too complicated for explanation to end-user or details are not needed at all;
72
     * you may use the following placeholders in the message:
73
     * - `{attribute}`: the label of the attribute being validated
74
     * - `{value}`: the value of the attribute being validated
75
     * @see simpleErrorMessage to use this message for all possible errors
76
     */
77
    public $message;
78
79
    /**
80
     * @var string user-defined error message used when domain name contains an invalid character;
81
     * you may use the following placeholders in the message:
82
     * - `{attribute}`: the label of the attribute being validated
83
     * - `{value}`: the value of the attribute being validated
84
     */
85
    public $messageInvalidCharacter;
86
87
    /**
88
     * @var string user-defined error message used when number of domain name labels is smaller than [[labelNumberMin]];
89
     * you may use the following placeholders in the message:
90
     * - `{attribute}`: the label of the attribute being validated
91
     * - `{labelNumberMin}`: the value of [[labelNumberMin]]
92
     * - `{value}`: the value of the attribute being validated
93
     */
94
    public $messageLabelNumberMin;
95
96
    /**
97
     * @var string user-defined error message used when domain name label starts or ends with an invalid character;
98
     * you may use the following placeholders in the message:
99
     * - `{attribute}`: the label of the attribute being validated
100
     * - `{value}`: the value of the attribute being validated
101
     */
102
    public $messageLabelStartEnd;
103
104
    /**
105
     * @var string user-defined error message used when domain name label is too long;
106
     * you may use the following placeholders in the message:
107
     * - `{attribute}`: the label of the attribute being validated
108
     * - `{value}`: the value of the attribute being validated
109
     */
110
    public $messageLabelTooLong;
111
112
    /**
113
     * @var string user-defined error message used when domain name label is too short;
114
     * you may use the following placeholders in the message:
115
     * - `{attribute}`: the label of the attribute being validated
116
     * - `{value}`: the value of the attribute being validated
117
     */
118
    public $messageLabelTooShort;
119
120
    /**
121
     * @var string user-defined error message used when domain name is not a string;
122
     * you may use the following placeholders in the message:
123
     * - `{attribute}`: the label of the attribute being validated
124
     * - `{value}`: the value of the attribute being validated
125
     */
126
    public $messageNotString;
127
128
    /**
129
     * @var string user-defined error message used when domain name is too long;
130
     * you may use the following placeholders in the message:
131
     * - `{attribute}`: the label of the attribute being validated
132
     * - `{value}`: the value of the attribute being validated
133
     */
134
    public $messageTooLong;
135
136
    /**
137
     * @var string user-defined error message used when domain name is too short;
138
     * you may use the following placeholders in the message:
139
     * - `{attribute}`: the label of the attribute being validated
140
     * - `{value}`: the value of the attribute being validated
141
     */
142
    public $messageTooShort;
143
144
    /**
145
     * @var boolean whether to always use simple error message;
146
     * defaults to false, meaning that validator should use specialized error messages for different errors,
147
     * it should help end-user to understand reason of error; set it to true if detailed error messages don't fit
148
     * for your application then [[message]] will be used in all cases
149
     */
150
    public $simpleErrorMessage = false;
151
152
    /**
153
     * @inheritdoc
154
     */
155 197
    public function init()
156
    {
157 197
        parent::init();
158 197
        if ($this->enableIDN && !function_exists('idn_to_ascii')) {
159 1
            throw new InvalidConfigException(
160
                'In order to use IDN validation intl extension must be installed and enabled.'
161 1
            );
162
        }
163 197
        if (!isset($this->encoding)) {
164 197
            $this->encoding = Yii::$app->charset;
165 197
        }
166 197
        if (!isset($this->i18nBasePath)) {
167 197
            $this->i18nBasePath = dirname(__DIR__) . '/messages';
168 197
        }
169 197
        Yii::$app->i18n->translations['kdn/yii2/validators/domain'] = [
170 197
            'class' => 'yii\i18n\PhpMessageSource',
171 197
            'basePath' => $this->i18nBasePath,
172 197
            'fileMap' => ['kdn/yii2/validators/domain' => 'domain.php'],
173
        ];
174 197
    }
175
176
    /**
177
     * @inheritdoc
178
     */
179 196
    protected function validateValue($value)
180
    {
181 196
        if (!is_string($value)) {
182 13
            return $this->getErrorMessage('messageNotString');
183
        }
184
185 183
        if (empty($value)) {
186 2
            return $this->getErrorMessage('messageTooShort');
187
        }
188
189 181
        if ($this->allowURL) {
190 133
            $host = parse_url($value, PHP_URL_HOST);
191 133
            if (isset($host) && $host !== false) {
192 46
                $value = $host;
193 46
            }
194 133
        }
195
196 181
        if ($this->enableIDN) {
197 102
            $idnaInfo = null;
198 102
            $options = IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ;
199 102
            $asciiValue = idn_to_ascii($value, $options, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
200 102
            if ($asciiValue !== false) {
201 73
                $value = $asciiValue;
202 73
            } else {
203 29
                $idnaErrors = null;
204 29
                if (is_array($idnaInfo) && array_key_exists('errors', $idnaInfo)) {
205 26
                    $idnaErrors = $idnaInfo['errors'];
206 26
                }
207 29
                if ($idnaErrors & IDNA_ERROR_DOMAIN_NAME_TOO_LONG) {
208 1
                    $errorMessageName = 'messageTooLong';
209 29
                } elseif ($idnaErrors & IDNA_ERROR_EMPTY_LABEL) {
210 2
                    $errorMessageName = 'messageLabelTooShort';
211 28
                } elseif ($idnaErrors & IDNA_ERROR_LABEL_TOO_LONG) {
212 4
                    $errorMessageName = 'messageLabelTooLong';
213 26
                } elseif ($idnaErrors & IDNA_ERROR_DISALLOWED) {
214 1
                    $errorMessageName = 'messageInvalidCharacter';
215 22
                } elseif ($idnaErrors & IDNA_ERROR_LEADING_HYPHEN || $idnaErrors & IDNA_ERROR_TRAILING_HYPHEN) {
216 12
                    $errorMessageName = 'messageLabelStartEnd';
217 21
                } elseif (empty($idnaInfo)) {
218 3
                    $errorMessageName = 'messageTooLong';
219 3
                } else {
220 6
                    $errorMessageName = 'message';
221
                }
222 29
                return $this->getErrorMessage($errorMessageName);
223
            }
224 73
        }
225
226
        // ignore trailing dot
227 152
        if (mb_substr($value, -1, 1, $this->encoding) == '.') {
228 27
            $value = substr_replace($value, '', -1);
229 27
        }
230
231
        // 253 characters limit is same as 127 levels,
232
        // domain name with 127 levels with 1 character per label will be 253 characters long
233 152
        if (mb_strlen($value, $this->encoding) > 253) {
234 2
            return $this->getErrorMessage('messageTooLong');
235
        }
236
237 150
        $labels = explode('.', $value);
238 150
        $labelsCount = count($labels);
239
240 150
        if ($labelsCount < $this->labelNumberMin) {
241 3
            return $this->getErrorMessage('messageLabelNumberMin', ['labelNumberMin' => $this->labelNumberMin]);
242
        }
243
244 150
        for ($i = 0; $i < $labelsCount; $i++) {
245 150
            $label = $labels[$i];
246 150
            $labelLength = mb_strlen($label, $this->encoding);
247
248 150
            if (empty($label)) {
249 6
                return $this->getErrorMessage('messageLabelTooShort');
250
            }
251
252 147
            if ($labelLength > 63) {
253 2
                return $this->getErrorMessage('messageLabelTooLong');
254
            }
255
256 145
            if ($this->allowUnderscore) {
257 2
                $pattern = '/^[a-z\d-_]+$/i';
258 2
            } else {
259 143
                $pattern = '/^[a-z\d-]+$/i';
260
            }
261 145
            if (!preg_match($pattern, $label)) {
262 58
                return $this->getErrorMessage('messageInvalidCharacter');
263
            }
264
265 91
            if ($i == $labelsCount - 1 && !ctype_alpha($label[0])
266 91
                || !ctype_alnum($label[0])
267 90
                || !ctype_alnum($label[$labelLength - 1])
268 91
            ) {
269 7
                return $this->getErrorMessage('messageLabelStartEnd');
270
            }
271 88
        }
272
273 79
        if ($this->checkDNS && !checkdnsrr($value, 'ANY')) {
274 2
            return $this->getErrorMessage('messageDNS');
275
        }
276
277 79
        return null;
278
    }
279
280
    /**
281
     * Get error message by name.
282
     * @param string $name error message name
283
     * @param array $params parameters to be inserted into the error message
284
     * @return string error message.
285
     */
286 100
    protected function getErrorMessage($name, $params = [])
287
    {
288 100
        if ($this->simpleErrorMessage) {
289 1
            $name = 'message';
290 1
        }
291 100
        if (isset($this->$name)) {
292 4
            return [$this->$name, $params];
293
        }
294 99
        $this->$name = Yii::t('kdn/yii2/validators/domain', $this->getDefaultErrorMessages()[$name]);
295 99
        return [$this->$name, $params];
296
    }
297
298
    /**
299
     * Get default error messages.
300
     * @return array default error messages.
301
     */
302 99
    protected function getDefaultErrorMessages()
303
    {
304
        $messages = [
305 99
            'message' => '{attribute} is invalid.',
306 99
            'messageDNS' => 'DNS record corresponding to {attribute} not found.',
307
            'messageLabelNumberMin' =>
308
                '{attribute} should consist of at least {labelNumberMin, number} labels separated by ' .
309 99
                '{labelNumberMin, plural, =2{dot} other{dots}}.',
310 99
            'messageLabelTooShort' => 'Each label of {attribute} should contain at least 1 character.',
311 99
            'messageNotString' => '{attribute} must be a string.',
312 99
            'messageTooShort' => '{attribute} should contain at least 1 character.',
313 99
        ];
314 99
        if ($this->enableIDN) {
315 41
            $messages['messageLabelStartEnd'] =
316
                'Each label of {attribute} should start and end with letter or number.' .
317 41
                ' The rightmost label of {attribute} should start with letter.';
318 41
            $messages['messageLabelTooLong'] = 'Label of {attribute} is too long.';
319 41
            $messages['messageTooLong'] = '{attribute} is too long.';
320 41
            if ($this->allowUnderscore) {
321 1
                $messages['messageInvalidCharacter'] =
322
                    'Each label of {attribute} can consist of only letters, numbers, hyphens and underscores.';
323 1
            } else {
324 40
                $messages['messageInvalidCharacter'] =
325
                    'Each label of {attribute} can consist of only letters, numbers and hyphens.';
326
            }
327 41
        } else {
328 58
            $messages['messageLabelStartEnd'] =
329
                'Each label of {attribute} should start and end with latin letter or number.' .
330 58
                ' The rightmost label of {attribute} should start with latin letter.';
331 58
            $messages['messageLabelTooLong'] = 'Each label of {attribute} should contain at most 63 characters.';
332 58
            $messages['messageTooLong'] = '{attribute} should contain at most 253 characters.';
333 58
            if ($this->allowUnderscore) {
334 1
                $messages['messageInvalidCharacter'] =
335
                    'Each label of {attribute} can consist of only latin letters, numbers, hyphens and underscores.';
336 1
            } else {
337 57
                $messages['messageInvalidCharacter'] =
338
                    'Each label of {attribute} can consist of only latin letters, numbers and hyphens.';
339
            }
340
        }
341 99
        return $messages;
342
    }
343
}
344