]> jfr.im git - uguu.git/blame - src/Classes/Upload.php
replace name generator method
[uguu.git] / src / Classes / Upload.php
CommitLineData
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
32namespace Pomf\Uguu\Classes;
33
34class 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}