]> jfr.im git - uguu.git/blame - src/Classes/Upload.php
cleanup and minor bug fix
[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 $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,
fe5fd1ac 31 * name, size, XXH3 hash, extension, and MIME type
52053519
GJ
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'],
fe5fd1ac 47 'XXH' => hash_file('xxh3', $file['tmp_name']),
52053519
GJ
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
6d05c810 56 $dupeResult = $this->Connector->antiDupe($this->FILE_INFO['XXH']);
52053519
GJ
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'],
6d05c810 70 $this->FILE_INFO['XXH'],
52053519
GJ
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'
6d05c810 91 * 'XXH' => 'example'
52053519
GJ
92 * 'EXTENSION' => 'example'
93 * 'MIME' => 'example'
94 *
95 * ]
96 * 1 => array:2 [▼
97 * 'TEMP_NAME' => 'example'
98 * 'NAME' => 'example'
99 * 'SIZE' => 'example'
6d05c810 100 * 'XXH' => 'example'
52053519
GJ
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 141 case $this->Connector->CONFIG['BLACKLIST_DB']:
6d05c810 142 $this->Connector->checkFileBlacklist($this->FILE_INFO['XXH']);
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 [
6d05c810 173 'hash' => $this->FILE_INFO['XXH'],
52053519
GJ
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']) {
196 $ip = $_SERVER['REMOTE_ADDR'];
197 }
198 $this->fingerPrintInfo = [
199 'timestamp' => time(),
200 'useragent' => $USER_AGENT,
201 'ip' => $ip,
fe5fd1ac 202 'ip_hash' => hash('xxh3', $_SERVER['REMOTE_ADDR'] . $USER_AGENT),
52053519
GJ
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 *
d7cda189 248 * @return string | bool The file extension of the file.
52053519 249 */
834e65f1 250 public function fileExtension(array $file):string | bool
52053519 251 {
8f135020 252 if(str_contains($file['name'], '.')){
52053519 253 $extension = explode('.', $file['name']);
8f135020 254 $dotCount = substr_count($file['name'], '.');
52053519 255 return match ($dotCount) {
52053519
GJ
256 2 => $this->doubleDotExtension($extension),
257 default => end($extension)
258 };
8f135020 259 }
834e65f1 260 return false;
4246dede 261 }
52053519
GJ
262
263 /**
264 * > Check if the file's MIME type is in the blacklist
265 *
266 */
267 public function checkMimeBlacklist():void
268 {
269 if (in_array($this->FILE_INFO['MIME'], $this->Connector->CONFIG['BLOCKED_MIME'])) {
270 $this->Connector->response->error(415, 'Filetype not allowed');
cec6349e 271 }
52053519
GJ
272 }
273
274 /**
275 * > Check if the file extension is in the blacklist
276 *
277 */
278 public function checkExtensionBlacklist():void
279 {
280 if (in_array($this->FILE_INFO['EXTENSION'], $this->Connector->CONFIG['BLOCKED_EXTENSIONS'])) {
281 $this->Connector->response->error(415, 'Filetype not allowed');
cec6349e 282 }
52053519
GJ
283 }
284
285 public function checkNameLength(string $fileName):string
286 {
287 if (strlen($fileName) > 250) {
288 return substr($fileName, 0, 250);
289 } else {
290 return $fileName;
cec6349e 291 }
52053519
GJ
292 }
293
294 /**
295 * Generates a random string of characters, checks if it exists in the database,
296 * and if it does, it generates another one
297 *
298 * @param $extension string The file extension.
299 *
300 * @return string A string
301 */
302 public function generateName(string $extension):string
303 {
304 do {
305 if ($this->Connector->CONFIG['FILES_RETRIES'] === 0) {
306 $this->Connector->response->error(500, 'Gave up trying to find an unused name!');
307 }
308 $NEW_NAME = '';
309 for ($i = 0; $i < $this->Connector->CONFIG['NAME_LENGTH']; $i++) {
310 $index = rand(0, strlen($this->Connector->CONFIG['ID_CHARSET']) - 1);
311 $NEW_NAME .= $this->Connector->CONFIG['ID_CHARSET'][$index];
312 }
834e65f1 313 if ($extension) {
52053519
GJ
314 $NEW_NAME .= '.' . $extension;
315 }
316 } while ($this->Connector->dbCheckNameExists($NEW_NAME));
f0b5e51c 317 return $NEW_NAME;
52053519 318 }
8f135020 319 }