UploadedFile::save()   B
last analyzed

Complexity

Conditions 11
Paths 12

Size

Total Lines 39
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 19
c 0
b 0
f 0
nc 12
nop 2
dl 0
loc 39
rs 7.3166

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
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 3.0.0
13
 */
14
15
namespace Quantum\Libraries\Storage;
16
17
use Quantum\Libraries\Storage\Contracts\LocalFilesystemAdapterInterface;
18
use Quantum\Libraries\Storage\Contracts\FilesystemAdapterInterface;
19
use Quantum\Libraries\Storage\Exceptions\FileSystemException;
20
use Quantum\Libraries\Storage\Exceptions\FileUploadException;
21
use Quantum\Libraries\Lang\Exceptions\LangException;
22
use Quantum\Environment\Exceptions\EnvException;
23
use Quantum\Config\Exceptions\ConfigException;
24
use Quantum\App\Exceptions\BaseException;
25
use Quantum\Di\Exceptions\DiException;
26
use Gumlet\ImageResizeException;
27
use Quantum\Loader\Loader;
28
use Quantum\Loader\Setup;
29
use ReflectionException;
30
use Gumlet\ImageResize;
31
use Quantum\Di\Di;
32
use SplFileInfo;
33
use finfo;
34
35
/**
36
 * Class File
37
 * @package Quantum\Libraries\Storage
38
 */
39
class UploadedFile extends SplFileInfo
40
{
41
    /**
42
     * Local File System
43
     * @var LocalFilesystemAdapterInterface
44
     */
45
    protected $localFileSystem;
46
47
    /**
48
     * Remove File System
49
     * @var FilesystemAdapterInterface
50
     */
51
    protected $remoteFileSystem = null;
52
53
    /**
54
     * Original file name provided by client
55
     * @var string
56
     */
57
    protected $originalName = null;
58
59
    /**
60
     * File name (without extension)
61
     * @var string
62
     */
63
    protected $name = null;
64
65
    /**
66
     * File extension
67
     * @var string
68
     */
69
    protected $extension = null;
70
71
    /**
72
     * File mime type
73
     * @var string
74
     */
75
    protected $mimetype = null;
76
77
    /**
78
     * ImageResize function name
79
     * @var string
80
     */
81
    protected $imageModifierFuncName = null;
82
83
    /**
84
     * ImageResize function arguments
85
     * @var array
86
     */
87
    protected $params = [];
88
89
    /**
90
     * Upload error code messages
91
     * @var array
92
     */
93
    protected $errorMessages = [
94
        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
95
        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
96
        3 => 'The uploaded file was only partially uploaded',
97
        4 => 'No file was uploaded',
98
        6 => 'Missing a temporary folder',
99
        7 => 'Failed to write file to disk',
100
        8 => 'A PHP extension stopped the file upload',
101
    ];
102
103
    /**
104
     * Allowed mime types => allowed extensions map
105
     * @var array
106
     */
107
    protected $allowedMimeTypes = [
108
        'image/jpeg' => ['jpg', 'jpeg'],
109
        'image/png' => ['png'],
110
        'application/pdf' => ['pdf'],
111
    ];
112
113
    /**
114
     * Upload error code
115
     * @var int
116
     */
117
    protected $errorCode;
118
119
    /**
120
     * @param array $meta
121
     * @throws BaseException
122
     * @throws ConfigException
123
     * @throws DiException
124
     * @throws ReflectionException
125
     */
126
    public function __construct(array $meta)
127
    {
128
        $adapter = fs()->getAdapter();
129
130
        if (!$adapter instanceof LocalFilesystemAdapterInterface) {
131
            throw FileSystemException::notInstanceOf(
132
                get_class($adapter),
133
                LocalFilesystemAdapterInterface::class
134
            );
135
        }
136
137
        $this->localFileSystem = $adapter;
138
139
        $this->originalName = $meta['name'];
140
        $this->errorCode = $meta['error'];
141
142
        $this->loadAllowedMimeTypesFromConfig();
143
144
        parent::__construct($meta['tmp_name']);
145
    }
146
147
    /**
148
     * Sets the allowed mime types => extensions map
149
     * @param array $allowedMimeTypes
150
     * @param bool $merge
151
     * @return $this
152
     */
153
    public function setAllowedMimeTypes(array $allowedMimeTypes, bool $merge = true): UploadedFile
154
    {
155
        $this->setAllowedMimeTypesMap($allowedMimeTypes, $merge);
156
        return $this;
157
    }
158
159
    /**
160
     * Get name
161
     * @return string
162
     */
163
    public function getName(): string
164
    {
165
        if (!$this->name) {
166
            $this->name = $this->localFileSystem->fileName($this->originalName);
167
        }
168
169
        return $this->name;
170
    }
171
172
    /**
173
     * Set name (without extension)
174
     * @param string $name
175
     * @return $this
176
     */
177
    public function setName(string $name): UploadedFile
178
    {
179
        $this->name = $name;
180
        return $this;
181
    }
182
183
    /**
184
     * Sets the remote file system adapter
185
     * @param FilesystemAdapterInterface $remoteFileSystem
186
     * @return $this
187
     */
188
    public function setRemoteFileSystem(FilesystemAdapterInterface $remoteFileSystem): UploadedFile
189
    {
190
        $this->remoteFileSystem = $remoteFileSystem;
191
        return $this;
192
    }
193
194
    /**
195
     * Gets the remote file system adapter
196
     * @return FilesystemAdapterInterface|null
197
     */
198
    public function getRemoteFileSystem(): ?FilesystemAdapterInterface
199
    {
200
        return $this->remoteFileSystem;
201
    }
202
203
    /**
204
     * Get file extension (without leading dot)
205
     * @return string
206
     */
207
    public function getExtension(): string
208
    {
209
        if (!$this->extension) {
210
            $this->extension = strtolower($this->localFileSystem->extension($this->originalName));
211
        }
212
213
        return $this->extension;
214
    }
215
216
    /**
217
     * Get file name with extension
218
     * @return string
219
     */
220
    public function getNameWithExtension(): string
221
    {
222
        return $this->getName() . '.' . $this->getExtension();
223
    }
224
225
    /**
226
     * Get mime type
227
     * @return string
228
     */
229
    public function getMimeType(): string
230
    {
231
        if (!$this->mimetype) {
232
            $fileInfo = new finfo(FILEINFO_MIME);
233
            $mimetype = $fileInfo->file($this->getPathname());
234
            $mimetypeParts = preg_split('/\s*[;,]\s*/', $mimetype);
235
            $this->mimetype = strtolower($mimetypeParts[0]);
236
            unset($fileInfo);
237
        }
238
239
        return $this->mimetype;
240
    }
241
242
    /**
243
     * Get md5
244
     * @return string
245
     */
246
    public function getMd5(): string
247
    {
248
        return md5_file($this->getPathname());
249
    }
250
251
    /**
252
     * Get image dimensions
253
     * @return array
254
     * @throws FileUploadException
255
     */
256
    public function getDimensions(): array
257
    {
258
        if (!$this->isImage($this->getPathname())) {
259
            throw FileUploadException::fileTypeNotAllowed($this->getExtension());
260
        }
261
262
        [$width, $height] = getimagesize($this->getPathname());
263
264
        return [
265
            'width' => $width,
266
            'height' => $height,
267
        ];
268
    }
269
270
    /**
271
     * Save the uploaded file
272
     * @param string $dest
273
     * @param bool $overwrite
274
     * @return bool
275
     * @throws BaseException
276
     * @throws FileSystemException
277
     * @throws FileUploadException
278
     * @throws ImageResizeException
279
     * @throws EnvException
280
     */
281
    public function save(string $dest, bool $overwrite = false): bool
282
    {
283
        if ($this->errorCode !== UPLOAD_ERR_OK) {
284
            throw new FileUploadException($this->getErrorMessage());
285
        }
286
287
        if (!$this->localFileSystem->isFile($this->getPathname())) {
288
            throw FileUploadException::fileNotFound($this->getPathname());
289
        }
290
291
        if (!$this->allowed($this->getExtension(), $this->getMimeType())) {
292
            throw FileUploadException::fileTypeNotAllowed($this->getExtension());
293
        }
294
295
        $filePath = $dest . DS . $this->getNameWithExtension();
296
297
        if (!$this->remoteFileSystem) {
298
            if (!$this->localFileSystem->isDirectory($dest)) {
299
                throw FileSystemException::directoryNotExists($dest);
300
            }
301
302
            if (!$this->localFileSystem->isWritable($dest)) {
303
                throw FileSystemException::directoryNotWritable($dest);
304
            }
305
306
            if ($overwrite === false && $this->localFileSystem->exists($filePath)) {
307
                throw FileSystemException::fileAlreadyExists($filePath);
308
            }
309
        }
310
311
        if (!$this->moveUploadedFile($filePath)) {
312
            return false;
313
        }
314
315
        if ($this->imageModifierFuncName) {
316
            $this->applyModifications($filePath);
317
        }
318
319
        return true;
320
    }
321
322
    /**
323
     * Sets modification function on image
324
     * @param string $funcName
325
     * @param array $params
326
     * @return $this
327
     * @throws BaseException
328
     * @throws FileUploadException
329
     * @throws LangException
330
     */
331
    public function modify(string $funcName, array $params): UploadedFile
332
    {
333
        if (!$this->isImage($this->getPathname())) {
334
            throw FileUploadException::fileTypeNotAllowed($this->getExtension());
335
        }
336
337
        if (!method_exists(ImageResize::class, $funcName)) {
338
            throw BaseException::methodNotSupported($funcName, ImageResize::class);
339
        }
340
341
        $this->imageModifierFuncName = $funcName;
342
        $this->params = $params;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Gets the error code
349
     * @return int
350
     */
351
    public function getErrorCode(): int
352
    {
353
        return $this->errorCode;
354
    }
355
356
    /**
357
     * Gets the error message from code
358
     * @return string
359
     */
360
    public function getErrorMessage(): string
361
    {
362
        return $this->errorMessages[$this->errorCode];
363
    }
364
365
    /**
366
     * Tells whether the file was uploaded
367
     * @return bool
368
     */
369
    public function isUploaded(): bool
370
    {
371
        return is_uploaded_file($this->getPathname());
372
    }
373
374
    /**
375
     * Checks if the given file is image
376
     * @param $filePath
377
     * @return bool
378
     */
379
    public function isImage($filePath): bool
380
    {
381
        return (bool) getimagesize($filePath);
382
    }
383
384
    /**
385
     * Moves an uploaded file to a new location
386
     * @param string $filePath
387
     * @return bool
388
     */
389
    protected function moveUploadedFile(string $filePath): bool
390
    {
391
        if ($this->remoteFileSystem) {
392
            return (bool)$this->remoteFileSystem->put($filePath, $this->localFileSystem->get($this->getPathname()));
0 ignored issues
show
Bug introduced by
It seems like $this->localFileSystem->get($this->getPathname()) can also be of type false; however, parameter $content of Quantum\Libraries\Storag...AdapterInterface::put() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

392
            return (bool)$this->remoteFileSystem->put($filePath, /** @scrutinizer ignore-type */ $this->localFileSystem->get($this->getPathname()));
Loading history...
393
        } elseif ($this->isUploaded()) {
394
            return move_uploaded_file($this->getPathname(), $filePath);
395
        } else {
396
            return $this->localFileSystem->copy($this->getPathname(), $filePath);
397
        }
398
    }
399
400
    /**
401
     * Validates upload against allowed mime types => extensions map
402
     * @param string $extension
403
     * @param string $mimeType
404
     * @return bool
405
     */
406
    protected function allowed(string $extension, string $mimeType): bool
407
    {
408
        $extension = strtolower($extension);
409
        $mimeType = strtolower($mimeType);
410
411
        return isset($this->allowedMimeTypes[$mimeType]) &&
412
            in_array($extension, $this->allowedMimeTypes[$mimeType], true);
413
    }
414
415
    /**
416
     * Loads allowed mime types from config (shared/config/uploads.php) if present.
417
     * @throws ConfigException
418
     * @throws DiException
419
     * @throws FileUploadException
420
     * @throws ReflectionException
421
     */
422
    protected function loadAllowedMimeTypesFromConfig(): void
423
    {
424
        if (!config()->has('uploads')) {
425
            $loader = Di::get(Loader::class);
426
            $setup = new Setup('config', 'uploads');
427
            $loader->setup($setup);
428
429
            if (!$loader->fileExists()) {
430
                return;
431
            }
432
433
            config()->import($setup);
434
        }
435
436
        $allowedMimeTypesMap = config()->get('uploads.allowed_mime_types') ?? [];
437
438
        if (!is_array($allowedMimeTypesMap)) {
439
            throw FileUploadException::incorrectMimeTypesConfig('uploads');
440
        }
441
442
        if ($allowedMimeTypesMap !== []) {
443
            $this->setAllowedMimeTypesMap($allowedMimeTypesMap);
444
        }
445
    }
446
447
    /**
448
     * Sets the allowed mime types => extensions map
449
     * @param array $allowedMimeTypes
450
     * @param bool $merge
451
     * @return void
452
     */
453
    protected function setAllowedMimeTypesMap(array $allowedMimeTypes, bool $merge = true): void
454
    {
455
        $this->allowedMimeTypes = $merge ? array_merge_recursive($this->allowedMimeTypes, $allowedMimeTypes) : $allowedMimeTypes;
456
    }
457
458
    /**
459
     * Applies modifications on image
460
     * @param $filePath
461
     * @throws ImageResizeException
462
     */
463
    protected function applyModifications($filePath)
464
    {
465
        $image = new ImageResize($filePath);
466
        call_user_func_array([$image, $this->imageModifierFuncName], $this->params);
467
468
        $image->save($filePath);
469
    }
470
}
471