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