]>
jfr.im git - uguu.git/blob - src/Classes/Upload.php
5 * @copyright Copyright (c) 2022-2024 Go Johansson (nokonoko) <neku@pomf.se>
8 * Note that this was previously distributed under the MIT license 2015-2022.
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.
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.
16 * This project can be found at <https://github.com/nokonoko/Uguu>.
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.
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.
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/>.
32 namespace Pomf\Uguu\Classes
;
34 class Upload
extends Response
36 public array $FILE_INFO;
37 public array $fingerPrintInfo;
38 private mixed $Connector;
41 * Resolves and processes an array of files, performing various checks and operations on each file.
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.
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.
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.
67 public function reFiles(array $files):array
69 $this->Connector
= new Connector();
71 $files = $this->diverseArray($files);
72 foreach ($files as $file) {
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),
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;
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']);
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'],
111 * Rearranges a multidimensional array by exchanging the keys of the first and second level.
113 * @param array $files The multidimensional array to be rearranged.
115 * @return array The rearranged array with exchanged keys of the first and second level.
117 public function diverseArray(array $files):array
120 foreach ($files as $key1 => $value1) {
121 foreach ($value1 as $key2 => $value2) {
122 $result[$key2][$key1] = $value2;
129 * Performs various checks (if enabled), insert info into database, moves file to storage
130 * location, then returns an array of file information.
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.
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
143 public function uploadFile():array
145 if ($this->Connector
->CONFIG
['RATE_LIMIT']) {
147 $this->Connector
->checkRateLimit(
148 $this->fingerPrintInfo
,
149 $this->Connector
->CONFIG
['RATE_LIMIT_TIMEOUT'],
150 $this->Connector
->CONFIG
['RATE_LIMIT_FILES'],
153 $this->Connector
->response
->error(
155 'Rate limit, please wait ' . $this->Connector
->CONFIG
['RATE_LIMIT_TIMEOUT'] .
156 ' seconds before uploading again.',
160 if ($this->Connector
->CONFIG
['BLACKLIST_DB']) {
161 $this->Connector
->checkFileBlacklist($this->FILE_INFO
['XXH']);
163 if ($this->Connector
->CONFIG
['FILTER_MODE'] && empty($this->FILE_INFO
['EXTENSION'])) {
164 $this->checkMimeBlacklist();
166 if ($this->Connector
->CONFIG
['FILTER_MODE'] && !empty($this->FILE_INFO
['EXTENSION'])) {
167 $this->checkMimeBlacklist();
168 $this->checkExtensionBlacklist();
170 if (!$this->Connector
->CONFIG
['FILTER_MODE'] && empty($this->FILE_INFO
['EXTENSION'])) {
171 $this->checkMimeWhitelist();
173 if (!$this->Connector
->CONFIG
['FILTER_MODE'] && !empty($this->FILE_INFO
['EXTENSION'])) {
174 $this->checkMimeWhitelist();
175 $this->checkExtensionWhitelist();
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.');
185 $this->FILE_INFO
['TEMP_NAME'],
186 $this->Connector
->CONFIG
['FILES_ROOT'] .
187 $this->FILE_INFO
['FILENAME'],
190 $this->Connector
->response
->error(500, 'Failed to move file to destination.');
192 if (!chmod($this->Connector
->CONFIG
['FILES_ROOT'] . $this->FILE_INFO
['FILENAME'], 0644)) {
193 $this->Connector
->response
->error(500, 'Failed to change file permissions.');
195 $this->Connector
->newIntoDB($this->FILE_INFO
, $this->fingerPrintInfo
);
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'],
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.
211 * @param $files_amount int The amount of files that are being uploaded.
214 public function fingerPrint(int $files_amount):void
216 if (!empty($_SERVER['HTTP_USER_AGENT'])) {
217 $USER_AGENT = filter_var($_SERVER['HTTP_USER_AGENT'], FILTER_SANITIZE_ENCODED
);
219 if ($this->Connector
->CONFIG
['LOG_IP']) {
220 $ip = $_SERVER['REMOTE_ADDR'];
222 $this->fingerPrintInfo
= [
223 'timestamp' => time(),
224 'useragent' => $USER_AGENT,
226 'ip_hash' => hash('xxh3', $_SERVER['REMOTE_ADDR'] . $USER_AGENT),
227 'files_amount' => $files_amount,
230 $this->Connector
->response
->error(500, 'Invalid user agent.');
235 * Returns the MIME type of a file
237 * @param $file array The file to be checked.
239 * @return string The MIME type of the file.
241 public function fileMIME(array $file):string
243 $FILE_INFO = finfo_open(FILEINFO_MIME_TYPE
);
244 return finfo_file($FILE_INFO, $file['tmp_name']);
248 * Determines the double dot file extension from the given file.
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.
254 * @param array $extension An array of strings representing file extensions.
256 * @return string The extracted extension.
258 public function doubleDotExtension(array $extension):string
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'])) {
265 return end($extension);
269 * Determines the file extension from the given file.
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.
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.
279 * @param array $file The file array containing the name of the file.
281 * @return string|bool The file extension if it exists, or false if the file name does not contain a dot.
283 public function fileExtension(array $file):string|bool
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)
297 * Checks if the MIME type of the uploaded file is in the blacklist.
299 * If the MIME is in the blacklist, an error is returned indicating that the filetype
303 public function checkMimeBlacklist():void
305 if (in_array($this->FILE_INFO
['MIME'], $this->Connector
->CONFIG
['FILTER_MIME'])) {
306 $this->Connector
->response
->error(415, 'Filetype not allowed');
311 * Checks if the MIME type of the uploaded file is in the whitelist.
313 * If the MIME type is not in the whitelist, an error is returned indicating that the filetype
317 public function checkMimeWhitelist():void
319 if (!in_array($this->FILE_INFO
['MIME'], $this->Connector
->CONFIG
['FILTER_MIME'])) {
320 $this->Connector
->response
->error(415, 'Filetype not allowed');
325 * Checks if the extension of the uploaded file is in the blacklist.
327 * If the extension is in the blacklist, an error is returned indicating that the filetype
331 public function checkExtensionBlacklist():void
333 if (in_array($this->FILE_INFO
['EXTENSION'], $this->Connector
->CONFIG
['FILTER_EXTENSIONS'])) {
334 $this->Connector
->response
->error(415, 'Filetype not allowed');
339 * Checks if the extension of the uploaded file is in the whitelist.
341 * If the extension is not in the whitelist, an error is returned indicating that the filetype
345 public function checkExtensionWhitelist():void
347 if (!in_array($this->FILE_INFO
['EXTENSION'], $this->Connector
->CONFIG
['FILTER_EXTENSIONS'])) {
348 $this->Connector
->response
->error(415, 'Filetype not allowed');
353 * Checks if the length of the given filename exceeds 250 characters.
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.
358 * @param string $fileName The filename to check the length for.
360 * @return string The filename, either unchanged or truncated if its length exceeds 250 characters.
362 public function checkNameLength(string $fileName):string
364 if (strlen($fileName) > 250) {
365 return substr($fileName, 0, 250);
371 * Generates a unique name for a file.
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.
380 * @param string $extension The extension of the file.
382 * @return string The generated unique name for the file.
384 public function generateName(string $extension):string
387 if ($this->Connector
->CONFIG
['FILES_RETRIES'] === 0) {
388 $this->Connector
->response
->error(500, 'Gave up trying to find an unused name!');
390 $NEW_NAME = $this->Connector
->randomizer
->getBytesFromString(
391 $this->Connector
->CONFIG
['ID_CHARSET'],
392 $this->Connector
->CONFIG
['NAME_LENGTH'],
395 $NEW_NAME .= '.' . $extension;
397 } while ($this->Connector
->dbCheckNameExists($NEW_NAME));