]>
Commit | Line | Data |
---|---|---|
e480c0e5 | 1 | <?php |
e480c0e5 | 2 | /** |
cec6349e | 3 | * Uguu |
8f7f8840 | 4 | * |
52053519 | 5 | * @copyright Copyright (c) 2022-2023 Go Johansson (nokonoko) <neku@pomf.se> |
8f7f8840 | 6 | * |
cec6349e GJ |
7 | * This program is free software: you can redistribute it and/or modify |
8 | * it under the terms of the GNU General Public License as published by | |
9 | * the Free Software Foundation, either version 3 of the License, or | |
10 | * (at your option) any later version. | |
8f7f8840 | 11 | * |
cec6349e GJ |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | * GNU General Public License for more details. | |
8f7f8840 | 16 | * |
cec6349e GJ |
17 | * You should have received a copy of the GNU General Public License |
18 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | |
8f7f8840 | 19 | */ |
52053519 | 20 | |
f059e2cf | 21 | namespace Pomf\Uguu\Classes; |
52053519 GJ |
22 | |
23 | class Upload extends Response | |
e480c0e5 | 24 | { |
52053519 GJ |
25 | public array $FILE_INFO; |
26 | public array $fingerPrintInfo; | |
27 | private mixed $Connector; | |
28 | ||
29 | /** | |
30 | * Takes an array of files, and returns an array of arrays containing the file's temporary name, | |
31 | * name, size, SHA1 hash, extension, and MIME type | |
32 | * | |
33 | * @param $files array The files array from the $_FILES superglobal. | |
34 | * | |
35 | * @return array An array of arrays. | |
36 | */ | |
37 | public function reFiles(array $files):array | |
38 | { | |
39 | $this->Connector = new Connector(); | |
40 | $result = []; | |
41 | $files = $this->diverseArray($files); | |
42 | foreach ($files as $file) { | |
43 | $this->FILE_INFO = [ | |
44 | 'TEMP_NAME' => $file['tmp_name'], | |
45 | 'NAME' => strip_tags($this->checkNameLength($file['name'])), | |
46 | 'SIZE' => $file['size'], | |
47 | 'SHA1' => sha1_file($file['tmp_name']), | |
48 | 'EXTENSION' => $this->fileExtension($file), | |
49 | 'MIME' => $this->fileMIME($file), | |
50 | 'DUPE' => false, | |
51 | 'FILENAME' => null, | |
52 | ]; | |
53 | // Check if anti dupe is enabled | |
54 | if ($this->Connector->CONFIG['ANTI_DUPE']) { | |
55 | // Check if hash exists in DB, if it does return the name of the file | |
56 | $dupeResult = $this->Connector->antiDupe($this->FILE_INFO['SHA1']); | |
57 | if ($dupeResult['result']) { | |
58 | $this->FILE_INFO['FILENAME'] = $dupeResult['name']; | |
59 | $this->FILE_INFO['DUPE'] = true; | |
60 | } | |
f0b5e51c | 61 | } |
52053519 GJ |
62 | // If its not a dupe then generate a new name |
63 | if (!$this->FILE_INFO['DUPE']) { | |
64 | $this->FILE_INFO['FILENAME'] = $this->generateName($this->FILE_INFO['EXTENSION']); | |
65 | } | |
66 | $result[] = [ | |
67 | $this->FILE_INFO['TEMP_NAME'], | |
68 | $this->FILE_INFO['NAME'], | |
69 | $this->FILE_INFO['SIZE'], | |
70 | $this->FILE_INFO['SHA1'], | |
71 | $this->FILE_INFO['EXTENSION'], | |
72 | $this->FILE_INFO['MIME'], | |
73 | $this->FILE_INFO['DUPE'], | |
74 | $this->FILE_INFO['FILENAME'], | |
75 | ]; | |
f0b5e51c | 76 | } |
52053519 | 77 | return $result; |
e480c0e5 | 78 | } |
52053519 GJ |
79 | |
80 | /** | |
81 | * Takes an array of arrays and returns an array of arrays with the keys and values swapped | |
82 | * | |
83 | * @param $files array an array of arrays | |
84 | * | |
85 | * @return array ``` | |
86 | * array:2 [▼ | |
87 | * 0 => array:2 [▼ | |
88 | * 'TEMP_NAME' => 'example' | |
89 | * 'NAME' => 'example' | |
90 | * 'SIZE' => 'example' | |
91 | * 'SHA1' => 'example' | |
92 | * 'EXTENSION' => 'example' | |
93 | * 'MIME' => 'example' | |
94 | * | |
95 | * ] | |
96 | * 1 => array:2 [▼ | |
97 | * 'TEMP_NAME' => 'example' | |
98 | * 'NAME' => 'example' | |
99 | * 'SIZE' => 'example' | |
100 | * 'SHA1' => 'example' | |
101 | * 'EXTENSION' => 'example' | |
102 | * 'MIME' => 'example' | |
103 | * ] | |
104 | * ] | |
105 | * ``` | |
106 | */ | |
107 | public function diverseArray(array $files):array | |
108 | { | |
109 | $result = []; | |
110 | foreach ($files as $key1 => $value1) { | |
111 | foreach ($value1 as $key2 => $value2) { | |
112 | $result[$key2][$key1] = $value2; | |
113 | } | |
cec6349e | 114 | } |
52053519 | 115 | return $result; |
e480c0e5 | 116 | } |
52053519 GJ |
117 | |
118 | /** | |
119 | * Takes a file, checks if it's blacklisted, moves it to the file storage, and then logs it to the database | |
120 | * | |
121 | * @return array An array containing the hash, name, url, and size of the file. | |
122 | */ | |
123 | public function uploadFile():array | |
124 | { | |
125 | switch (true) { | |
126 | case $this->Connector->CONFIG['RATE_LIMIT']: | |
127 | if ( | |
128 | $this->Connector->checkRateLimit( | |
129 | $this->fingerPrintInfo, | |
130 | $this->Connector->CONFIG['RATE_LIMIT_TIMEOUT'], | |
131 | $this->Connector->CONFIG['RATE_LIMIT_FILES'], | |
132 | ) | |
133 | ) { | |
134 | $this->Connector->response->error( | |
135 | 500, | |
136 | 'Rate limit, please wait ' . $this->Connector->CONFIG['RATE_LIMIT_TIMEOUT'] . | |
137 | ' seconds before uploading again.', | |
138 | ); | |
139 | } | |
f0b5e51c | 140 | // Continue |
52053519 GJ |
141 | case $this->Connector->CONFIG['BLACKLIST_DB']: |
142 | $this->Connector->checkFileBlacklist($this->FILE_INFO['SHA1']); | |
f0b5e51c | 143 | // Continue |
52053519 GJ |
144 | case $this->Connector->CONFIG['FILTER_MODE'] && empty($this->FILE_INFO['EXTENSION']): |
145 | $this->checkMimeBlacklist(); | |
f0b5e51c | 146 | // Continue |
52053519 GJ |
147 | case $this->Connector->CONFIG['FILTER_MODE'] && !empty($this->FILE_INFO['EXTENSION']): |
148 | $this->checkMimeBlacklist(); | |
149 | $this->checkExtensionBlacklist(); | |
f0b5e51c | 150 | // Continue |
e2c8b572 | 151 | } |
52053519 GJ |
152 | // If its not a dupe then skip checking if file can be written and |
153 | // skip inserting it into the DB. | |
154 | if (!$this->FILE_INFO['DUPE']) { | |
155 | if (!is_dir($this->Connector->CONFIG['FILES_ROOT'])) { | |
156 | $this->Connector->response->error(500, 'File storage path not accessible.'); | |
157 | } | |
158 | if ( | |
159 | !move_uploaded_file( | |
160 | $this->FILE_INFO['TEMP_NAME'], | |
161 | $this->Connector->CONFIG['FILES_ROOT'] . | |
162 | $this->FILE_INFO['FILENAME'], | |
163 | ) | |
164 | ) { | |
165 | $this->Connector->response->error(500, 'Failed to move file to destination.'); | |
166 | } | |
167 | if (!chmod($this->Connector->CONFIG['FILES_ROOT'] . $this->FILE_INFO['FILENAME'], 0644)) { | |
168 | $this->Connector->response->error(500, 'Failed to change file permissions.'); | |
169 | } | |
170 | $this->Connector->newIntoDB($this->FILE_INFO, $this->fingerPrintInfo); | |
171 | } | |
172 | return [ | |
173 | 'hash' => $this->FILE_INFO['SHA1'], | |
174 | 'name' => $this->FILE_INFO['NAME'], | |
175 | 'filename' => $this->FILE_INFO['FILENAME'], | |
176 | 'url' => 'https://' . $this->Connector->CONFIG['FILE_DOMAIN'] . '/' . $this->FILE_INFO['FILENAME'], | |
177 | 'size' => $this->FILE_INFO['SIZE'], | |
178 | 'dupe' => $this->FILE_INFO['DUPE'], | |
cec6349e | 179 | ]; |
e480c0e5 | 180 | } |
52053519 GJ |
181 | |
182 | /** | |
183 | * Takes the amount of files that are being uploaded, and creates a fingerprint of the user's IP address, | |
184 | * user agent, and the amount of files being | |
185 | * uploaded | |
186 | * | |
187 | * @param $files_amount int The amount of files that are being uploaded. | |
188 | * | |
189 | */ | |
190 | public function fingerPrint(int $files_amount):void | |
191 | { | |
192 | if (!empty($_SERVER['HTTP_USER_AGENT'])) { | |
193 | $USER_AGENT = filter_var($_SERVER['HTTP_USER_AGENT'], FILTER_SANITIZE_ENCODED); | |
194 | $ip = null; | |
195 | if ($this->Connector->CONFIG['LOG_IP']) { | |
c6a86fc3 | 196 | $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']; |
52053519 GJ |
197 | } |
198 | $this->fingerPrintInfo = [ | |
199 | 'timestamp' => time(), | |
200 | 'useragent' => $USER_AGENT, | |
201 | 'ip' => $ip, | |
202 | 'ip_hash' => hash('sha1', $_SERVER['REMOTE_ADDR'] . $USER_AGENT), | |
203 | 'files_amount' => $files_amount, | |
204 | ]; | |
205 | } else { | |
206 | $this->Connector->response->error(500, 'Invalid user agent.'); | |
207 | } | |
e480c0e5 | 208 | } |
52053519 GJ |
209 | |
210 | /** | |
211 | * Returns the MIME type of a file | |
212 | * | |
213 | * @param $file array The file to be checked. | |
214 | * | |
215 | * @return string The MIME type of the file. | |
216 | */ | |
217 | public function fileMIME(array $file):string | |
218 | { | |
219 | $FILE_INFO = finfo_open(FILEINFO_MIME_TYPE); | |
220 | return finfo_file($FILE_INFO, $file['tmp_name']); | |
e480c0e5 | 221 | } |
52053519 GJ |
222 | |
223 | /** | |
224 | * It takes an array of strings, and returns the last two strings joined by a dot, | |
225 | * unless the last two strings are in the array of strings in the | |
226 | * `DOUBLE_DOTS_EXTENSIONS` config variable, in which case it returns the last string | |
227 | * | |
228 | * @param $extension array The extension of the file. | |
229 | * | |
230 | * @return string The last two elements of the array are joined together and returned. | |
231 | */ | |
232 | public function doubleDotExtension(array $extension):string | |
233 | { | |
234 | $doubleDotArray = array_slice($extension, -2, 2); | |
235 | $doubleDot = strtolower(preg_replace('/[^a-zA-Z.]/', '', join('.', $doubleDotArray))); | |
236 | if (in_array($doubleDot, $this->Connector->CONFIG['DOUBLE_DOTS_EXTENSIONS'])) { | |
237 | return $doubleDot; | |
238 | } else { | |
239 | return end($extension); | |
240 | } | |
e480c0e5 | 241 | } |
52053519 GJ |
242 | |
243 | /** | |
244 | * Takes a file and returns the file extension | |
245 | * | |
246 | * @param $file array The file you want to get the extension from. | |
247 | * | |
248 | * @return string The file extension of the file. | |
249 | */ | |
250 | public function fileExtension(array $file):string | |
251 | { | |
252 | $extension = explode('.', $file['name']); | |
253 | $dotCount = substr_count($file['name'], '.'); | |
254 | return match ($dotCount) { | |
ee5e0add | 255 | 0 => $this->lookupExtension($file['type']), |
52053519 GJ |
256 | 1 => end($extension), |
257 | 2 => $this->doubleDotExtension($extension), | |
258 | default => end($extension) | |
259 | }; | |
4246dede | 260 | } |
52053519 GJ |
261 | |
262 | /** | |
263 | * > Check if the file's MIME type is in the blacklist | |
264 | * | |
265 | */ | |
266 | public function checkMimeBlacklist():void | |
267 | { | |
268 | if (in_array($this->FILE_INFO['MIME'], $this->Connector->CONFIG['BLOCKED_MIME'])) { | |
269 | $this->Connector->response->error(415, 'Filetype not allowed'); | |
cec6349e | 270 | } |
52053519 GJ |
271 | } |
272 | ||
273 | /** | |
274 | * > Check if the file extension is in the blacklist | |
275 | * | |
276 | */ | |
277 | public function checkExtensionBlacklist():void | |
278 | { | |
279 | if (in_array($this->FILE_INFO['EXTENSION'], $this->Connector->CONFIG['BLOCKED_EXTENSIONS'])) { | |
280 | $this->Connector->response->error(415, 'Filetype not allowed'); | |
cec6349e | 281 | } |
52053519 GJ |
282 | } |
283 | ||
284 | public function checkNameLength(string $fileName):string | |
285 | { | |
286 | if (strlen($fileName) > 250) { | |
287 | return substr($fileName, 0, 250); | |
288 | } else { | |
289 | return $fileName; | |
cec6349e | 290 | } |
52053519 GJ |
291 | } |
292 | ||
293 | /** | |
294 | * Generates a random string of characters, checks if it exists in the database, | |
295 | * and if it does, it generates another one | |
296 | * | |
297 | * @param $extension string The file extension. | |
298 | * | |
299 | * @return string A string | |
300 | */ | |
301 | public function generateName(string $extension):string | |
302 | { | |
303 | do { | |
304 | if ($this->Connector->CONFIG['FILES_RETRIES'] === 0) { | |
305 | $this->Connector->response->error(500, 'Gave up trying to find an unused name!'); | |
306 | } | |
307 | $NEW_NAME = ''; | |
308 | for ($i = 0; $i < $this->Connector->CONFIG['NAME_LENGTH']; $i++) { | |
309 | $index = rand(0, strlen($this->Connector->CONFIG['ID_CHARSET']) - 1); | |
310 | $NEW_NAME .= $this->Connector->CONFIG['ID_CHARSET'][$index]; | |
311 | } | |
312 | if (!empty($extension)) { | |
313 | $NEW_NAME .= '.' . $extension; | |
314 | } | |
315 | } while ($this->Connector->dbCheckNameExists($NEW_NAME)); | |
f0b5e51c | 316 | return $NEW_NAME; |
52053519 | 317 | } |
ee5e0add JR |
318 | |
319 | private function lookupExtension(string $mimetype):string | |
320 | { | |
321 | $types = [ | |
322 | 'image/gif' => 'gif', | |
323 | 'image/jpeg' => 'jpg', | |
324 | 'image/avif' => 'avif', | |
325 | 'image/png' => 'png', | |
326 | 'image/tiff' => 'tiff', | |
327 | 'image/vnd.wap.wbmp' => 'wbmp', | |
328 | 'image/webp' => 'webp', | |
329 | 'image/x-icon' => 'ico', | |
330 | 'image/x-jng' => 'jng', | |
331 | 'image/x-ms-bmp' => 'bmp', | |
332 | 'application/pdf' => 'pdf', | |
333 | 'application/postscript' => 'ps', | |
334 | 'application/x-7z-compressed' => '7z', | |
335 | 'application/zip' => 'zip', | |
336 | 'audio/midi' => 'mid', | |
337 | 'audio/mpeg' => 'mp3', | |
338 | 'audio/ogg' => 'ogg', | |
339 | 'audio/x-m4a' => 'm4a', | |
340 | 'audio/x-realaudio' => 'ra', | |
341 | 'video/3gpp' => '3gpp', | |
342 | 'video/mp2t' => 'ts', | |
343 | 'video/mp4' => 'mp4', | |
344 | 'video/mpeg' => 'mpeg', | |
345 | 'video/quicktime' => 'mov', | |
346 | 'video/webm' => 'webm', | |
347 | 'video/x-flv' => 'flv', | |
348 | 'video/x-m4v' => 'm4v', | |
349 | 'video/x-mng' => 'mng', | |
350 | 'video/x-ms-asf' => 'asx', | |
351 | 'video/x-ms-wmv' => 'wmv', | |
352 | 'video/x-msvideo' => 'avi', | |
353 | ]; | |
354 | if (isset($types[$mimetype])) { | |
355 | return $types[$mimetype]; | |
356 | } else { | |
357 | $this->Connector->response->error(400, 'Unknown MIME type. Add a file extension to your filename.'); | |
358 | } | |
359 | } | |
c6a86fc3 | 360 | } |