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 | */ |
14 | declare(strict_types=1); |
15 | |
16 | namespace Modules\Media\Models; |
17 | |
18 | use phpOMS\Log\FileLogger; |
19 | use phpOMS\Security\EncryptionHelper; |
20 | use phpOMS\System\File\Local\Directory; |
21 | use 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 | */ |
32 | class 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 | } |