Passed
Pull Request — master (#361)
by
unknown
03:22
created

UploadedFile::allowed()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 2.9.8
13
 */
14
15
namespace Quantum\Libraries\Storage;
16
17
use Quantum\Libraries\Storage\Contracts\FilesystemAdapterInterface;
18
use Quantum\Libraries\Storage\Exceptions\FileSystemException;
19
use Quantum\Libraries\Storage\Exceptions\FileUploadException;
20
use Quantum\Libraries\Storage\Factories\FileSystemFactory;
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 Quantum\Loader\Setup;
27
use Gumlet\ImageResizeException;
28
use ReflectionException;
29
use Gumlet\ImageResize;
30
use SplFileInfo;
31
use finfo;
32
33
/**
34
 * Class File
35
 * @package Quantum\Libraries\Storage
36
 */
37
class UploadedFile extends SplFileInfo
38
{
39
40
    /**
41
     * Local File System
42
     * @var FilesystemAdapterInterface
43
     */
44
    protected $localFileSystem;
45
46
    /**
47
     * Remove File System
48
     * @var FilesystemAdapterInterface
49
     */
50
    protected $remoteFileSystem = null;
51
52
    /**
53
     * Original file name provided by client
54
     * @var string
55
     */
56
    protected $originalName = null;
57
58
    /**
59
     * File name (without extension)
60
     * @var string
61
     */
62
    protected $name = null;
63
64
    /**
65
     * File extension
66
     * @var string
67
     */
68
    protected $extension = null;
69
70
    /**
71
     * File mime type
72
     * @var string
73
     */
74
    protected $mimetype = null;
75
76
    /**
77
     * ImageResize function name
78
     * @var string
79
     */
80
    protected $imageModifierFuncName = null;
81
82
    /**
83
     * ImageResize function arguments
84
     * @var array
85
     */
86
    protected $params = [];
87
88
    /**
89
     * Upload error code messages
90
     * @var array
91
     */
92
    protected $errorMessages = [
93
        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
94
        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
95
        3 => 'The uploaded file was only partially uploaded',
96
        4 => 'No file was uploaded',
97
        6 => 'Missing a temporary folder',
98
        7 => 'Failed to write file to disk',
99
        8 => 'A PHP extension stopped the file upload'
100
    ];
101
102
    /**
103
     * Allowed mime types => allowed extensions map
104
     * @var array
105
     */
106
    protected $allowedMimeTypes = [
107
        'image/jpeg' => ['jpg', 'jpeg'],
108
        'image/png' => ['png'],
109
        'application/pdf' => ['pdf'],
110
    ];
111
112
    /**
113
     * Upload error code
114
     * @var int
115
     */
116
    protected $errorCode;
117
118
    /**
119
     * @param array $meta
120
     * @throws BaseException
121
     * @throws ConfigException
122
     * @throws DiException
123
     * @throws ReflectionException
124
     */
125
    public function __construct(array $meta)
126
    {
127
        $this->localFileSystem = FileSystemFactory::get();
0 ignored issues
show
Documentation Bug introduced by
It seems like Quantum\Libraries\Storag...ileSystemFactory::get() of type Quantum\Libraries\Storage\FileSystem is incompatible with the declared type Quantum\Libraries\Storag...esystemAdapterInterface of property $localFileSystem.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
128
129
        $this->originalName = $meta['name'];
130
        $this->errorCode = $meta['error'];
131
132
        $this->loadAllowedMimeTypesFromConfig();
133
134
        parent::__construct($meta['tmp_name']);
135
    }
136
137
    /**
138
     * Overrides the default allowed mime types => extensions map
139
     * @param array $allowedMimeTypes
140
     * @return $this
141
     */
142
    public function setAllowedMimeTypes(array $allowedMimeTypes): UploadedFile
143
    {
144
        $this->allowedMimeTypes = $allowedMimeTypes;
145
        return $this;
146
    }
147
148
    /**
149
     * Get name
150
     * @return string
151
     */
152
    public function getName(): string
153
    {
154
        if (!$this->name) {
155
            $this->name = $this->localFileSystem->fileName($this->originalName);
0 ignored issues
show
Bug introduced by
The method fileName() does not exist on Quantum\Libraries\Storag...esystemAdapterInterface. It seems like you code against a sub-type of Quantum\Libraries\Storag...esystemAdapterInterface such as Quantum\Libraries\Storag...\LocalFileSystemAdapter. ( Ignorable by Annotation )

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

155
            /** @scrutinizer ignore-call */ 
156
            $this->name = $this->localFileSystem->fileName($this->originalName);
Loading history...
156
        }
157
158
        return $this->name;
159
    }
160
161
    /**
162
     * Set name (without extension)
163
     * @param string $name
164
     * @return $this
165
     */
166
    public function setName(string $name): UploadedFile
167
    {
168
        $this->name = $name;
169
        return $this;
170
    }
171
172
    /**
173
     * Sets the remote file system adapter
174
     * @param FilesystemAdapterInterface $remoteFileSystem
175
     * @return $this
176
     */
177
    public function setRemoteFileSystem(FilesystemAdapterInterface $remoteFileSystem): UploadedFile
178
    {
179
        $this->remoteFileSystem = $remoteFileSystem;
180
        return $this;
181
    }
182
183
    /**
184
     * Gets the remote file system adapter
185
     * @return FilesystemAdapterInterface|null
186
     */
187
    public function getRemoteFileSystem(): ?FilesystemAdapterInterface
188
    {
189
        return $this->remoteFileSystem;
190
    }
191
192
    /**
193
     * Get file extension (without leading dot)
194
     * @return string
195
     */
196
    public function getExtension(): string
197
    {
198
        if (!$this->extension) {
199
            $this->extension = strtolower($this->localFileSystem->extension($this->originalName));
0 ignored issues
show
Bug introduced by
The method extension() does not exist on Quantum\Libraries\Storag...esystemAdapterInterface. It seems like you code against a sub-type of Quantum\Libraries\Storag...esystemAdapterInterface such as Quantum\Libraries\Storag...\LocalFileSystemAdapter. ( Ignorable by Annotation )

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

199
            $this->extension = strtolower($this->localFileSystem->/** @scrutinizer ignore-call */ extension($this->originalName));
Loading history...
200
        }
201
202
        return $this->extension;
203
    }
204
205
    /**
206
     * Get file name with extension
207
     * @return string
208
     */
209
    public function getNameWithExtension(): string
210
    {
211
        return $this->getName() . '.' . $this->getExtension();
212
    }
213
214
    /**
215
     * Get mime type
216
     * @return string
217
     */
218
    public function getMimeType(): string
219
    {
220
        if (!$this->mimetype) {
221
            $fileInfo = new finfo(FILEINFO_MIME);
222
            $mimetype = $fileInfo->file($this->getPathname());
223
            $mimetypeParts = preg_split('/\s*[;,]\s*/', $mimetype);
224
            $this->mimetype = strtolower($mimetypeParts[0]);
225
            unset($fileInfo);
226
        }
227
228
        return $this->mimetype;
229
    }
230
231
    /**
232
     * Get md5
233
     * @return string
234
     */
235
    public function getMd5(): string
236
    {
237
        return md5_file($this->getPathname());
238
    }
239
240
    /**
241
     * Get image dimensions
242
     * @return array
243
     * @throws FileUploadException
244
     */
245
    public function getDimensions(): array
246
    {
247
        if (!$this->isImage($this->getPathname())) {
248
            throw FileUploadException::fileTypeNotAllowed($this->getExtension());
249
        }
250
251
        list($width, $height) = getimagesize($this->getPathname());
252
253
        return [
254
            'width' => $width,
255
            'height' => $height
256
        ];
257
    }
258
259
    /**
260
     * Save the uploaded file
261
     * @param string $dest
262
     * @param bool $overwrite
263
     * @return bool
264
     * @throws BaseException
265
     * @throws FileSystemException
266
     * @throws FileUploadException
267
     * @throws ImageResizeException
268
     * @throws EnvException
269
     */
270
    public function save(string $dest, bool $overwrite = false): bool
271
    {
272
        if ($this->errorCode !== UPLOAD_ERR_OK) {
273
            throw new FileUploadException($this->getErrorMessage());
274
        }
275
276
        if (!$this->localFileSystem->isFile($this->getPathname())) {
277
            throw FileUploadException::fileNotFound($this->getPathname());
278
        }
279
280
        if (!$this->allowed($this->getExtension(), $this->getMimeType())) {
281
            throw FileUploadException::fileTypeNotAllowed($this->getExtension());
282
        }
283
284
        $filePath = $dest . DS . $this->getNameWithExtension();
285
286
        if (!$this->remoteFileSystem) {
287
            if (!$this->localFileSystem->isDirectory($dest)) {
288
                throw FileSystemException::directoryNotExists($dest);
289
            }
290
291
            if (!$this->localFileSystem->isWritable($dest)) {
0 ignored issues
show
Bug introduced by
The method isWritable() does not exist on Quantum\Libraries\Storag...esystemAdapterInterface. It seems like you code against a sub-type of Quantum\Libraries\Storag...esystemAdapterInterface such as Quantum\Libraries\Storag...\LocalFileSystemAdapter. ( Ignorable by Annotation )

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

291
            if (!$this->localFileSystem->/** @scrutinizer ignore-call */ isWritable($dest)) {
Loading history...
292
                throw FileSystemException::directoryNotWritable($dest);
293
            }
294
295
            if ($overwrite === false && $this->localFileSystem->exists($filePath)) {
296
                throw FileSystemException::fileAlreadyExists($filePath);
297
            }
298
        }
299
300
        if (!$this->moveUploadedFile($filePath)) {
301
            return false;
302
        }
303
304
        if ($this->imageModifierFuncName) {
305
            $this->applyModifications($filePath);
306
        }
307
308
        return true;
309
    }
310
311
    /**
312
     * Sets modification function on image
313
     * @param string $funcName
314
     * @param array $params
315
     * @return $this
316
     * @throws BaseException
317
     * @throws FileUploadException
318
     * @throws LangException
319
     */
320
    public function modify(string $funcName, array $params): UploadedFile
321
    {
322
        if (!$this->isImage($this->getPathname())) {
323
            throw FileUploadException::fileTypeNotAllowed($this->getExtension());
324
        }
325
326
        if (!method_exists(ImageResize::class, $funcName)) {
327
            throw BaseException::methodNotSupported($funcName, ImageResize::class);
328
        }
329
330
        $this->imageModifierFuncName = $funcName;
331
        $this->params = $params;
332
333
        return $this;
334
    }
335
336
    /**
337
     * Gets the error code
338
     * @return int
339
     */
340
    public function getErrorCode(): int
341
    {
342
        return $this->errorCode;
343
    }
344
345
    /**
346
     * Gets the error message from code
347
     * @return string
348
     */
349
    public function getErrorMessage(): string
350
    {
351
        return $this->errorMessages[$this->errorCode];
352
    }
353
354
    /**
355
     * Tells whether the file was uploaded
356
     * @return bool
357
     */
358
    public function isUploaded(): bool
359
    {
360
        return is_uploaded_file($this->getPathname());
361
    }
362
363
    /**
364
     * Checks if the given file is image
365
     * @param $filePath
366
     * @return bool
367
     */
368
    public function isImage($filePath): bool
369
    {
370
        return !!getimagesize($filePath);
371
    }
372
373
    /**
374
     * Moves an uploaded file to a new location
375
     * @param string $filePath
376
     * @return bool
377
     */
378
    protected function moveUploadedFile(string $filePath): bool
379
    {
380
        if ($this->remoteFileSystem) {
381
            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

381
            return (bool)$this->remoteFileSystem->put($filePath, /** @scrutinizer ignore-type */ $this->localFileSystem->get($this->getPathname()));
Loading history...
382
        } else {
383
            if ($this->isUploaded()) {
384
                return move_uploaded_file($this->getPathname(), $filePath);
385
            } else {
386
                return $this->localFileSystem->copy($this->getPathname(), $filePath);
387
            }
388
        }
389
    }
390
391
    /**
392
     * Validates upload against allowed mime types => extensions map
393
     * @param string $extension
394
     * @param string $mimeType
395
     * @return bool
396
     */
397
    protected function allowed(string $extension, string $mimeType): bool
398
    {
399
        $extension = strtolower($extension);
400
        $mimeType = strtolower($mimeType);
401
402
        return isset($this->allowedMimeTypes[$mimeType]) &&
403
            in_array($extension, $this->allowedMimeTypes[$mimeType], true);
404
    }
405
406
    /**
407
     * Loads allowed mime types from config (shared/config/uploads.php) if present.
408
	 * @return void
409
     */
410
    protected function loadAllowedMimeTypesFromConfig(): void
411
    {
412
		if (!config()->has('uploads')) {
413
			config()->import(new Setup('config', 'uploads'));
414
		}
415
416
        $allowedMimeTypesMap = config()->get('uploads.allowed_mime_types');
417
418
        if (is_array($allowedMimeTypesMap)) {
419
            $this->allowedMimeTypes = $allowedMimeTypesMap;
420
        }
421
    }
422
423
    /**
424
     * Applies modifications on image
425
     * @param $filePath
426
     * @throws ImageResizeException
427
     */
428
    protected function applyModifications($filePath)
429
    {
430
        $image = new ImageResize($filePath);
431
        call_user_func_array([$image, $this->imageModifierFuncName], $this->params);
432
433
        $image->save($filePath);
434
    }
435
}