Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
n/a
0 / 0
n/a
0 / 0
CRAP
n/a
0 / 0
UploadFile
n/a
0 / 0
n/a
0 / 0
45
n/a
0 / 0
 upload
n/a
0 / 0
n/a
0 / 0
28
 createFileName
n/a
0 / 0
n/a
0 / 0
8
 findOutputDir
n/a
0 / 0
n/a
0 / 0
1
 getUploadError
n/a
0 / 0
n/a
0 / 0
5
 getAllowedTypes
n/a
0 / 0
n/a
0 / 0
1
 setAllowedTypes
n/a
0 / 0
n/a
0 / 0
1
 addAllowedTypes
n/a
0 / 0
n/a
0 / 0
1
1<?php
2
3/**
4 * Jingga
5 *
6 * PHP Version 8.1
7 *
8 * @package   Modules\Media\Models
9 * @copyright Dennis Eichhorn
10 * @license   OMS License 2.0
11 * @version   1.0.0
12 * @link      https://jingga.app
13 */
14declare(strict_types=1);
15
16namespace Modules\Media\Models;
17
18use phpOMS\Log\FileLogger;
19use phpOMS\Security\EncryptionHelper;
20use phpOMS\System\File\Local\Directory;
21use phpOMS\System\File\Local\File;
22
23/**
24 * Upload.
25 *
26 * @package Modules\Media\Models
27 * @license OMS License 2.0
28 * @link    https://jingga.app
29 * @since   1.0.0
30 * @codeCoverageIgnore
31 */
32class UploadFile
33{
34    /**
35     * Limit of iterations to find a possible random path for the file to upload to.
36     *
37     * @var int
38     * @since 1.0.0
39     */
40    private const PATH_GENERATION_LIMIT = 1000;
41
42    /**
43     * Image interlaced.
44     *
45     * @var bool
46     * @since 1.0.0
47     */
48    public bool $isInterlaced = true;
49
50    /**
51     * Upload max size.
52     *
53     * @var int
54     * @since 1.0.0
55     */
56    public int $maxSize = 50000000;
57
58    /**
59     * Allowed mime types.
60     *
61     * @var string[]
62     * @since 1.0.0
63     */
64    private $allowedTypes = [];
65
66    /**
67     * Output directory.
68     *
69     * @var string
70     * @since 1.0.0
71     */
72    public string $outputDir = __DIR__ . '/../../Modules/Media/Files';
73
74    /**
75     * Output file name.
76     *
77     * @var bool
78     * @since 1.0.0
79     */
80    public bool $preserveFileName = true;
81
82    /**
83     * Upload file to server.
84     *
85     * @param array    $files         File data ($_FILE)
86     * @param string[] $fileNames     File name
87     * @param bool     $absolute      Use absolute path
88     * @param string   $encryptionKey Encryption key
89     * @param string   $encoding      Encoding used for uploaded file. Empty string will not convert file content.
90     *
91     * @return array
92     *
93     * @throws \Exception
94     *
95     * @since 1.0.0
96     */
97    public function upload(
98        array $files,
99        array $fileNames = [],
100        bool $absolute = false,
101        string $encryptionKey = '',
102        string $encoding = 'UTF-8'
103    ) : array
104    {
105        $result    = [];
106        $fileNames = $this->preserveFileName || \count($files) !== \count($fileNames) ? [] : $fileNames;
107
108        if (\count($files) === \count($files, \COUNT_RECURSIVE)) {
109            $files = [$files];
110        }
111
112        if (!$absolute && \count($files) > 1) {
113            $this->outputDir = $this->findOutputDir();
114        }
115
116        $fCounter = -1;
117        foreach ($files as $key => $f) {
118            if (!isset($f['name'], $f['tmp_name'], $f['size'])) {
119                return [];
120            }
121
122            ++$fCounter;
123
124            $path = $this->outputDir;
125
126            $subdir = '';
127            if (($last = \strripos($f['name'], '/')) !== false) {
128                $subdir    = \substr($f['name'], 0, $last);
129                $f['name'] = \substr($f['name'], $last + 1);
130            }
131
132            if (!\is_file($f['tmp_name'])) {
133                $result[$key]['status'] = UploadStatus::FILE_NOT_FOUND;
134
135                return $result;
136            }
137
138            if ($path === '') {
139                $path = File::dirpath($f['tmp_name']);
140            }
141
142            if ($subdir !== '') {
143                $path .= '/' . $subdir;
144            }
145
146            $result[$key]           = [];
147            $result[$key]['status'] = UploadStatus::OK;
148
149            if (!isset($f['error'])) {
150                $result[$key]['status'] = UploadStatus::WRONG_PARAMETERS;
151
152                return $result;
153            } elseif ($f['error'] !== \UPLOAD_ERR_OK) {
154                $result[$key]['status'] = $this->getUploadError($f['error']);
155
156                return $result;
157            }
158
159            $result[$key]['size'] = $f['size'];
160
161            if ($f['size'] > $this->maxSize) {
162                $result[$key]['status'] = UploadStatus::CONFIG_SIZE;
163
164                return $result;
165            }
166
167            if (!empty($this->allowedTypes) && !\in_array($f['type'], $this->allowedTypes, true)) {
168                $result[$key]['status'] = UploadStatus::WRONG_EXTENSION;
169
170                return $result;
171            }
172
173            $split                     = \explode('.', $f['name']);
174            $result[$key]['filename']  = empty($fileNames) ? $f['name'] : $fileNames[$fCounter];
175            $result[$key]['extension'] = ($c = \count($split)) > 1 ? $split[$c - 1] : '';
176
177            if (!$this->preserveFileName || \is_file($path . '/' . $result[$key]['filename'])) {
178                try {
179                    $result[$key]['filename'] = $this->createFileName(
180                        $path,
181                        $this->preserveFileName ? $result[$key]['filename'] : '',
182                        $result[$key]['extension']
183                    );
184                } catch (\Throwable $_) {
185                    $result[$key]['filename'] = $f['name'];
186                    $result[$key]['status']   = UploadStatus::FAILED_HASHING;
187
188                    return $result;
189                }
190            }
191
192            if (!\is_dir($path) && !Directory::create($path, 0755, true)) {
193                FileLogger::getInstance()->error('Couldn\t upload media file. There maybe is a problem with your permission or uploaded file.');
194            }
195
196            if (!\rename($f['tmp_name'], $dest = $path . '/' . $result[$key]['filename'])) {
197                $result[$key]['status'] = UploadStatus::NOT_MOVABLE;
198
199                return $result;
200            }
201
202            if ($encryptionKey !== '') {
203                $isEncrypted = EncryptionHelper::encryptFile($dest, $dest, $encryptionKey);
204
205                if (!$isEncrypted) {
206                    $result[$key]['status'] = UploadStatus::NOT_ENCRYPTABLE;
207
208                    return $result;
209                }
210            }
211
212            /*
213            if ($this->isInterlaced && \in_array($extension, FileUtils::IMAGE_EXTENSION)) {
214                //$this->interlace($extension, $dest);
215            }
216            */
217
218            /*
219            if ($encoding !== '') {
220                // changing encoding bugs out image files
221                //FileUtils::changeFileEncoding($dest, $encoding);
222            }*/
223
224            $result[$key]['path'] = $path;
225        }
226
227        return $result;
228    }
229
230    /**
231     * Create file name if file already exists or if file name should be random.
232     *
233     * @param string $path      Path where file should be saved
234     * @param string $tempName  Temp. file name generated during upload
235     * @param string $extension Extension name
236     *
237     * @return string
238     *
239     * @throws \Exception This exception is thrown if the file couldn't be created
240     *
241     * @since 1.0.0
242     */
243    private function createFileName(string $path, string $tempName, string $extension) : string
244    {
245        $rnd   = '';
246        $limit = -1;
247
248        $nameWithoutExtension = empty($tempName)
249            ? ''
250            : (empty($extension)
251                ? $tempName
252                : \substr($tempName, 0, -\strlen($extension) - 1)
253            );
254
255        $fileName = $tempName;
256
257        while (\is_file($path . '/' . $fileName)) {
258            if ($limit >= self::PATH_GENERATION_LIMIT) {
259                throw new \Exception('No file path could be found. Potential attack!');
260            }
261
262            ++$limit;
263            $tempName = empty($nameWithoutExtension)
264                ? \sha1($tempName . $rnd)
265                : $nameWithoutExtension . ($limit === 1 ? '' : '_' . $rnd);
266
267            $fileName = empty($extension)
268                ? $tempName
269                : $tempName . '.' . $extension;
270
271            $rnd = \bin2hex(\random_bytes(3));
272        }
273
274        return $fileName;
275    }
276
277    /**
278     * Make image interlace
279     *
280     * @param string $extension Image extension
281     * @param string $path      File path
282     *
283     * @return void
284     *
285     * @since 1.0.0
286     */
287    /*
288    private function interlace(string $extension, string $path) : void
289    {
290        if ($extension === 'png') {
291            $img = \imagecreatefrompng($path);
292        } elseif ($extension === 'jpg' || $extension === 'jpeg') {
293            $img = \imagecreatefromjpeg($path);
294        } else {
295            $img = \imagecreatefromgif($path);
296        }
297
298        if ($img === false) {
299            return;
300        }
301
302        \imageinterlace($img, $this->isInterlaced);
303
304        if ($extension === 'png') {
305            \imagepng($img, $path);
306        } elseif ($extension === 'jpg' || $extension === 'jpeg') {
307            \imagejpeg($img, $path);
308        } else {
309            \imagegif($img, $path);
310        }
311
312        \imagedestroy($img);
313    }
314    */
315
316    /**
317     * Find unique output path for batch of files
318     *
319     * @return string
320     *
321     * @since 1.0.0
322     */
323    private function findOutputDir() : string
324    {
325        $rndPath = '';
326
327        do {
328            $rndPath = \str_pad(\dechex(\mt_rand(0, 65535)), 4, '0', \STR_PAD_LEFT);
329        } while (\is_dir($this->outputDir . '/_' . $rndPath));
330
331        return $this->outputDir . '/_' . $rndPath;
332    }
333
334    /**
335     * Get upload error
336     *
337     * @param mixed $error Error type
338     *
339     * @return int
340     *
341     * @since 1.0.0
342     */
343    private function getUploadError($error) : int
344    {
345        switch ($error) {
346            case \UPLOAD_ERR_NO_FILE:
347                return UploadStatus::NOTHING_UPLOADED;
348            case \UPLOAD_ERR_INI_SIZE:
349            case \UPLOAD_ERR_FORM_SIZE:
350                return UploadStatus::UPLOAD_SIZE;
351            default:
352                return UploadStatus::UNKNOWN_ERROR;
353        }
354    }
355
356    /**
357     * @return string[]
358     *
359     * @since 1.0.0
360     */
361    public function getAllowedTypes() : array
362    {
363        return $this->allowedTypes;
364    }
365
366    /**
367     * @param string[] $allowedTypes Allowed file types
368     *
369     * @return void
370     *
371     * @since 1.0.0
372     */
373    public function setAllowedTypes(array $allowedTypes) : void
374    {
375        $this->allowedTypes = $allowedTypes;
376    }
377
378    /**
379     * @param string $allowedTypes Allowed file types
380     *
381     * @return void
382     *
383     * @since 1.0.0
384     */
385    public function addAllowedTypes(string $allowedTypes) : void
386    {
387        $this->allowedTypes[] = $allowedTypes;
388    }
389}