]>
Commit | Line | Data |
---|---|---|
e480c0e5 | 1 | <?php |
9184e8d4 GJ |
2 | /* |
3 | * Uguu | |
4 | * | |
5 | * @copyright Copyright (c) 2022-2024 Go Johansson (nokonoko) <neku@pomf.se> | |
6 | * @links | |
7 | * | |
8 | * Note that this was previously distributed under the MIT license 2015-2022. | |
9 | * | |
10 | * If you are a company that wants to use Uguu I urge you to contact me to | |
11 | * solve any potential license issues rather then using pre-2022 code. | |
12 | * | |
13 | * A special thanks goes out to the open source community around the world | |
14 | * for supporting and being the backbone of projects like Uguu. | |
15 | * | |
16 | * This project can be found at <https://github.com/nokonoko/Uguu>. | |
17 | * | |
18 | * This program is free software: you can redistribute it and/or modify | |
19 | * it under the terms of the GNU General Public License as published by | |
20 | * the Free Software Foundation, either version 3 of the License, or | |
21 | * (at your option) any later version. | |
22 | * | |
23 | * This program is distributed in the hope that it will be useful, | |
24 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
25 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
26 | * GNU General Public License for more details. | |
27 | * | |
28 | * You should have received a copy of the GNU General Public License | |
29 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | |
30 | */ | |
31 | ||
32 | namespace Pomf\Uguu\Classes; | |
33 | ||
34 | class Upload extends Response | |
35 | { | |
36 | public array $FILE_INFO; | |
37 | public array $fingerPrintInfo; | |
38 | private mixed $Connector; | |
39 | ||
40 | /** | |
41 | * Resolves and processes an array of files, performing various checks and operations on each file. | |
8f7f8840 | 42 | * |
9184e8d4 GJ |
43 | * Check if the file is a dupe (if enabled). |
44 | * Generate a new name (if not a dupe). | |
45 | * Generate hash of the file. | |
46 | * Get the extension of the file. | |
47 | * Get the MIME of the file. | |
48 | * Get the size of the file. | |
8f7f8840 | 49 | * |
9184e8d4 GJ |
50 | * @param array $files An array of file data. Each element should be an associative array |
51 | * * with the following keys: | |
52 | * * - 'tmp_name' : The temporary name of the uploaded file. | |
53 | * * - 'name' : The original name of the file. | |
54 | * * - 'size' : The size of the file in bytes. | |
8f7f8840 | 55 | * |
9184e8d4 GJ |
56 | * @return array An array containing information about each uploaded file. Each element of the array |
57 | * * is an associative array with the following keys: | |
58 | * * - 'temp_name' : The temporary name of the uploaded file. | |
59 | * * - 'name' : The processed name of the file after checking for length and removing tags. | |
60 | * * - 'size' : The size of the uploaded file in bytes. | |
61 | * * - 'xxh' : The xxhash of the uploaded file. | |
62 | * * - 'extension' : The file extension. | |
63 | * * - 'mime' : The MIME type of the file. | |
64 | * * - 'dupe' : Indicates if the uploaded file is a duplicate. | |
65 | * * - 'filename' : The final filename of the uploaded file. | |
8f7f8840 | 66 | */ |
9184e8d4 | 67 | public function reFiles(array $files):array |
e480c0e5 | 68 | { |
9184e8d4 GJ |
69 | $this->Connector = new Connector(); |
70 | $result = []; | |
71 | $files = $this->diverseArray($files); | |
72 | foreach ($files as $file) { | |
73 | $this->FILE_INFO = [ | |
74 | 'TEMP_NAME' => $file['tmp_name'], | |
75 | 'NAME' => strip_tags($this->checkNameLength($file['name'])), | |
76 | 'SIZE' => $file['size'], | |
77 | 'XXH' => hash_file('xxh3', $file['tmp_name']), | |
78 | 'EXTENSION' => $this->fileExtension($file), | |
79 | 'MIME' => $this->fileMIME($file), | |
80 | 'DUPE' => false, | |
81 | 'FILENAME' => null, | |
82 | ]; | |
83 | // Check if anti dupe is enabled | |
84 | if ($this->Connector->CONFIG['ANTI_DUPE']) { | |
85 | // Check if hash exists in DB, if it does return the name of the file | |
86 | $dupeResult = $this->Connector->antiDupe($this->FILE_INFO['XXH']); | |
87 | if ($dupeResult['result']) { | |
88 | $this->FILE_INFO['FILENAME'] = $dupeResult['name']; | |
89 | $this->FILE_INFO['DUPE'] = true; | |
52053519 | 90 | } |
f0b5e51c | 91 | } |
9184e8d4 GJ |
92 | // If its not a dupe then generate a new name |
93 | if (!$this->FILE_INFO['DUPE']) { | |
94 | $this->FILE_INFO['FILENAME'] = $this->generateName($this->FILE_INFO['EXTENSION']); | |
95 | } | |
96 | $result[] = [ | |
97 | $this->FILE_INFO['TEMP_NAME'], | |
98 | $this->FILE_INFO['NAME'], | |
99 | $this->FILE_INFO['SIZE'], | |
100 | $this->FILE_INFO['XXH'], | |
101 | $this->FILE_INFO['EXTENSION'], | |
102 | $this->FILE_INFO['MIME'], | |
103 | $this->FILE_INFO['DUPE'], | |
104 | $this->FILE_INFO['FILENAME'], | |
105 | ]; | |
e480c0e5 | 106 | } |
9184e8d4 GJ |
107 | return $result; |
108 | } | |
52053519 | 109 | |
9184e8d4 GJ |
110 | /** |
111 | * Rearranges a multidimensional array by exchanging the keys of the first and second level. | |
112 | * | |
113 | * @param array $files The multidimensional array to be rearranged. | |
114 | * | |
115 | * @return array The rearranged array with exchanged keys of the first and second level. | |
116 | */ | |
117 | public function diverseArray(array $files):array | |
118 | { | |
119 | $result = []; | |
120 | foreach ($files as $key1 => $value1) { | |
121 | foreach ($value1 as $key2 => $value2) { | |
122 | $result[$key2][$key1] = $value2; | |
cec6349e | 123 | } |
e480c0e5 | 124 | } |
9184e8d4 GJ |
125 | return $result; |
126 | } | |
52053519 | 127 | |
9184e8d4 GJ |
128 | /** |
129 | * Performs various checks (if enabled), insert info into database, moves file to storage | |
130 | * location, then returns an array of file information. | |
131 | * | |
132 | * If a check is triggered or another error occurs it will return an error stating why | |
133 | * the file was unable to be uploaded. | |
134 | * | |
135 | * @return array An array containing the following information: | |
136 | * - hash : The hash value of the uploaded file | |
137 | * - name : The name of the uploaded file | |
138 | * - filename : The filename of the uploaded file | |
139 | * - url : The URL of the uploaded file | |
140 | * - size : The size of the uploaded file | |
141 | * - dupe : Boolean indicating whether the file is a duplicate | |
142 | */ | |
143 | public function uploadFile():array | |
144 | { | |
145 | if ($this->Connector->CONFIG['RATE_LIMIT']) { | |
146 | if ( | |
147 | $this->Connector->checkRateLimit( | |
148 | $this->fingerPrintInfo, | |
149 | $this->Connector->CONFIG['RATE_LIMIT_TIMEOUT'], | |
150 | $this->Connector->CONFIG['RATE_LIMIT_FILES'], | |
151 | ) | |
152 | ) { | |
153 | $this->Connector->response->error( | |
154 | 500, | |
155 | 'Rate limit, please wait ' . $this->Connector->CONFIG['RATE_LIMIT_TIMEOUT'] . | |
4469e4dc | 156 | ' seconds before uploading again.', |
9184e8d4 | 157 | ); |
4469e4dc | 158 | } |
9184e8d4 GJ |
159 | } |
160 | if ($this->Connector->CONFIG['BLACKLIST_DB']) { | |
161 | $this->Connector->checkFileBlacklist($this->FILE_INFO['XXH']); | |
162 | } | |
163 | if ($this->Connector->CONFIG['FILTER_MODE'] && empty($this->FILE_INFO['EXTENSION'])) { | |
164 | $this->checkMimeBlacklist(); | |
165 | } | |
166 | if ($this->Connector->CONFIG['FILTER_MODE'] && !empty($this->FILE_INFO['EXTENSION'])) { | |
167 | $this->checkMimeBlacklist(); | |
168 | $this->checkExtensionBlacklist(); | |
169 | } | |
170 | if (!$this->Connector->CONFIG['FILTER_MODE'] && empty($this->FILE_INFO['EXTENSION'])) { | |
171 | $this->checkMimeWhitelist(); | |
172 | } | |
173 | if (!$this->Connector->CONFIG['FILTER_MODE'] && !empty($this->FILE_INFO['EXTENSION'])) { | |
174 | $this->checkMimeWhitelist(); | |
175 | $this->checkExtensionWhitelist(); | |
176 | } | |
177 | // If its not a dupe then skip checking if file can be written and | |
178 | // skip inserting it into the DB. | |
179 | if (!$this->FILE_INFO['DUPE']) { | |
180 | if (!is_dir($this->Connector->CONFIG['FILES_ROOT'])) { | |
181 | $this->Connector->response->error(500, 'File storage path not accessible.'); | |
e2c8b572 | 182 | } |
9184e8d4 GJ |
183 | if ( |
184 | !move_uploaded_file( | |
185 | $this->FILE_INFO['TEMP_NAME'], | |
186 | $this->Connector->CONFIG['FILES_ROOT'] . | |
52053519 | 187 | $this->FILE_INFO['FILENAME'], |
9184e8d4 GJ |
188 | ) |
189 | ) { | |
190 | $this->Connector->response->error(500, 'Failed to move file to destination.'); | |
52053519 | 191 | } |
9184e8d4 GJ |
192 | if (!chmod($this->Connector->CONFIG['FILES_ROOT'] . $this->FILE_INFO['FILENAME'], 0644)) { |
193 | $this->Connector->response->error(500, 'Failed to change file permissions.'); | |
194 | } | |
195 | $this->Connector->newIntoDB($this->FILE_INFO, $this->fingerPrintInfo); | |
e480c0e5 | 196 | } |
9184e8d4 GJ |
197 | return [ |
198 | 'hash' => $this->FILE_INFO['XXH'], | |
199 | 'name' => $this->FILE_INFO['NAME'], | |
200 | 'filename' => $this->FILE_INFO['FILENAME'], | |
201 | 'url' => 'https://' . $this->Connector->CONFIG['FILE_DOMAIN'] . '/' . $this->FILE_INFO['FILENAME'], | |
202 | 'size' => $this->FILE_INFO['SIZE'], | |
203 | 'dupe' => $this->FILE_INFO['DUPE'], | |
204 | ]; | |
205 | } | |
52053519 | 206 | |
9184e8d4 GJ |
207 | /** |
208 | * Takes the amount of files that are being uploaded, and creates a fingerprint of the user's IP address, | |
209 | * user agent, and the amount of files being uploaded. | |
210 | * | |
211 | * @param $files_amount int The amount of files that are being uploaded. | |
212 | * | |
213 | */ | |
214 | public function fingerPrint(int $files_amount):void | |
215 | { | |
216 | if (!empty($_SERVER['HTTP_USER_AGENT'])) { | |
217 | $USER_AGENT = filter_var($_SERVER['HTTP_USER_AGENT'], FILTER_SANITIZE_ENCODED); | |
218 | $ip = null; | |
219 | if ($this->Connector->CONFIG['LOG_IP']) { | |
220 | $ip = $_SERVER['REMOTE_ADDR']; | |
52053519 | 221 | } |
9184e8d4 GJ |
222 | $this->fingerPrintInfo = [ |
223 | 'timestamp' => time(), | |
224 | 'useragent' => $USER_AGENT, | |
225 | 'ip' => $ip, | |
226 | 'ip_hash' => hash('xxh3', $_SERVER['REMOTE_ADDR'] . $USER_AGENT), | |
227 | 'files_amount' => $files_amount, | |
228 | ]; | |
229 | } else { | |
230 | $this->Connector->response->error(500, 'Invalid user agent.'); | |
e480c0e5 | 231 | } |
9184e8d4 | 232 | } |
52053519 | 233 | |
9184e8d4 GJ |
234 | /** |
235 | * Returns the MIME type of a file | |
236 | * | |
237 | * @param $file array The file to be checked. | |
238 | * | |
239 | * @return string The MIME type of the file. | |
240 | */ | |
241 | public function fileMIME(array $file):string | |
242 | { | |
243 | $FILE_INFO = finfo_open(FILEINFO_MIME_TYPE); | |
244 | return finfo_file($FILE_INFO, $file['tmp_name']); | |
245 | } | |
52053519 | 246 | |
9184e8d4 GJ |
247 | /** |
248 | * Determines the double dot file extension from the given file. | |
249 | * | |
250 | * If the last two elements of the array contain double dots, those will be extracted and concatenated. | |
251 | * If the resulting double-dot extension is present in the whitelist, it will be returned. | |
252 | * Otherwise, the last element of the array will be returned. | |
253 | * | |
254 | * @param array $extension An array of strings representing file extensions. | |
255 | * | |
256 | * @return string The extracted extension. | |
257 | */ | |
258 | public function doubleDotExtension(array $extension):string | |
259 | { | |
260 | $doubleDotArray = array_slice($extension, -2, 2); | |
261 | $doubleDot = strtolower(preg_replace('/[^a-zA-Z.]/', '', join('.', $doubleDotArray))); | |
262 | if (in_array($doubleDot, $this->Connector->CONFIG['DOUBLE_DOTS_EXTENSIONS'])) { | |
263 | return $doubleDot; | |
e480c0e5 | 264 | } |
9184e8d4 GJ |
265 | return end($extension); |
266 | } | |
52053519 | 267 | |
9184e8d4 GJ |
268 | /** |
269 | * Determines the file extension from the given file. | |
270 | * | |
271 | * The method checks if the file name contains a dot (.). If it does, the file name is split | |
272 | * using the dot as the delimiter to extract the extension. The number of dots in the file name | |
273 | * is also counted to handle special cases. | |
274 | * | |
275 | * If the file name contains exactly two dots, the method calls the doubleDotExtension() function | |
276 | * to handle the special case. Otherwise, the method returns the last element of the exploded | |
277 | * file name array, which represents the extension. | |
278 | * | |
279 | * @param array $file The file array containing the name of the file. | |
280 | * | |
281 | * @return string|bool The file extension if it exists, or false if the file name does not contain a dot. | |
282 | */ | |
283 | public function fileExtension(array $file):string|bool | |
284 | { | |
285 | if (str_contains($file['name'], '.')) { | |
286 | $extension = explode('.', $file['name']); | |
287 | $dotCount = substr_count($file['name'], '.'); | |
288 | return match ($dotCount) { | |
289 | 2 => $this->doubleDotExtension($extension), | |
290 | default => end($extension) | |
291 | }; | |
4246dede | 292 | } |
9184e8d4 GJ |
293 | return false; |
294 | } | |
52053519 | 295 | |
9184e8d4 GJ |
296 | /** |
297 | * Checks if the MIME type of the uploaded file is in the blacklist. | |
298 | * | |
299 | * If the MIME is in the blacklist, an error is returned indicating that the filetype | |
300 | * is not allowed. | |
301 | * | |
302 | */ | |
303 | public function checkMimeBlacklist():void | |
304 | { | |
305 | if (in_array($this->FILE_INFO['MIME'], $this->Connector->CONFIG['FILTER_MIME'])) { | |
306 | $this->Connector->response->error(415, 'Filetype not allowed'); | |
52053519 | 307 | } |
9184e8d4 | 308 | } |
52053519 | 309 | |
9184e8d4 GJ |
310 | /** |
311 | * Checks if the MIME type of the uploaded file is in the whitelist. | |
312 | * | |
313 | * If the MIME type is not in the whitelist, an error is returned indicating that the filetype | |
314 | * is not allowed. | |
315 | * | |
316 | */ | |
317 | public function checkMimeWhitelist():void | |
318 | { | |
319 | if (!in_array($this->FILE_INFO['MIME'], $this->Connector->CONFIG['FILTER_MIME'])) { | |
320 | $this->Connector->response->error(415, 'Filetype not allowed'); | |
4469e4dc | 321 | } |
9184e8d4 | 322 | } |
4469e4dc | 323 | |
9184e8d4 GJ |
324 | /** |
325 | * Checks if the extension of the uploaded file is in the blacklist. | |
326 | * | |
327 | * If the extension is in the blacklist, an error is returned indicating that the filetype | |
328 | * is not allowed. | |
329 | * | |
330 | */ | |
331 | public function checkExtensionBlacklist():void | |
332 | { | |
333 | if (in_array($this->FILE_INFO['EXTENSION'], $this->Connector->CONFIG['FILTER_EXTENSIONS'])) { | |
334 | $this->Connector->response->error(415, 'Filetype not allowed'); | |
52053519 | 335 | } |
9184e8d4 | 336 | } |
52053519 | 337 | |
9184e8d4 GJ |
338 | /** |
339 | * Checks if the extension of the uploaded file is in the whitelist. | |
340 | * | |
341 | * If the extension is not in the whitelist, an error is returned indicating that the filetype | |
342 | * is not allowed. | |
343 | * | |
344 | */ | |
345 | public function checkExtensionWhitelist():void | |
346 | { | |
347 | if (!in_array($this->FILE_INFO['EXTENSION'], $this->Connector->CONFIG['FILTER_EXTENSIONS'])) { | |
348 | $this->Connector->response->error(415, 'Filetype not allowed'); | |
4469e4dc | 349 | } |
9184e8d4 | 350 | } |
4469e4dc | 351 | |
9184e8d4 GJ |
352 | /** |
353 | * Checks if the length of the given filename exceeds 250 characters. | |
354 | * | |
355 | * If the length of the filename exceeds 250 characters, it is truncated to a maximum of 250 characters. | |
356 | * Otherwise, the filename remains unchanged. | |
357 | * | |
358 | * @param string $fileName The filename to check the length for. | |
359 | * | |
360 | * @return string The filename, either unchanged or truncated if its length exceeds 250 characters. | |
361 | */ | |
362 | public function checkNameLength(string $fileName):string | |
363 | { | |
364 | if (strlen($fileName) > 250) { | |
365 | return substr($fileName, 0, 250); | |
52053519 | 366 | } |
9184e8d4 GJ |
367 | return $fileName; |
368 | } | |
52053519 | 369 | |
9184e8d4 GJ |
370 | /** |
371 | * Generates a unique name for a file. | |
372 | * | |
373 | * This method generates a random name for a file by selecting characters from the ID_CHARSET | |
374 | * defined in the Connector's CONFIG. If an extension is provided, it appends the extension | |
375 | * to the generated name. The method then checks if the generated name already exists in the | |
376 | * database using the dbCheckNameExists() function. If the generated name | |
377 | * already exists, it generates a new name until a unique one is found. If the maximum number | |
378 | * of retries is reached, an error is returned. | |
379 | * | |
380 | * @param string $extension The extension of the file. | |
381 | * | |
382 | * @return string The generated unique name for the file. | |
383 | */ | |
384 | public function generateName(string $extension):string | |
385 | { | |
386 | do { | |
387 | if ($this->Connector->CONFIG['FILES_RETRIES'] === 0) { | |
388 | $this->Connector->response->error(500, 'Gave up trying to find an unused name!'); | |
389 | } | |
390 | $NEW_NAME = $this->Connector->randomizer->getBytesFromString( | |
391 | $this->Connector->CONFIG['ID_CHARSET'], | |
392 | $this->Connector->CONFIG['NAME_LENGTH'], | |
393 | ); | |
394 | if ($extension) { | |
395 | $NEW_NAME .= '.' . $extension; | |
396 | } | |
397 | } while ($this->Connector->dbCheckNameExists($NEW_NAME)); | |
398 | return $NEW_NAME; | |
8f135020 | 399 | } |
9184e8d4 | 400 | } |