Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.49% covered (success)
79.49%
93 / 117
90.32% covered (success)
90.32%
28 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Upload
79.49% covered (success)
79.49%
93 / 117
90.32% covered (success)
90.32%
28 / 31
123.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkDuplicate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doesFileExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefaults
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 setUploadDir
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 setMaxSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAllowedTypes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setDisallowedTypes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addAllowedType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addDisallowedType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeAllowedType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeDisallowedType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 overwrite
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUploadDir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUploadedFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUploadedFullPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDisallowedTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAllowed
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 isNotAllowed
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 isOverwrite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSuccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrorCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrorMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fileExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkFilename
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 test
75.00% covered (success)
75.00%
15 / 20
0.00% covered (danger)
0.00%
0 / 1
15.64
 upload
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Http\Server;
15
16/**
17 * HTTP server upload class
18 *
19 * @category   Pop
20 * @package    Pop\Http
21 * @author     Nick Sagona, III <dev@nolainteractive.com>
22 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
23 * @license    http://www.popphp.org/license     New BSD License
24 * @version    5.0.0
25 */
26class Upload
27{
28
29    /**
30     * File is too big by the user-defined max size
31     */
32    const UPLOAD_ERR_USER_SIZE = 9;
33
34    /**
35     * File is not allowed, per user-definition
36     */
37    const UPLOAD_ERR_NOT_ALLOWED = 10;
38
39    /**
40     * Upload directory does not exist
41     */
42    const UPLOAD_ERR_DIR_NOT_EXIST = 11;
43
44    /**
45     * Upload directory not writable
46     */
47    const UPLOAD_ERR_DIR_NOT_WRITABLE = 12;
48
49    /**
50     * File security error
51     */
52    const UPLOAD_ERR_FILE_NOT_SECURE = 13;
53
54    /**
55     * Unexpected error
56     */
57    const UPLOAD_ERR_UNEXPECTED = 14;
58
59    /**
60     * Error messageed
61     * @var array
62     */
63    protected static array $errorMessages = [
64         0 => 'The file uploaded successfully',
65         1 => 'The uploaded file exceeds the upload_max_filesize directive',
66         2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form',
67         3 => 'The uploaded file was only partially uploaded',
68         4 => 'No file was uploaded',
69         6 => 'Missing a temporary folder',
70         7 => 'Failed to write file to disk',
71         8 => 'A PHP extension stopped the file upload',
72         9 => 'The uploaded file exceeds the user-defined max file size',
73        10 => 'The uploaded file is not allowed',
74        11 => 'The specified upload directory does not exist',
75        12 => 'The specified upload directory is not writable',
76        13 => 'The uploaded file was not uploaded securely',
77        14 => 'Unexpected error'
78    ];
79
80    /**
81     * The upload directory path
82     * @var ?string
83     */
84    protected ?string $uploadDir = null;
85
86    /**
87     * The final filename of the uploaded file
88     * @var ?string
89     */
90    protected ?string $uploadedFile = null;
91
92    /**
93     * Allowed maximum file size
94     * @var int
95     */
96    protected int $maxSize = 0;
97
98    /**
99     * Allowed file types
100     * @var array
101     */
102    protected array $allowedTypes = [];
103
104    /**
105     * Disallowed file types
106     * @var array
107     */
108    protected array $disallowedTypes = [];
109
110    /**
111     * Overwrite flag
112     * @var bool
113     */
114    protected bool $overwrite = false;
115
116    /**
117     * Error flag
118     * @var int
119     */
120    protected int $error = 0;
121
122    /**
123     * Constructor
124     *
125     * Instantiate a file upload object
126     *
127     * @param  string $dir
128     * @param  int    $maxSize
129     * @param  ?array $disallowedTypes
130     * @param  ?array $allowedTypes
131     */
132    public function __construct(string $dir, int $maxSize = 0, ?array $disallowedTypes = null, ?array $allowedTypes = null)
133    {
134        $this->setUploadDir($dir);
135        $this->setMaxSize($maxSize);
136
137        if (($disallowedTypes !== null) && (count($disallowedTypes) > 0)) {
138            $this->setDisallowedTypes($disallowedTypes);
139        }
140        if (($allowedTypes !== null) && (count($allowedTypes) > 0)) {
141            $this->setAllowedTypes($allowedTypes);
142        }
143    }
144
145    /**
146     * Create an upload object
147     *
148     * @param  string $dir
149     * @param  int    $maxSize
150     * @param  ?array $disallowedTypes
151     * @param  ?array $allowedTypes
152     * @return Upload
153     */
154    public static function create(string $dir, int $maxSize = 0, ?array $disallowedTypes = null, ?array $allowedTypes = null): Upload
155    {
156        return new static($dir, $maxSize, $disallowedTypes, $allowedTypes);
157    }
158
159    /**
160     * Check for a duplicate filename in the upload directory, and return a modified filename if it exists already
161     *
162     * @param  string $dir
163     * @param  string $file
164     * @return string
165     */
166    public static function checkDuplicate(string $dir, string $file): string
167    {
168        return (new static($dir))->checkFilename($file);
169    }
170
171    /**
172     * Check if the file exists already in the upload directory
173     *
174     * @param  string $dir
175     * @param  string $file
176     * @return bool
177     */
178    public static function doesFileExists(string $dir, string $file): bool
179    {
180        return (new static($dir))->fileExists($file);
181    }
182
183    /**
184     * Set default file upload settings
185     *
186     * @return Upload
187     */
188    public function setDefaults(): Upload
189    {
190        // Allow basic text, graphic, audio/video, data and archive file types
191        $allowedTypes = [
192            'ai', 'aif', 'aiff', 'avi', 'bmp', 'bz2', 'csv', 'doc', 'docx', 'eps', 'fla', 'flv', 'gif', 'gz',
193            'jpe','jpg', 'jpeg', 'log', 'md', 'mov', 'mp2', 'mp3', 'mp4', 'mpg', 'mpeg', 'otf', 'pdf',
194            'png', 'ppt', 'pptx', 'psd', 'rar', 'svg', 'swf', 'tar', 'tbz', 'tbz2', 'tgz', 'tif', 'tiff', 'tsv',
195            'ttf', 'txt', 'wav', 'wma', 'wmv', 'xls', 'xlsx', 'xml', 'zip'
196        ];
197
198        // Disallow programming/development file types
199        $disallowedTypes = [
200            'css', 'htm', 'html', 'js', 'json', 'pgsql', 'php', 'php3', 'php4', 'php5', 'sql', 'sqlite', 'yaml', 'yml'
201        ];
202
203        // Set max file size to 10 MBs
204        $this->setMaxSize(10000000);
205        $this->setAllowedTypes($allowedTypes);
206        $this->setDisallowedTypes($disallowedTypes);
207
208        return $this;
209    }
210
211    /**
212     * Set the upload directory
213     *
214     * @param  string $dir
215     * @return Upload
216     */
217    public function setUploadDir(string $dir): Upload
218    {
219        // Check to see if the upload directory exists.
220        if (!file_exists($dir) || !is_dir($dir)) {
221            $this->error = self::UPLOAD_ERR_DIR_NOT_EXIST;
222        // Check to see if the permissions are set correctly.
223        } else if (!is_writable($dir)) {
224            $this->error = self::UPLOAD_ERR_DIR_NOT_WRITABLE;
225        }
226
227        $this->uploadDir = $dir;
228        return $this;
229    }
230
231    /**
232     * Set the upload directory
233     *
234     * @param  int $maxSize
235     * @return Upload
236     */
237    public function setMaxSize(int $maxSize): Upload
238    {
239        $this->maxSize = (int)$maxSize;
240        return $this;
241    }
242
243    /**
244     * Set the allowed types
245     *
246     * @param  array $allowedTypes
247     * @return Upload
248     */
249    public function setAllowedTypes(array $allowedTypes): Upload
250    {
251        foreach ($allowedTypes as $type) {
252            $this->addAllowedType($type);
253        }
254        return $this;
255    }
256
257    /**
258     * Set the disallowed types
259     *
260     * @param  array $disallowedTypes
261     * @return Upload
262     */
263    public function setDisallowedTypes(array $disallowedTypes): Upload
264    {
265        foreach ($disallowedTypes as $type) {
266            $this->addDisallowedType($type);
267        }
268        return $this;
269    }
270
271    /**
272     * Add an allowed type
273     *
274     * @param  string $type
275     * @return Upload
276     */
277    public function addAllowedType(string $type): Upload
278    {
279        if (!in_array(strtolower($type), $this->allowedTypes)) {
280            $this->allowedTypes[] = strtolower($type);
281        }
282        return $this;
283    }
284
285    /**
286     * Add a disallowed type
287     *
288     * @param  string $type
289     * @return Upload
290     */
291    public function addDisallowedType(string $type): Upload
292    {
293        if (!in_array(strtolower($type), $this->disallowedTypes)) {
294            $this->disallowedTypes[] = strtolower($type);
295        }
296        return $this;
297    }
298
299    /**
300     * Remove an allowed type
301     *
302     * @param  string $type
303     * @return Upload
304     */
305    public function removeAllowedType(string $type): Upload
306    {
307        if (in_array(strtolower($type), $this->allowedTypes)) {
308            unset($this->allowedTypes[array_search(strtolower($type), $this->allowedTypes)]);
309        }
310        return $this;
311    }
312
313    /**
314     * Remove a disallowed type
315     *
316     * @param  string $type
317     * @return Upload
318     */
319    public function removeDisallowedType(string $type): Upload
320    {
321        if (in_array(strtolower($type), $this->disallowedTypes)) {
322            unset($this->disallowedTypes[array_search(strtolower($type), $this->disallowedTypes)]);
323        }
324        return $this;
325    }
326
327    /**
328     * Set the overwrite flag
329     *
330     * @param  bool $overwrite
331     * @return Upload
332     */
333    public function overwrite(bool $overwrite): Upload
334    {
335        $this->overwrite = (bool)$overwrite;
336        return $this;
337    }
338
339    /**
340     * Get the upload directory
341     *
342     * @return string
343     */
344    public function getUploadDir(): string
345    {
346        return $this->uploadDir;
347    }
348
349    /**
350     * Get uploaded file
351     *
352     * @return string|null
353     */
354    public function getUploadedFile(): string|null
355    {
356        return $this->uploadedFile;
357    }
358
359    /**
360     * Get uploaded file full path
361     *
362     * @return string
363     */
364    public function getUploadedFullPath(): string
365    {
366        return $this->uploadDir . DIRECTORY_SEPARATOR . $this->uploadedFile;
367    }
368
369    /**
370     * Get the max size allowed
371     *
372     * @return int
373     */
374    public function getMaxSize(): int
375    {
376        return $this->maxSize;
377    }
378
379    /**
380     * Get the disallowed file types
381     *
382     * @return array
383     */
384    public function getDisallowedTypes(): array
385    {
386        return $this->disallowedTypes;
387    }
388
389    /**
390     * Get the allowed file types
391     *
392     * @return array
393     */
394    public function getAllowedTypes(): array
395    {
396        return $this->allowedTypes;
397    }
398
399    /**
400     * Determine if a file type is allowed
401     *
402     * @param  string $ext
403     * @return bool
404     */
405    public function isAllowed(string $ext): bool
406    {
407        $disallowed = ((count($this->disallowedTypes) > 0) && (in_array(strtolower($ext), $this->disallowedTypes)));
408        $allowed    = ((count($this->allowedTypes) == 0) ||
409            ((count($this->allowedTypes) > 0) && (in_array(strtolower($ext), $this->allowedTypes))));
410
411        return ((!$disallowed) && ($allowed));
412    }
413
414    /**
415     * Determine if a file type is not allowed
416     *
417     * @param  string $ext
418     * @return bool
419     */
420    public function isNotAllowed(string $ext): bool
421    {
422        $disallowed = ((count($this->disallowedTypes) > 0) && (in_array(strtolower($ext), $this->disallowedTypes)));
423        $allowed    = ((count($this->allowedTypes) == 0) ||
424            ((count($this->allowedTypes) > 0) && (in_array(strtolower($ext), $this->allowedTypes))));
425
426        return (($disallowed) && (!$allowed));
427    }
428
429    /**
430     * Determine if the overwrite flag is set
431     *
432     * @return bool
433     */
434    public function isOverwrite(): bool
435    {
436        return $this->overwrite;
437    }
438
439    /**
440     * Determine if the upload was a success
441     *
442     * @return bool
443     */
444    public function isSuccess(): bool
445    {
446        return ($this->error == UPLOAD_ERR_OK);
447    }
448
449    /**
450     * Determine if the upload was an error
451     *
452     * @return bool
453     */
454    public function isError(): bool
455    {
456        return ($this->error != UPLOAD_ERR_OK);
457    }
458
459    /**
460     * Get the upload error code
461     *
462     * @return int
463     */
464    public function getErrorCode(): int
465    {
466        return $this->error;
467    }
468
469    /**
470     * Get the upload error message
471     *
472     * @return string
473     */
474    public function getErrorMessage(): string
475    {
476        return self::$errorMessages[$this->error];
477    }
478
479    /**
480     * Check if filename exists in the upload directory
481     *
482     * @param  string $file
483     * @return bool
484     */
485    public function fileExists(string $file): bool
486    {
487        return (file_exists($this->uploadDir . DIRECTORY_SEPARATOR . $file));
488    }
489
490    /**
491     * Check filename for duplicates, returning a new filename appended with _#
492     *
493     * @param  string $file
494     * @return string
495     */
496    public function checkFilename(string $file): string
497    {
498        $newFilename  = $file;
499        $parts        = pathinfo($file);
500        $origFilename = $parts['filename'];
501        $ext          = (isset($parts['extension']) && ($parts['extension'] != '')) ? '.' . $parts['extension'] : null;
502        $i            = 1;
503
504        while ($this->fileExists($newFilename)) {
505            $newFilename = $origFilename . '_' . $i . $ext;
506            $i++;
507        }
508
509        return $newFilename;
510    }
511
512    /**
513     * Test a file upload before moving it
514     *
515     * @param  array $file
516     * @return bool
517     */
518    public function test(array $file): bool
519    {
520        if ($this->error != 0) {
521            return false;
522        } else {
523            if (!isset($file['error']) || !isset($file['size']) || !isset($file['tmp_name']) || !isset($file['name'])) {
524                return false;
525            } else {
526                $this->error = $file['error'];
527                if ($this->error != 0) {
528                    return false;
529                } else {
530                    $fileSize  = $file['size'];
531                    $fileParts = pathinfo($file['name']);
532                    $ext       = (isset($fileParts['extension'])) ? $fileParts['extension'] : null;
533
534                    if (($this->maxSize > 0) && ($fileSize > $this->maxSize)) {
535                        $this->error = self::UPLOAD_ERR_USER_SIZE;
536                        return false;
537                    } else if (($ext !== null) && (!$this->isAllowed($ext))) {
538                        $this->error = self::UPLOAD_ERR_NOT_ALLOWED;
539                        return false;
540                    } else if ($this->error == 0) {
541                        return true;
542                    } else {
543                        $this->error = self::UPLOAD_ERR_UNEXPECTED;
544                        return false;
545                    }
546                }
547            }
548        }
549    }
550
551    /**
552     * Upload file to the upload dir, returns the newly uploaded file
553     *
554     * @param  array   $file
555     * @param  ?string $to
556     * @param  bool    $secure
557     * @return mixed
558     */
559    public function upload(array $file, ?string $to = null, bool $secure = true): mixed
560    {
561        if ($this->test($file)) {
562            if ($to === null) {
563                $to = $file['name'];
564            }
565            if (!$this->overwrite) {
566                $to = $this->checkFilename($to);
567            }
568
569            $this->uploadedFile = $to;
570            $to = $this->uploadDir . DIRECTORY_SEPARATOR . $to;
571
572            $isUploaded = is_uploaded_file($file['tmp_name']);
573
574            // Move the uploaded file, creating a file object with it.
575            if (($secure) && !($isUploaded)) {
576                $this->error = self::UPLOAD_ERR_FILE_NOT_SECURE;
577                return false;
578            } else {
579                $result = ((!$secure) && !($isUploaded)) ?
580                    rename($file['tmp_name'], $to) : move_uploaded_file($file['tmp_name'], $to);
581
582                if ($result) {
583                    return $this->uploadedFile;
584                } else {
585                    $this->error = self::UPLOAD_ERR_UNEXPECTED;
586                    return false;
587                }
588            }
589        } else {
590            return false;
591        }
592
593    }
594
595}