Completed
Push — 2.1 ( 75349f...bf116e )
by Alexander
29:27
created

FileValidator::validateAttribute()   C

Complexity

Conditions 12
Paths 27

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 22
cts 22
cp 1
rs 5.1612
c 0
b 0
f 0
cc 12
eloc 23
nc 27
nop 2
crap 12

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\validators;
9
10
use Yii;
11
use yii\helpers\FileHelper;
12
use yii\http\UploadedFile;
13
14
/**
15
 * FileValidator verifies if an attribute is receiving a valid uploaded file.
16
 *
17
 * Note that you should enable `fileinfo` PHP extension.
18
 *
19
 * @property int $sizeLimit The size limit for uploaded files. This property is read-only.
20
 *
21
 * @author Qiang Xue <[email protected]>
22
 * @since 2.0
23
 */
24
class FileValidator extends Validator
25
{
26
    /**
27
     * @var array|string a list of file name extensions that are allowed to be uploaded.
28
     * This can be either an array or a string consisting of file extension names
29
     * separated by space or comma (e.g. "gif, jpg").
30
     * Extension names are case-insensitive. Defaults to null, meaning all file name
31
     * extensions are allowed.
32
     * @see wrongExtension for the customized message for wrong file type.
33
     */
34
    public $extensions;
35
    /**
36
     * @var bool whether to check file type (extension) with mime-type. If extension produced by
37
     * file mime-type check differs from uploaded file extension, the file will be considered as invalid.
38
     */
39
    public $checkExtensionByMimeType = true;
40
    /**
41
     * @var array|string a list of file MIME types that are allowed to be uploaded.
42
     * This can be either an array or a string consisting of file MIME types
43
     * separated by space or comma (e.g. "text/plain, image/png").
44
     * The mask with the special character `*` can be used to match groups of mime types.
45
     * For example `image/*` will pass all mime types, that begin with `image/` (e.g. `image/jpeg`, `image/png`).
46
     * Mime type names are case-insensitive. Defaults to null, meaning all MIME types are allowed.
47
     * @see wrongMimeType for the customized message for wrong MIME type.
48
     */
49
    public $mimeTypes;
50
    /**
51
     * @var int the minimum number of bytes required for the uploaded file.
52
     * Defaults to null, meaning no limit.
53
     * @see tooSmall for the customized message for a file that is too small.
54
     */
55
    public $minSize;
56
    /**
57
     * @var int the maximum number of bytes required for the uploaded file.
58
     * Defaults to null, meaning no limit.
59
     * Note, the size limit is also affected by `upload_max_filesize` and `post_max_size` INI setting
60
     * and the 'MAX_FILE_SIZE' hidden field value. See [[getSizeLimit()]] for details.
61
     * @see http://php.net/manual/en/ini.core.php#ini.upload-max-filesize
62
     * @see http://php.net/post-max-size
63
     * @see getSizeLimit
64
     * @see tooBig for the customized message for a file that is too big.
65
     */
66
    public $maxSize;
67
    /**
68
     * @var int the maximum file count the given attribute can hold.
69
     * Defaults to 1, meaning single file upload. By defining a higher number,
70
     * multiple uploads become possible. Setting it to `0` means there is no limit on
71
     * the number of files that can be uploaded simultaneously.
72
     *
73
     * > Note: The maximum number of files allowed to be uploaded simultaneously is
74
     * also limited with PHP directive `max_file_uploads`, which defaults to 20.
75
     *
76
     * @see http://php.net/manual/en/ini.core.php#ini.max-file-uploads
77
     * @see tooMany for the customized message when too many files are uploaded.
78
     */
79
    public $maxFiles = 1;
80
    /**
81
     * @var string the error message used when a file is not uploaded correctly.
82
     */
83
    public $message;
84
    /**
85
     * @var string the error message used when no file is uploaded.
86
     * Note that this is the text of the validation error message. To make uploading files required,
87
     * you have to set [[skipOnEmpty]] to `false`.
88
     */
89
    public $uploadRequired;
90
    /**
91
     * @var string the error message used when the uploaded file is too large.
92
     * You may use the following tokens in the message:
93
     *
94
     * - {attribute}: the attribute name
95
     * - {file}: the uploaded file name
96
     * - {limit}: the maximum size allowed (see [[getSizeLimit()]])
97
     * - {formattedLimit}: the maximum size formatted
98
     *   with [[\yii\i18n\Formatter::asShortSize()|Formatter::asShortSize()]]
99
     */
100
    public $tooBig;
101
    /**
102
     * @var string the error message used when the uploaded file is too small.
103
     * You may use the following tokens in the message:
104
     *
105
     * - {attribute}: the attribute name
106
     * - {file}: the uploaded file name
107
     * - {limit}: the value of [[minSize]]
108
     * - {formattedLimit}: the value of [[minSize]] formatted
109
     *   with [[\yii\i18n\Formatter::asShortSize()|Formatter::asShortSize()]
110
     */
111
    public $tooSmall;
112
    /**
113
     * @var string the error message used if the count of multiple uploads exceeds limit.
114
     * You may use the following tokens in the message:
115
     *
116
     * - {attribute}: the attribute name
117
     * - {limit}: the value of [[maxFiles]]
118
     */
119
    public $tooMany;
120
    /**
121
     * @var string the error message used when the uploaded file has an extension name
122
     * that is not listed in [[extensions]]. You may use the following tokens in the message:
123
     *
124
     * - {attribute}: the attribute name
125
     * - {file}: the uploaded file name
126
     * - {extensions}: the list of the allowed extensions.
127
     */
128
    public $wrongExtension;
129
    /**
130
     * @var string the error message used when the file has an mime type
131
     * that is not allowed by [[mimeTypes]] property.
132
     * You may use the following tokens in the message:
133
     *
134
     * - {attribute}: the attribute name
135
     * - {file}: the uploaded file name
136
     * - {mimeTypes}: the value of [[mimeTypes]]
137
     */
138
    public $wrongMimeType;
139
140
141
    /**
142
     * @inheritdoc
143
     */
144 36
    public function init()
145
    {
146 36
        parent::init();
147 36
        if ($this->message === null) {
148 36
            $this->message = Yii::t('yii', 'File upload failed.');
149
        }
150 36
        if ($this->uploadRequired === null) {
151 36
            $this->uploadRequired = Yii::t('yii', 'Please upload a file.');
152
        }
153 36
        if ($this->tooMany === null) {
154 36
            $this->tooMany = Yii::t('yii', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.');
155
        }
156 36
        if ($this->wrongExtension === null) {
157 36
            $this->wrongExtension = Yii::t('yii', 'Only files with these extensions are allowed: {extensions}.');
158
        }
159 36
        if ($this->tooBig === null) {
160 36
            $this->tooBig = Yii::t('yii', 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.');
161
        }
162 36
        if ($this->tooSmall === null) {
163 36
            $this->tooSmall = Yii::t('yii', 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.');
164
        }
165 36
        if (!is_array($this->extensions)) {
166 23
            $this->extensions = preg_split('/[\s,]+/', strtolower($this->extensions), -1, PREG_SPLIT_NO_EMPTY);
167
        } else {
168 15
            $this->extensions = array_map('strtolower', $this->extensions);
169
        }
170 36
        if ($this->wrongMimeType === null) {
171 36
            $this->wrongMimeType = Yii::t('yii', 'Only files with these MIME types are allowed: {mimeTypes}.');
172
        }
173 36
        if (!is_array($this->mimeTypes)) {
174 36
            $this->mimeTypes = preg_split('/[\s,]+/', strtolower($this->mimeTypes), -1, PREG_SPLIT_NO_EMPTY);
175
        } else {
176 1
            $this->mimeTypes = array_map('strtolower', $this->mimeTypes);
177
        }
178 36
    }
179
180
    /**
181
     * @inheritdoc
182
     */
183 7
    public function validateAttribute($model, $attribute)
184
    {
185 7
        if ($this->maxFiles != 1) {
186 1
            $files = $model->$attribute;
187 1
            if (!is_array($files)) {
188 1
                $this->addError($model, $attribute, $this->uploadRequired);
189
190 1
                return;
191
            }
192 1
            foreach ($files as $i => $file) {
193 1
                if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) {
194 1
                    unset($files[$i]);
195
                }
196
            }
197 1
            $model->$attribute = $files;
198 1
            if (empty($files)) {
199 1
                $this->addError($model, $attribute, $this->uploadRequired);
200
            }
201 1
            if ($this->maxFiles && count($files) > $this->maxFiles) {
202 1
                $this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]);
203
            } else {
204 1
                foreach ($files as $file) {
205 1
                    $result = $this->validateValue($file);
206 1
                    if (!empty($result)) {
207 1
                        $this->addError($model, $attribute, $result[0], $result[1]);
208
                    }
209
                }
210
            }
211
        } else {
212 7
            $result = $this->validateValue($model->$attribute);
213 7
            if (!empty($result)) {
214 7
                $this->addError($model, $attribute, $result[0], $result[1]);
215
            }
216
        }
217 7
    }
218
219
    /**
220
     * @inheritdoc
221
     */
222 31
    protected function validateValue($value)
223
    {
224 31
        if (!$value instanceof UploadedFile || $value->getError() == UPLOAD_ERR_NO_FILE) {
225 1
            return [$this->uploadRequired, []];
226
        }
227
228 31
        switch ($value->error) {
229 31
            case UPLOAD_ERR_OK:
230 27
                if ($this->maxSize !== null && $value->size > $this->getSizeLimit()) {
231
                    return [
232 1
                        $this->tooBig,
233
                        [
234 1
                            'file' => $value->getClientFilename(),
235 1
                            'limit' => $this->getSizeLimit(),
236 1
                            'formattedLimit' => Yii::$app->formatter->asShortSize($this->getSizeLimit()),
237
                        ],
238
                    ];
239 27
                } elseif ($this->minSize !== null && $value->size < $this->minSize) {
240
                    return [
241 1
                        $this->tooSmall,
242
                        [
243 1
                            'file' => $value->getClientFilename(),
244 1
                            'limit' => $this->minSize,
245 1
                            'formattedLimit' => Yii::$app->formatter->asShortSize($this->minSize),
246
                        ],
247
                    ];
248 27
                } elseif (!empty($this->extensions) && !$this->validateExtension($value)) {
249 7
                    return [$this->wrongExtension, ['file' => $value->getClientFilename(), 'extensions' => implode(', ', $this->extensions)]];
250 22
                } elseif (!empty($this->mimeTypes) && !$this->validateMimeType($value)) {
251 5
                    return [$this->wrongMimeType, ['file' => $value->getClientFilename(), 'mimeTypes' => implode(', ', $this->mimeTypes)]];
252
                }
253
254 17
                return null;
255 5
            case UPLOAD_ERR_INI_SIZE:
256 5
            case UPLOAD_ERR_FORM_SIZE:
257 1
                return [$this->tooBig, [
258 1
                    'file' => $value->getClientFilename(),
259 1
                    'limit' => $this->getSizeLimit(),
260 1
                    'formattedLimit' => Yii::$app->formatter->asShortSize($this->getSizeLimit()),
261
                ]];
262 5
            case UPLOAD_ERR_PARTIAL:
263 2
                Yii::warning('File was only partially uploaded: ' . $value->getClientFilename(), __METHOD__);
264 2
                break;
265 3
            case UPLOAD_ERR_NO_TMP_DIR:
266 1
                Yii::warning('Missing the temporary folder to store the uploaded file: ' . $value->getClientFilename(), __METHOD__);
267 1
                break;
268 2
            case UPLOAD_ERR_CANT_WRITE:
269 1
                Yii::warning('Failed to write the uploaded file to disk: ' . $value->getClientFilename(), __METHOD__);
270 1
                break;
271 1
            case UPLOAD_ERR_EXTENSION:
272 1
                Yii::warning('File upload was stopped by some PHP extension: ' . $value->getClientFilename(), __METHOD__);
273 1
                break;
274
            default:
275
                break;
276
        }
277
278 5
        return [$this->message, []];
279
    }
280
281
    /**
282
     * Returns the maximum size allowed for uploaded files.
283
     *
284
     * This is determined based on four factors:
285
     *
286
     * - 'upload_max_filesize' in php.ini
287
     * - 'post_max_size' in php.ini
288
     * - 'MAX_FILE_SIZE' hidden field
289
     * - [[maxSize]]
290
     *
291
     * @return int the size limit for uploaded files.
292
     */
293 2
    public function getSizeLimit()
294
    {
295
        // Get the lowest between post_max_size and upload_max_filesize, log a warning if the first is < than the latter
296 2
        $limit = $this->sizeToBytes(ini_get('upload_max_filesize'));
297 2
        $postLimit = $this->sizeToBytes(ini_get('post_max_size'));
298 2
        if ($postLimit > 0 && $postLimit < $limit) {
299
            Yii::warning('PHP.ini\'s \'post_max_size\' is less than \'upload_max_filesize\'.', __METHOD__);
300
            $limit = $postLimit;
301
        }
302 2
        if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) {
303 2
            $limit = $this->maxSize;
304
        }
305
306 2
        if (($request = Yii::$app->getRequest()) instanceof \yii\web\Request) {
307 1
            $maxFileSize = Yii::$app->getRequest()->getParsedBodyParam('MAX_FILE_SIZE', 0);
308 1
            if ($maxFileSize > 0 && $maxFileSize < $limit) {
309 1
                $limit = (int)$maxFileSize;
310
            }
311
        }
312
313 2
        return $limit;
314
    }
315
316
    /**
317
     * @inheritdoc
318
     * @param bool $trim
319
     */
320 1
    public function isEmpty($value, $trim = false)
0 ignored issues
show
Unused Code introduced by
The parameter $trim is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
321
    {
322 1
        $value = is_array($value) ? reset($value) : $value;
323 1
        return !($value instanceof UploadedFile) || $value->error == UPLOAD_ERR_NO_FILE;
324
    }
325
326
    /**
327
     * Converts php.ini style size to bytes.
328
     *
329
     * @param string $sizeStr $sizeStr
330
     * @return int
331
     */
332 2
    private function sizeToBytes($sizeStr)
333
    {
334 2
        switch (substr($sizeStr, -1)) {
335 2
            case 'M':
336
            case 'm':
337 2
                return (int) $sizeStr * 1048576;
338
            case 'K':
339
            case 'k':
340
                return (int) $sizeStr * 1024;
341
            case 'G':
342
            case 'g':
343
                return (int) $sizeStr * 1073741824;
344
            default:
345
                return (int) $sizeStr;
346
        }
347
    }
348
349
    /**
350
     * Checks if given uploaded file have correct type (extension) according current validator settings.
351
     * @param UploadedFile $file
352
     * @return bool
353
     */
354 14
    protected function validateExtension($file)
355
    {
356 14
        $extension = mb_strtolower($file->extension, 'UTF-8');
357
358 14
        if ($this->checkExtensionByMimeType) {
359 12
            $mimeType = FileHelper::getMimeType($file->tempFilename, null, false);
360 12
            if ($mimeType === null) {
361
                return false;
362
            }
363
364 12
            $extensionsByMimeType = FileHelper::getExtensionsByMimeType($mimeType);
365
366 12
            if (!in_array($extension, $extensionsByMimeType, true)) {
367
                return false;
368
            }
369
        }
370
371 14
        if (!in_array($extension, $this->extensions, true)) {
372 7
            return false;
373
        }
374
375 9
        return true;
376
    }
377
378
    /**
379
     * Builds the RegExp from the $mask.
380
     *
381
     * @param string $mask
382
     * @return string the regular expression
383
     * @see mimeTypes
384
     */
385 11
    public function buildMimeTypeRegexp($mask)
386
    {
387 11
        return '/^' . str_replace('\*', '.*', preg_quote($mask, '/')) . '$/';
388
    }
389
390
    /**
391
     * Checks the mimeType of the $file against the list in the [[mimeTypes]] property.
392
     *
393
     * @param UploadedFile $file
394
     * @return bool whether the $file mimeType is allowed
395
     * @throws \yii\base\InvalidConfigException
396
     * @see mimeTypes
397
     * @since 2.0.8
398
     */
399 12
    protected function validateMimeType($file)
400
    {
401 12
        $fileMimeType = FileHelper::getMimeType($file->tempFilename);
402
403 12
        foreach ($this->mimeTypes as $mimeType) {
0 ignored issues
show
Bug introduced by
The expression $this->mimeTypes of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
404 12
            if ($mimeType === $fileMimeType) {
405
                return true;
406
            }
407
408 12
            if (strpos($mimeType, '*') !== false && preg_match($this->buildMimeTypeRegexp($mimeType), $fileMimeType)) {
409 12
                return true;
410
            }
411
        }
412
413 5
        return false;
414
    }
415
}
416