From f0b5e51c8b74544ee25aeb33070f97b0d7a4fb9d Mon Sep 17 00:00:00 2001 From: Go Johansson Date: Sun, 25 Dec 2022 05:50:55 +0100 Subject: [PATCH] bug fixes --- .php-cs-fixer.cache | 1 + package.json | 2 +- psalm.xml | 15 -- src/Classes/Database.php | 385 +++++++++++++++--------------- src/Classes/Upload.php | 491 ++++++++++++++++++++------------------ src/composer.json | 7 - src/config.json | 9 +- src/static/php/upload.php | 75 +++--- 8 files changed, 514 insertions(+), 471 deletions(-) create mode 100644 .php-cs-fixer.cache delete mode 100644 psalm.xml diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 0000000..de32d8a --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.1.13","version":"3.13.1","indent":" ","lineEnding":"\n","rules":{"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"braces":{"allow_single_line_anonymous_class_with_empty_body":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_typehint":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_braces":true,"no_blank_lines_after_class_opening":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_blank_line_before_namespace":true,"single_import_per_statement":{"group_to_single_imports":false},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_spaces_inside_parenthesis":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"hashes":{"\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder3788\/src\/static\/php\/upload.php":"b626750d2d9ac4f2b2f98a8ec8219533","src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder1142\/src\/static\/php\/upload.php":"b626750d2d9ac4f2b2f98a8ec8219533","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder24\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder1007\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder3242\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder681\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder1535\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder1163\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder4666\/src\/static\/php\/upload.php":"2aa4d70e18ce7445ddd4ba0f76fa0621","\/private\/var\/folders\/5_\/px3cvvbs4rd5bjzl8py2mjnc0000gq\/T\/PHP CS Fixertemp_folder5333\/src\/static\/php\/upload.php":"dc38feb300c1c8ab4614639d5a7af4f7"}} \ No newline at end of file diff --git a/package.json b/package.json index f8aa277..0123851 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uguu", - "version": "1.6.3", + "version": "1.6.4", "description": "Uguu is a simple lightweight temporary file host with support for drop, paste, click and API uploading.", "homepage": "https://uguu.se", "repository": { diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 880bc43..0000000 --- a/psalm.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/src/Classes/Database.php b/src/Classes/Database.php index 52ba22e..848e322 100644 --- a/src/Classes/Database.php +++ b/src/Classes/Database.php @@ -1,4 +1,5 @@ . */ - + namespace Pomf\Uguu\Classes; - + use Exception; use PDO; - - class Database + +class Database +{ + private PDO $DB; + + /** + * Sets the value of the DB variable. + * + * @param $DB PDO The database connection. + */ + public function setDB(PDO $DB): void { - private PDO $DB; - - /** - * Sets the value of the DB variable. - * - * @param $DB PDO The database connection. - */ - public function setDB(PDO $DB):void - { - $this->DB = $DB; - } - - /** - * Checks if a file name exists in the database - * - * @param $name string The name of the file. - * - * @return int The number of rows that match the query. - * @throws \Exception - */ - public function dbCheckNameExists(string $name):int - { - try { - $q = $this->DB->prepare('SELECT COUNT(filename) FROM files WHERE filename = (:name)'); - $q->bindValue(':name', $name); - $q->execute(); - return $q->fetchColumn(); - } - catch (Exception) { - throw new Exception('Cant check if name exists in DB.', 500); - } - } - - /** - * Checks if the file is blacklisted - * - * @param $FILE_INFO array An array containing the following: - * - * @throws \Exception - */ - public function checkFileBlacklist(array $FILE_INFO):void - { - try { - $q = $this->DB->prepare('SELECT hash, COUNT(*) AS count FROM blacklist WHERE hash = (:hash)'); - $q->bindValue(':hash', $FILE_INFO['SHA1']); - $q->execute(); - $result = $q->fetch(); - if ($result['count'] > 0) { - throw new Exception('File blacklisted!', 415); - } - } - catch (Exception) { - throw new Exception('Cant check blacklist DB.', 500); - } - } - - /** - * Checks if the file already exists in the database - * - * @param $hash string The hash of the file you want to check for. - * - * @throws \Exception - */ - public function antiDupe(string $hash):bool|array|string - { - if (!$this->CONFIG['ANTI_DUPE']) { - return true; - } - try { - $q = $this->DB->prepare( - 'SELECT filename, COUNT(*) AS count FROM files WHERE hash = (:hash)', - ); - $q->bindValue(':hash', $hash); - $q->execute(); - $result = $q->fetch(); - if ($result['count'] > 0) { - return $result['filename']; - } else { - return true; - } - } - catch (Exception) { - throw new Exception('Cant check for dupes in DB.', 500); - } - } - - /** - * Inserts a new file into the database - * - * @param $FILE_INFO array - * @param $fingerPrintInfo array - * - * @throws \Exception - */ - public function newIntoDB(array $FILE_INFO, array $fingerPrintInfo):void - { - try { - $q = $this->DB->prepare( - 'INSERT INTO files (hash, originalname, filename, size, date, ip)' . - 'VALUES (:hash, :orig, :name, :size, :date, :ip)', - ); - $q->bindValue(':hash', $FILE_INFO['SHA1']); - $q->bindValue(':orig', $FILE_INFO['NAME']); - $q->bindValue(':name', $FILE_INFO['NEW_NAME']); - $q->bindValue(':size', $FILE_INFO['SIZE'], PDO::PARAM_INT); - $q->bindValue(':date', $fingerPrintInfo['timestamp']); - $q->bindValue(':ip', $fingerPrintInfo['ip']); - $q->execute(); - } - catch (Exception) { - throw new Exception('Cant insert into DB.', 500); - } - } - - /** - * Creates a new row in the database with the information provided - * - * @param $fingerPrintInfo array - */ - public function createRateLimit(array $fingerPrintInfo):void - { - $q = $this->DB->prepare( - 'INSERT INTO timestamp (iphash, files, time)' . - 'VALUES (:iphash, :files, :time)', - ); - $q->bindValue(':iphash', $fingerPrintInfo['ip_hash']); - $q->bindValue(':files', $fingerPrintInfo['files_amount']); - $q->bindValue(':time', $fingerPrintInfo['timestamp']); + $this->DB = $DB; + } + + /** + * Checks if a file name exists in the database + * + * @param $name string The name of the file. + * + * @return int The number of rows that match the query. + * @throws \Exception + */ + public function dbCheckNameExists(string $name): int + { + try { + $q = $this->DB->prepare('SELECT COUNT(filename) FROM files WHERE filename = (:name)'); + $q->bindValue(':name', $name); $q->execute(); - } - - /** - * Update the rate limit table with the new file count and timestamp - * - * @param $fCount int The number of files uploaded by the user. - * @param $iStamp boolean A boolean value that determines whether or not to update the timestamp. - * @param $fingerPrintInfo array An array containing the following keys: - */ - public function updateRateLimit(int $fCount, bool $iStamp, array $fingerPrintInfo):void - { - if ($iStamp) { - $q = $this->DB->prepare( - 'UPDATE ratelimit SET files = (:files), time = (:time) WHERE iphash = (:iphash)', - ); - $q->bindValue(':time', $fingerPrintInfo['timestamp']); - } else { - $q = $this->DB->prepare( - 'UPDATE ratelimit SET files = (:files) WHERE iphash = (:iphash)', - ); - } - $q->bindValue(':files', $fCount); - $q->bindValue(':iphash', $fingerPrintInfo['ip_hash']); - $q->execute(); - } - - /** - * Checks if the user has uploaded more than 100 files in the last minute, if so it returns true, if not it updates the database with the new file - * count and timestamp - * - * @param $fingerPrintInfo array An array containing the following: - * - * @return bool A boolean value. - */ - public function checkRateLimit(array $fingerPrintInfo):bool - { - $q = $this->DB->prepare( - 'SELECT files, time, iphash, COUNT(*) AS count FROM ratelimit WHERE iphash = (:iphash)', - ); - $q->bindValue(':iphash', $fingerPrintInfo['ip_hash']); - $q->execute(); - $result = $q->fetch(); - $nTime = $fingerPrintInfo['timestamp'] - (60); - switch (true) { - //If more then 100 files trigger rate-limit - case $result['files'] > 100: - return true; - //if timestamp is older than one minute, set new files count and timestamp - case $result['time'] < $nTime: - $this->updateRateLimit($fingerPrintInfo['files_amount'], true, $fingerPrintInfo); - break; - //if timestamp isn't older than one-minute update the files count - case $result['time'] > $nTime: - $this->updateRateLimit($fingerPrintInfo['files_amount'] + $result['files'], false, $fingerPrintInfo); - break; - //If there is no other match a record does not exist, create one - default: - $this->createRateLimit($fingerPrintInfo); - break; - } - return false; + return $q->fetchColumn(); + } catch (Exception) { + throw new Exception('Cant check if name exists in DB.', 500); } } + + /** + * Checks if the file is blacklisted + * + * @param $FILE_INFO array An array containing the following: + * + * @throws \Exception + */ + public function checkFileBlacklist(array $FILE_INFO): void + { + try { + $q = $this->DB->prepare('SELECT hash, COUNT(*) AS count FROM blacklist WHERE hash = (:hash)'); + $q->bindValue(':hash', $FILE_INFO['SHA1']); + $q->execute(); + $result = $q->fetch(); + if ($result['count'] > 0) { + throw new Exception('File blacklisted!', 415); + } + } catch (Exception) { + throw new Exception('Cant check blacklist DB.', 500); + } + } + + /** + * Checks if the file already exists in the database + * + * @param $hash string The hash of the file you want to check for. + * + * @throws \Exception + */ + public function antiDupe(string $hash): array + { + try { + $q = $this->DB->prepare( + 'SELECT filename, COUNT(*) AS count FROM files WHERE hash = (:hash)', + ); + $q->bindValue(':hash', $hash); + $q->execute(); + $result = $q->fetch(); + if ($result['count'] > 0) { + return [ + 'result' => true, + 'name' => $result['filename'], + ]; + } else { + return [ + 'result' => false + ]; + } + } catch (Exception) { + throw new Exception('Cant check for dupes in DB.', 500); + } + } + + /** + * Inserts a new file into the database + * + * @param $FILE_INFO array + * @param $fingerPrintInfo array + * + * @throws \Exception + */ + public function newIntoDB(array $FILE_INFO, array $fingerPrintInfo): void + { + try { + $q = $this->DB->prepare( + 'INSERT INTO files (hash, originalname, filename, size, date, ip)' . + 'VALUES (:hash, :orig, :name, :size, :date, :ip)', + ); + $q->bindValue(':hash', $FILE_INFO['SHA1']); + $q->bindValue(':orig', $FILE_INFO['NAME']); + $q->bindValue(':name', $FILE_INFO['NEW_NAME']); + $q->bindValue(':size', $FILE_INFO['SIZE'], PDO::PARAM_INT); + $q->bindValue(':date', $fingerPrintInfo['timestamp']); + $q->bindValue(':ip', $fingerPrintInfo['ip']); + $q->execute(); + } catch (Exception) { + throw new Exception('Cant insert into DB.', 500); + } + } + + /** + * Creates a new row in the database with the information provided + * + * @param $fingerPrintInfo array + */ + public function createRateLimit(array $fingerPrintInfo): void + { + $q = $this->DB->prepare( + 'INSERT INTO timestamp (iphash, files, time)' . + 'VALUES (:iphash, :files, :time)', + ); + $q->bindValue(':iphash', $fingerPrintInfo['ip_hash']); + $q->bindValue(':files', $fingerPrintInfo['files_amount']); + $q->bindValue(':time', $fingerPrintInfo['timestamp']); + $q->execute(); + } + + /** + * Update the rate limit table with the new file count and timestamp + * + * @param $fCount int The number of files uploaded by the user. + * @param $iStamp boolean A boolean value that determines whether or not to update the timestamp. + * @param $fingerPrintInfo array An array containing the following keys: + */ + public function updateRateLimit(int $fCount, bool $iStamp, array $fingerPrintInfo): void + { + if ($iStamp) { + $q = $this->DB->prepare( + 'UPDATE ratelimit SET files = (:files), time = (:time) WHERE iphash = (:iphash)', + ); + $q->bindValue(':time', $fingerPrintInfo['timestamp']); + } else { + $q = $this->DB->prepare( + 'UPDATE ratelimit SET files = (:files) WHERE iphash = (:iphash)', + ); + } + $q->bindValue(':files', $fCount); + $q->bindValue(':iphash', $fingerPrintInfo['ip_hash']); + $q->execute(); + } + + /** + * Checks if the user has uploaded more than 100 files in the last minute, if so it returns true, if not it updates the database with the new file + * count and timestamp + * + * @param $fingerPrintInfo array An array containing the following: + * + * @return bool A boolean value. + */ + public function checkRateLimit(array $fingerPrintInfo): bool + { + $q = $this->DB->prepare( + 'SELECT files, time, iphash, COUNT(*) AS count FROM ratelimit WHERE iphash = (:iphash)', + ); + $q->bindValue(':iphash', $fingerPrintInfo['ip_hash']); + $q->execute(); + $result = $q->fetch(); + $nTime = $fingerPrintInfo['timestamp'] - (60); + switch (true) { + //If more then 100 files trigger rate-limit + case $result['files'] > 100: + return true; + //if timestamp is older than one minute, set new files count and timestamp + case $result['time'] < $nTime: + $this->updateRateLimit($fingerPrintInfo['files_amount'], true, $fingerPrintInfo); + break; + //if timestamp isn't older than one-minute update the files count + case $result['time'] > $nTime: + $this->updateRateLimit($fingerPrintInfo['files_amount'] + $result['files'], false, $fingerPrintInfo); + break; + //If there is no other match a record does not exist, create one + default: + $this->createRateLimit($fingerPrintInfo); + break; + } + return false; + } +} diff --git a/src/Classes/Upload.php b/src/Classes/Upload.php index 7c2f697..3739712 100644 --- a/src/Classes/Upload.php +++ b/src/Classes/Upload.php @@ -1,4 +1,5 @@ . */ - + namespace Pomf\Uguu\Classes; - + use Exception; - - class Upload extends Response + +class Upload extends Response +{ + public array $FILE_INFO; + public array $fingerPrintInfo; + private mixed $Connector; + + /** + * Takes an array of files, and returns an array of arrays containing the file's temporary name, + * name, size, SHA1 hash, extension, and MIME type + * + * @param $files array The files array from the $_FILES superglobal. + * + * @return array An array of arrays. + * @throws \Exception + */ + public function reFiles(array $files): array { - public array $FILE_INFO; - public array $fingerPrintInfo; - private mixed $Connector; - - /** - * Takes an array of files, and returns an array of arrays containing the file's temporary name, name, size, SHA1 hash, extension, and MIME type - * - * @param $files array The files array from the $_FILES superglobal. - * - * @return array An array of arrays. - * @throws \Exception - */ - public function reFiles(array $files):array - { - $this->Connector = new Connector(); - $this->Connector->setDB($this->Connector->DB); - $result = []; - $files = $this->diverseArray($files); - foreach ($files as $file) { - $hash = sha1_file($file['tmp_name']); - $this->FILE_INFO = [ - 'TEMP_NAME' => $file['tmp_name'], - 'NAME' => strip_tags($file['name']), - 'SIZE' => $file['size'], - 'SHA1' => $hash, - 'EXTENSION' => $this->fileExtension($file), - 'MIME' => $this->fileMIME($file), - 'NEW_NAME' => $this->generateName($this->fileExtension($file), $hash), - ]; - $result[] = [ - $this->FILE_INFO['TEMP_NAME'], - $this->FILE_INFO['NAME'], - $this->FILE_INFO['SIZE'], - $this->FILE_INFO['SHA1'], - $this->FILE_INFO['EXTENSION'], - $this->FILE_INFO['MIME'], - ]; - } - return $result; - } - - /** - * Takes an array of arrays and returns an array of arrays with the keys and values swapped - * - * @param $files array an array of arrays - * - * @return array ``` - * array:2 [▼ - * 0 => array:2 [▼ - * 'TEMP_NAME' => 'example' - * 'NAME' => 'example' - * 'SIZE' => 'example' - * 'SHA1' => 'example' - * 'EXTENSION' => 'example' - * 'MIME' => 'example' - * - * ] - * 1 => array:2 [▼ - * 'TEMP_NAME' => 'example' - * 'NAME' => 'example' - * 'SIZE' => 'example' - * 'SHA1' => 'example' - * 'EXTENSION' => 'example' - * 'MIME' => 'example' - * ] - * ] - * ``` - */ - public function diverseArray(array $files):array - { - $result = []; - foreach ($files as $key1 => $value1) { - foreach ($value1 as $key2 => $value2) { - $result[$key2][$key1] = $value2; + $this->Connector = new Connector(); + $this->Connector->setDB($this->Connector->DB); + $result = []; + $files = $this->diverseArray($files); + foreach ($files as $file) { + $hash = sha1_file($file['tmp_name']); + $this->FILE_INFO = [ + 'TEMP_NAME' => $file['tmp_name'], + 'NAME' => strip_tags($file['name']), + 'SIZE' => $file['size'], + 'SHA1' => $hash, + 'EXTENSION' => $this->fileExtension($file), + 'MIME' => $this->fileMIME($file), + ]; + + if ($this->Connector->CONFIG['ANTI_DUPE']) { + $dupeResult = $this->Connector->antiDupe($hash); + if ($dupeResult['result']) { + $this->FILE_INFO['NEW_NAME'] = $dupeResult['name']; } } - return $result; - } - - /** - * Takes a file, checks if it's blacklisted, moves it to the file storage, and then logs it to the database - * - * @return array An array containing the hash, name, url, and size of the file. - * @throws \Exception - */ - public function uploadFile():array - { - if ($this->Connector->CONFIG['RATE_LIMIT']) { - $this->Connector->checkRateLimit($this->fingerPrintInfo); + + if (!isset($this->FILE_INFO['NEW_NAME'])) { + $this->FILE_INFO['NEW_NAME'] = $this->generateName($this->FILE_INFO['EXTENSION']); } - if ($this->Connector->CONFIG['BLACKLIST_DB']) { - $this->Connector->checkFileBlacklist($this->FILE_INFO); - } - if ($this->Connector->CONFIG['FILTER_MODE'] && empty($this->FILE_INFO['EXTENSION'])) { - $this->checkMimeBlacklist(); - } - if ($this->Connector->CONFIG['FILTER_MODE'] && !empty($this->FILE_INFO['EXTENSION'])) { - $this->checkMimeBlacklist(); - $this->checkExtensionBlacklist(); - } - if (!is_dir($this->Connector->CONFIG['FILES_ROOT'])) { - throw new Exception('File storage path not accessible.', 500); - } - if ( - !move_uploaded_file( - $this->FILE_INFO['TEMP_NAME'], - $this->Connector->CONFIG['FILES_ROOT'] . - $this->FILE_INFO['NEW_NAME'], - ) - ) { - throw new Exception('Failed to move file to destination', 500); - } - if (!chmod($this->Connector->CONFIG['FILES_ROOT'] . $this->FILE_INFO['NEW_NAME'], 0644)) { - throw new Exception('Failed to change file permissions', 500); - } - if (!$this->Connector->CONFIG['LOG_IP']) { - $this->fingerPrintInfo['ip'] = null; - } - $this->Connector->newIntoDB($this->FILE_INFO, $this->fingerPrintInfo); - return [ - 'hash' => $this->FILE_INFO['SHA1'], - 'name' => $this->FILE_INFO['NAME'], - 'url' => $this->Connector->CONFIG['FILES_URL'] . '/' . $this->FILE_INFO['NEW_NAME'], - 'size' => $this->FILE_INFO['SIZE'], + + $result[] = [ + $this->FILE_INFO['TEMP_NAME'], + $this->FILE_INFO['NAME'], + $this->FILE_INFO['SIZE'], + $this->FILE_INFO['SHA1'], + $this->FILE_INFO['EXTENSION'], + $this->FILE_INFO['MIME'], ]; } - - /** - * Takes the amount of files that are being uploaded, and creates a fingerprint of the user's IP address, user agent, and the amount of files being - * uploaded - * - * @param $files_amount int The amount of files that are being uploaded. - * - * @throws \Exception - */ - public function fingerPrint(int $files_amount):void - { - if (!empty($_SERVER['HTTP_USER_AGENT'])) { - $USER_AGENT = filter_var($_SERVER['HTTP_USER_AGENT'], FILTER_SANITIZE_ENCODED); - $this->fingerPrintInfo = [ - 'timestamp' => time(), - 'useragent' => $USER_AGENT, - 'ip' => $_SERVER['REMOTE_ADDR'], - 'ip_hash' => hash('sha1', $_SERVER['REMOTE_ADDR'] . $USER_AGENT), - 'files_amount' => $files_amount, - ]; - } else { - throw new Exception('Invalid user agent.', 500); + return $result; + } + + /** + * Takes an array of arrays and returns an array of arrays with the keys and values swapped + * + * @param $files array an array of arrays + * + * @return array ``` + * array:2 [▼ + * 0 => array:2 [▼ + * 'TEMP_NAME' => 'example' + * 'NAME' => 'example' + * 'SIZE' => 'example' + * 'SHA1' => 'example' + * 'EXTENSION' => 'example' + * 'MIME' => 'example' + * + * ] + * 1 => array:2 [▼ + * 'TEMP_NAME' => 'example' + * 'NAME' => 'example' + * 'SIZE' => 'example' + * 'SHA1' => 'example' + * 'EXTENSION' => 'example' + * 'MIME' => 'example' + * ] + * ] + * ``` + */ + public function diverseArray(array $files): array + { + $result = []; + foreach ($files as $key1 => $value1) { + foreach ($value1 as $key2 => $value2) { + $result[$key2][$key1] = $value2; } } - - /** - * Returns the MIME type of a file - * - * @param $file array The file to be checked. - * - * @return string The MIME type of the file. - */ - public function fileMIME(array $file):string - { - $FILE_INFO = finfo_open(FILEINFO_MIME_TYPE); - return finfo_file($FILE_INFO, $file['tmp_name']); + return $result; + } + + /** + * Takes a file, checks if it's blacklisted, moves it to the file storage, and then logs it to the database + * + * @return array An array containing the hash, name, url, and size of the file. + * @throws \Exception + */ + public function uploadFile(): array + { + switch (true) { + case $this->Connector->CONFIG['RATE_LIMIT']: + $this->Connector->checkRateLimit($this->fingerPrintInfo); + // Continue + case $this->Connector->CONFIG['BLACKLIST_DB']: + $this->Connector->checkFileBlacklist($this->FILE_INFO); + // Continue + case $this->Connector->CONFIG['FILTER_MODE'] && empty($this->FILE_INFO['EXTENSION']): + $this->checkMimeBlacklist(); + // Continue + case $this->Connector->CONFIG['FILTER_MODE'] && !empty($this->FILE_INFO['EXTENSION']): + $this->checkMimeBlacklist(); + $this->checkExtensionBlacklist(); + // Continue + case !$this->Connector->CONFIG['LOG_IP']: + $this->fingerPrintInfo['ip'] = null; + // Continue } - - /** - * Takes a file and returns the file extension - * - * @param $file array The file you want to get the extension from. - * - * @return ?string The file extension of the file. - */ - public function fileExtension(array $file):?string - { - $extension = explode('.', $file['name']); - if (substr_count($file['name'], '.') > 0) { - return end($extension); - } else { - return null; - } + if (!is_dir($this->Connector->CONFIG['FILES_ROOT'])) { + throw new Exception('File storage path not accessible.', 500); } - - /** - * > Check if the file's MIME type is in the blacklist - * - * @throws \Exception - */ - public function checkMimeBlacklist():void - { - if (in_array($this->FILE_INFO['MIME'], $this->Connector->CONFIG['BLOCKED_MIME'])) { - throw new Exception('Filetype not allowed.', 415); - } + if ( + !move_uploaded_file( + $this->FILE_INFO['TEMP_NAME'], + $this->Connector->CONFIG['FILES_ROOT'] . + $this->FILE_INFO['NEW_NAME'], + ) + ) { + throw new Exception('Failed to move file to destination', 500); } - - /** - * > Check if the file extension is in the blacklist - * - * @throws \Exception - */ - public function checkExtensionBlacklist():void - { - if (in_array($this->FILE_INFO['EXTENSION'], $this->Connector->CONFIG['BLOCKED_EXTENSIONS'])) { - throw new Exception('Filetype not allowed.', 415); - } + if (!chmod($this->Connector->CONFIG['FILES_ROOT'] . $this->FILE_INFO['NEW_NAME'], 0644)) { + throw new Exception('Failed to change file permissions', 500); } - - /** - * Generates a random string of characters, checks if it exists in the database, and if it does, it generates another one - * - * @param $extension string The file extension. - * @param $hash string The hash of the file. - * - * @return string A string - * @throws \Exception - */ - public function generateName(string $extension, string $hash):string - { - if ($this->Connector->antiDupe($hash)) { - do { - if ($this->Connector->CONFIG['FILES_RETRIES'] === 0) { - throw new Exception('Gave up trying to find an unused name!', 500); - } - $NEW_NAME = ''; - $count = strlen($this->Connector->CONFIG['ID_CHARSET']); - while ($this->Connector->CONFIG['NAME_LENGTH']--) { - $NEW_NAME .= $this->Connector->CONFIG['ID_CHARSET'][mt_rand(0, $count - 1)]; - } - if (!empty($extension)) { - $NEW_NAME .= '.' . $extension; - } - } while ($this->Connector->dbCheckNameExists($NEW_NAME) > 0); - return $NEW_NAME; - } else { - return $this->Connector->antiDupe($hash); - } + + + + $this->Connector->newIntoDB($this->FILE_INFO, $this->fingerPrintInfo); + return [ + 'hash' => $this->FILE_INFO['SHA1'], + 'name' => $this->FILE_INFO['NAME'], + 'url' => $this->Connector->CONFIG['FILES_URL'] . '/' . $this->FILE_INFO['NEW_NAME'], + 'size' => $this->FILE_INFO['SIZE'], + ]; + } + + /** + * Takes the amount of files that are being uploaded, and creates a fingerprint of the user's IP address, + * user agent, and the amount of files being + * uploaded + * + * @param $files_amount int The amount of files that are being uploaded. + * + * @throws \Exception + */ + public function fingerPrint(int $files_amount): void + { + if (!empty($_SERVER['HTTP_USER_AGENT'])) { + $USER_AGENT = filter_var($_SERVER['HTTP_USER_AGENT'], FILTER_SANITIZE_ENCODED); + $this->fingerPrintInfo = [ + 'timestamp' => time(), + 'useragent' => $USER_AGENT, + 'ip' => $_SERVER['REMOTE_ADDR'], + 'ip_hash' => hash('sha1', $_SERVER['REMOTE_ADDR'] . $USER_AGENT), + 'files_amount' => $files_amount, + ]; + } else { + throw new Exception('Invalid user agent.', 500); } } + + /** + * Returns the MIME type of a file + * + * @param $file array The file to be checked. + * + * @return string The MIME type of the file. + */ + public function fileMIME(array $file): string + { + $FILE_INFO = finfo_open(FILEINFO_MIME_TYPE); + return finfo_file($FILE_INFO, $file['tmp_name']); + } + + /** + * It takes an array of strings, and returns the last two strings joined by a dot, + * unless the last two strings are in the array of strings in the + * `DOUBLE_DOTS_EXTENSIONS` config variable, in which case it returns the last string + * + * @param $extension array The extension of the file. + * + * @return string The last two elements of the array are joined together and returned. + */ + public function doubleDotExtension(array $extension): string + { + $doubleDotArray = array_slice($extension, -2, 2); + $doubleDot = strtolower(preg_replace('/[^a-zA-Z.]/', '', join('.', $doubleDotArray))); + if (in_array($doubleDot, $this->Connector->CONFIG['DOUBLE_DOTS_EXTENSIONS'])) { + return $doubleDot; + } else { + return end($extension); + } + } + + /** + * Takes a file and returns the file extension + * + * @param $file array The file you want to get the extension from. + * + * @return string The file extension of the file. + */ + public function fileExtension(array $file): string + { + $extension = explode('.', $file['name']); + $dotCount = substr_count($file['name'], '.'); + return match ($dotCount) { + 0 => null, + 1 => end($extension), + 2 => $this->doubleDotExtension($extension) + }; + } + + /** + * > Check if the file's MIME type is in the blacklist + * + * @throws \Exception + */ + public function checkMimeBlacklist(): void + { + if (in_array($this->FILE_INFO['MIME'], $this->Connector->CONFIG['BLOCKED_MIME'])) { + throw new Exception('Filetype not allowed.', 415); + } + } + + /** + * > Check if the file extension is in the blacklist + * + * @throws \Exception + */ + public function checkExtensionBlacklist(): void + { + if (in_array($this->FILE_INFO['EXTENSION'], $this->Connector->CONFIG['BLOCKED_EXTENSIONS'])) { + throw new Exception('Filetype not allowed.', 415); + } + } + + /** + * Generates a random string of characters, checks if it exists in the database, + * and if it does, it generates another one + * + * @param $extension string The file extension. + * @param $hash string The hash of the file. + * + * @return string A string + * @throws \Exception + */ + public function generateName(string $extension): string + { + do { + if ($this->Connector->CONFIG['FILES_RETRIES'] === 0) { + throw new Exception('Gave up trying to find an unused name!', 500); + } + $NEW_NAME = ''; + $count = strlen($this->Connector->CONFIG['ID_CHARSET']); + while ($this->Connector->CONFIG['NAME_LENGTH']--) { + $NEW_NAME .= $this->Connector->CONFIG['ID_CHARSET'][mt_rand(0, $count - 1)]; + } + if (!empty($extension)) { + $NEW_NAME .= '.' . $extension; + } + } while ($this->Connector->dbCheckNameExists($NEW_NAME) > 0); + return $NEW_NAME; + } +} diff --git a/src/composer.json b/src/composer.json index 4154339..ea1b94d 100644 --- a/src/composer.json +++ b/src/composer.json @@ -20,13 +20,6 @@ "ext-fileinfo": "*", "ext-pdo": "*" }, - "require-dev": { - "phpunit/phpunit": "^9.5.27", - "squizlabs/php_codesniffer": "^3.7.1", - "phpmd/phpmd": "^2.13.0", - "phpstan/phpstan": "^1.9.3", - "vimeo/psalm": "^5.2.0" - }, "config": { "optimize-autoloader": true, "classmap-authoritative": true diff --git a/src/config.json b/src/config.json index b129ff7..801ebd2 100755 --- a/src/config.json +++ b/src/config.json @@ -3,7 +3,7 @@ "allowErrors": false }, "dest": "dist", - "pkgVersion": "1.6.3", + "pkgVersion": "1.6.4", "pages": [ "index.ejs", "faq.ejs", @@ -39,6 +39,13 @@ "FILES_URL": "https://files.domain.com", "NAME_LENGTH": 8, "ID_CHARSET": "abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ", + "DOUBLE_DOTS_EXTENSIONS": [ + "tar.bz2", + "tar.gz", + "min.css", + "min.js", + "conf.bak" + ], "BLOCKED_EXTENSIONS": [ "exe", "scr", diff --git a/src/static/php/upload.php b/src/static/php/upload.php index ee6048e..b3f38c9 100644 --- a/src/static/php/upload.php +++ b/src/static/php/upload.php @@ -1,4 +1,8 @@ . */ - require_once __DIR__ . '/../vendor/autoload.php'; use Pomf\Uguu\Classes\Upload; use Pomf\Uguu\Classes\Response; - - function handleFile(string $outputFormat, array $files) - { - $upload = new Upload($outputFormat); - $files = $upload->reFiles($files); - try { - $upload->fingerPrint(count($files)); - $res = []; - foreach ($files as $ignored) { - $res[] = $upload->uploadFile(); - } - if (!empty($res)) { - $upload->send($res); - } - } catch (Exception $e) { - $upload->error($e->getCode(), $e->getMessage()); + + /** + * It takes a string and an array as arguments, creates a new Upload object, + * calls the reFiles method on the Upload object, calls the fingerPrint method on + * the Upload object, calls the uploadFile method on the Upload object, + * calls the send method on the Upload object, and calls the error method on the + * Upload object + * + * @param $outputFormat string The format of the output, json or xml + * @param $files array The file to be uploaded, which is an array. + * + * @throws \Exception + */ +function handleFile(string $outputFormat, array $files): void +{ + $upload = new Upload($outputFormat); + $files = $upload->reFiles($files); + try { + $upload->fingerPrint(count($files)); + $res = []; + foreach ($files as $ignored) { + $res[] = $upload->uploadFile(); } + if (!empty($res)) { + $upload->send($res); + } + } catch (Exception $e) { + $upload->error($e->getCode(), $e->getMessage()); } +} - if (!isset($_FILES['files']) or empty($_FILES['files'])) { - $response = new Response('json'); - $response->error(400, 'No input file(s)'); - } - if (isset($_GET['output']) and !empty($_GET['output'])) { - $resType = strtolower(preg_replace('/[^a-zA-Z]/', '', $_GET['output'])); - } else { - $resType = 'json'; - } - handleFile($resType, $_FILES['files']); \ No newline at end of file + $response = new Response('json'); + +if (!isset($_FILES['files']) or empty($_FILES['files'])) { + $response->error(400, 'No input file(s)'); +} +if (isset($_GET['output']) and !empty($_GET['output'])) { + $resType = strtolower(preg_replace('/[^a-zA-Z]/', '', $_GET['output'])); +} else { + $resType = 'json'; +} + +try { + handleFile($resType, $_FILES['files']); +} catch (Exception $e) { + $response->error($e->getCode(), $e->getMessage()); +}