UploadHandler::handleUpload()   D
last analyzed

Complexity

Conditions 21
Paths 48

Size

Total Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 60
rs 4.1666
c 0
b 0
f 0
cc 21
nc 48
nop 0

How to fix   Long Method    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
namespace yiicod\fileupload\components;
4
5
use Exception;
6
use stdClass;
7
use Yii;
8
use yii\helpers\ArrayHelper;
9
use yii\helpers\FileHelper;
10
use yiicod\fileupload\components\base\AccessUploadInterface;
11
use yiicod\fileupload\components\base\EventsUploaderInterface;
12
use yiicod\fileupload\components\base\UploaderInterface;
13
use yiicod\fileupload\components\common\FileName;
14
use yiicod\fileupload\components\common\UploadedFile;
15
use yiicod\fileupload\components\traits\ServerVariableTrait;
16
use yiicod\fileupload\components\traits\UploadedFileTrait;
17
use yiicod\fileupload\validators\FilesCountValidator;
18
use yiicod\fileupload\validators\FileSizeValidator;
19
use yiicod\fileupload\validators\FileTypeValidator;
20
use yiicod\fileupload\validators\FileUploadValidator;
21
use yiicod\fileupload\validators\PostSizeValidator;
22
use yiicod\fileupload\validators\ValidatorInterface;
23
24
/**
25
 * Class UploadHandler
26
 *
27
 * @package yiicod\fileupload\libs
28
 */
29
class UploadHandler
30
{
31
    use ServerVariableTrait,
32
        UploadedFileTrait;
33
34
    /**
35
     * List of file validators
36
     *
37
     * @var array
38
     */
39
    public $validators = [
40
        'FileUploadValidator' => [
41
            'class' => FileUploadValidator::class,
42
        ],
43
        'PostSizeValidator' => [
44
            'class' => PostSizeValidator::class,
45
        ],
46
        'FileTypeValidator' => [
47
            'class' => FileTypeValidator::class,
48
        ],
49
        'FileSizeValidator' => [
50
            'class' => FileSizeValidator::class,
51
        ],
52
        'FilesCountValidator' => [
53
            'class' => FilesCountValidator::class,
54
        ],
55
    ];
56
57
    /**
58
     * @var array
59
     */
60
    protected $options;
61
62
    /**
63
     * UploadHandler constructor.
64
     *
65
     * @param array $options
66
     * @param array $validators
67
     */
68
    public function __construct(array $options, array $validators = [])
69
    {
70
        $this->options = ArrayHelper::merge([
71
            'upload_dir' => dirname($this->getServerVar('SCRIPT_FILENAME')) . '/files/',
72
            'mkdir_mode' => 0755,
73
            'param_name' => 'files',
74
        ], $options);
75
76
        $this->validators = ArrayHelper::merge($this->validators, $validators);
77
    }
78
79
    /**
80
     * Post trigger.
81
     *
82
     * @param string $uploader
83
     * @param array $userData
84
     *
85
     * @return array
86
     *
87
     * @throws Exception
88
     */
89
    public function upload(string $uploader, array $userData)
90
    {
91
        $result = $this->handleUpload();
92
93
        $can = $this->canDo();
94
        if ($can) {
95
            /** @var UploaderInterface $inst */
96
            $uploader = new $uploader();
97
            if (false === is_a($uploader, UploaderInterface::class)) {
98
                throw new Exception('Uploader class must be instanceof UploaderInterface', 500);
99
            }
100
            if ($can && is_a($uploader, AccessUploadInterface::class)) {
101
                /** @var AccessUploadInterface $uploader */
102
                $can = $uploader->canUpload($userData);
103
            }
104
            if ($can && is_a($uploader, EventsUploaderInterface::class)) {
105
                /** @var EventsUploaderInterface $uploader */
106
                $can = $uploader->beforeUploading($userData);
107
            }
108
            if ($can) {
109
                foreach ($result[$this->options['param_name']] as $i => $fileData) {
110
                    $result[$this->options['param_name']][$i] = $this->onFileUpload($uploader, (array)$fileData, $userData);
111
                }
112
            } else {
113
                foreach ($result[$this->options['param_name']] as $i => $fileData) {
114
                    $result[$this->options['param_name']][$i]['isSuccess'] = false;
115
                    if (false === $can && is_a($uploader, AccessUploadInterface::class)) {
116
                        $result[$this->options['param_name']][$i]['error'] = $uploader->deniedUpload($userData);
117
                    }
118
                }
119
            }
120
            if ($can && is_a($uploader, EventsUploaderInterface::class)) {
121
                /* @var EventsUploaderInterface $uploader */
122
                $uploader->afterUploading($userData, $result[$this->options['param_name']]);
123
            }
124
        }
125
126
        return $result;
127
    }
128
129
    /**
130
     * Callback on post end method.
131
     *
132
     * @param UploaderInterface $uploader
133
     * @param $fileData
134
     * @param $userData
135
     *
136
     * @return array
137
     */
138
    public function onFileUpload(UploaderInterface $uploader, array $fileData, array $userData)
139
    {
140
        $path = $this->getUploadPath();
141
        $filePath = (rtrim($path, '/') . '/' . $fileData['name']);
142
143
        if (false === isset($fileData['error'])) {
144
            $fileData['isSuccess'] = true;
145
146
            $result = $uploader->upload($filePath, $userData, $fileData);
147
148
            if (is_array($result)) {
149
                $fileData = ArrayHelper::merge($fileData, $result);
150
            }
151
        } else {
152
            $fileData['isSuccess'] = false;
153
        }
154
155
        return $fileData;
156
    }
157
158
    /**
159
     * Handle post upload
160
     *
161
     * @return array
162
     */
163
    protected function handleUpload(): array
164
    {
165
        $upload = $this->getUploadData($this->options['param_name']);
166
        if ($upload && is_array($upload['tmp_name'])) {
167
            foreach ($upload['tmp_name'] as $index => $value) {
168
                $this->fileTypeExecutable($upload['tmp_name'][$index]);
169
            }
170
        } else {
171
            $this->fileTypeExecutable($upload['tmp_name']);
172
        }
173
174
        // Parse the Content-Disposition header, if available:
175
        $contentDispositionHeader = $this->getServerVar('HTTP_CONTENT_DISPOSITION');
176
        $fileName = $contentDispositionHeader ?
177
            rawurldecode(preg_replace(
178
                '/(^[^"]+")|("$)/',
179
                '',
180
                $contentDispositionHeader
181
            )) : null;
182
183
        // Parse the Content-Range header, which has the following form:
184
        // Content-Range: bytes 0-524287/2000000
185
        $contentRangeHeader = $this->getServerVar('HTTP_CONTENT_RANGE');
186
        $contentRange = $contentRangeHeader ?
187
            preg_split('/[^0-9]+/', $contentRangeHeader) : null;
188
        $size = $contentRange ? $contentRange[3] : null;
189
190
        $files = [];
191
        if ($upload) {
192
            if (is_array($upload['tmp_name'])) {
193
                // param_name is an array identifier like "files[]",
194
                // $upload is a multi-dimensional array:
195
                foreach ($upload['tmp_name'] as $index => $value) {
196
                    $files[] = $this->handleFileUpload(new UploadedFile($upload['tmp_name'][$index],
197
                        $fileName ? $fileName : $upload['name'][$index],
198
                        $size ? $size : $upload['size'][$index],
199
                        $upload['type'][$index],
200
                        ($upload['error'][$index]) ? $upload['error'][$index] : null,
201
                        $index,
202
                        $contentRange));
203
                }
204
            } else {
205
                // param_name is a single object identifier like "file",
206
                // $upload is a one-dimensional array:
207
                $files[] = $this->handleFileUpload(new UploadedFile(
208
                    isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
209
                    $fileName ? $fileName : (isset($upload['name']) ?
210
                        $upload['name'] : null),
211
                    $size ? $size : (isset($upload['size']) ?
212
                        $upload['size'] : $this->getServerVar('CONTENT_LENGTH')),
213
                    isset($upload['type']) ?
214
                        $upload['type'] : $this->getServerVar('CONTENT_TYPE'),
215
                    (isset($upload['error']) && $upload['error']) ? $upload['error'] : null,
216
                    null,
217
                    $contentRange));
218
            }
219
        }
220
221
        return [$this->options['param_name'] => $files];
222
    }
223
224
    /**
225
     * Can do upload
226
     *
227
     * @return bool
228
     */
229
    protected function canDo()
230
    {
231
        $contentRange = $this->getServerVar('HTTP_CONTENT_RANGE') ?
232
            preg_split('/[^0-9]+/', $this->getServerVar('HTTP_CONTENT_RANGE')) : null;
233
234
        if (($contentRange && $contentRange[3] - 1 == $contentRange[2]) || empty($contentRange)) {
235
            return true;
236
        }
237
238
        return false;
239
    }
240
241
    /**
242
     * Handle file upload
243
     *
244
     * @param UploadedFile $uploadedFile
245
     *
246
     * @return stdClass
247
     *
248
     * @throws Exception
249
     */
250
    protected function handleFileUpload(UploadedFile $uploadedFile)
251
    {
252
        $file = $uploadedFile;
253
        $file->name = (new FileName($this->options['upload_dir'], $this->options['file_name_length'] ?? null))->getFileName($file);
254
        $file->size = (int)$file->size;
255
        if ($this->validate($file)) {
256
            $uploadDir = $this->getUploadPath();
257
            if (!is_dir($uploadDir)) {
258
                FileHelper::createDirectory($uploadDir, $this->options['mkdir_mode'], true);
259
            }
260
            $filePath = $this->getUploadPath($file->name);
261
            $appendFile = $file->contentRange && is_file($filePath) &&
262
                $file->size > $this->getFileSize($filePath);
263
            if ($file->filePath && is_uploaded_file($file->filePath)) {
264
                // multipart/formdata uploads (POST method uploads)
265
                if ($appendFile) {
266
                    file_put_contents(
267
                        $filePath,
268
                        fopen($file->filePath, 'r'),
269
                        FILE_APPEND
270
                    );
271
                } else {
272
                    move_uploaded_file($file->filePath, $filePath);
273
                }
274
            } else {
275
                throw new Exception('File doesn\'t uploaded.');
276
            }
277
278
            $fileSize = $this->getFileSize($filePath, $appendFile);
279
            if ($fileSize !== $file->size) {
280
                $file->size = $fileSize;
281
                if (!$file->contentRange && $this->options['discard_aborted_uploads']) {
282
                    unlink($filePath);
283
                    $file->error = Yii::t('yiicod_fileupload', 'File upload aborted.');
284
                }
285
            }
286
        }
287
288
        return (object)(array)$file;
289
    }
290
291
    /**
292
     * Validate file
293
     *
294
     * @param $uploadedFile
295
     *
296
     * @return bool
297
     */
298
    protected function validate(UploadedFile &$uploadedFile)
299
    {
300
        foreach ($this->validators as $item) {
301
            /** @var ValidatorInterface $validator */
302
            $validator = Yii::createObject($item);
303
            if (false === $validator->validate($uploadedFile)) {
304
                return false;
305
            }
306
        }
307
308
        return true;
309
    }
310
311
    /**
312
     * Get file size
313
     *
314
     * @param string $filePath
315
     * @param bool $clearStatCache
316
     *
317
     * @return float
318
     */
319
    protected function getFileSize(string $filePath, bool $clearStatCache = false)
320
    {
321
        if ($clearStatCache) {
322
            if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
323
                clearstatcache(true, $filePath);
324
            } else {
325
                clearstatcache();
326
            }
327
        }
328
329
        return filesize($filePath);
330
    }
331
332
    /**
333
     * Get upload path
334
     *
335
     * @param null|string $fileName
336
     * @param null|string $version
337
     *
338
     * @return string
339
     */
340
    protected function getUploadPath(?string $fileName = null, ?string $version = null)
341
    {
342
        $fileName = $fileName ? $fileName : '';
343
        if (empty($version)) {
344
            $versionPath = '';
345
        } else {
346
            $versionPath = $version . '/';
347
        }
348
349
        return $this->options['upload_dir'] . $versionPath . $fileName;
350
    }
351
352
    /**
353
     * Check if uploaded file executable to prevent php injections
354
     *
355
     * @param $file
356
     *
357
     * @throws Exception
358
     */
359
    protected function fileTypeExecutable($file)
360
    {
361
        if ($file && 'text/x-php' == mime_content_type($file)) {
362
            throw new Exception('File type was blocked', 403);
363
        }
364
    }
365
}
366