]> jfr.im git - uguu.git/blob - src/Classes/Upload.php
add extension determination when its not provided
[uguu.git] / src / Classes / Upload.php
1 <?php
2 /**
3 * Uguu
4 *
5 * @copyright Copyright (c) 2022-2023 Go Johansson (nokonoko) <neku@pomf.se>
6 *
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.
11 *
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.
16 *
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/>.
19 */
20
21 namespace Pomf\Uguu\Classes;
22
23 class Upload extends Response
24 {
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 }
61 }
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 ];
76 }
77 return $result;
78 }
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 }
114 }
115 return $result;
116 }
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 }
140 // Continue
141 case $this->Connector->CONFIG['BLACKLIST_DB']:
142 $this->Connector->checkFileBlacklist($this->FILE_INFO['SHA1']);
143 // Continue
144 case $this->Connector->CONFIG['FILTER_MODE'] && empty($this->FILE_INFO['EXTENSION']):
145 $this->checkMimeBlacklist();
146 // Continue
147 case $this->Connector->CONFIG['FILTER_MODE'] && !empty($this->FILE_INFO['EXTENSION']):
148 $this->checkMimeBlacklist();
149 $this->checkExtensionBlacklist();
150 // Continue
151 }
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'],
179 ];
180 }
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['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
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 }
208 }
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']);
221 }
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 }
241 }
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) {
255 0 => $this->lookupExtension($file['type']),
256 1 => end($extension),
257 2 => $this->doubleDotExtension($extension),
258 default => end($extension)
259 };
260 }
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');
270 }
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');
281 }
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;
290 }
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));
316 return $NEW_NAME;
317 }
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 }
360 }