diff --git a/package.json b/package.json index 0123851..394fe4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uguu", - "version": "1.6.4", + "version": "1.6.5", "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/src/Classes/Database.php b/src/Classes/Database.php index 848e322..1b8880f 100644 --- a/src/Classes/Database.php +++ b/src/Classes/Database.php @@ -21,8 +21,10 @@ namespace Pomf\Uguu\Classes; + use DateTimeZone; use Exception; use PDO; + use DateTime; class Database { @@ -43,16 +45,21 @@ class Database * * @param $name string The name of the file. * - * @return int The number of rows that match the query. + * @return bool The number of rows that match the query. * @throws \Exception */ - public function dbCheckNameExists(string $name): int + public function dbCheckNameExists(string $name): bool { try { - $q = $this->DB->prepare('SELECT COUNT(filename) FROM files WHERE filename = (:name)'); + $q = $this->DB->prepare('SELECT * FROM files WHERE EXISTS + (SELECT filename FROM files WHERE filename = (:name)) LIMIT 1'); $q->bindValue(':name', $name); $q->execute(); - return $q->fetchColumn(); + $result = $q->fetch(); + if ($result) { + return true; + } + return false; } catch (Exception) { throw new Exception('Cant check if name exists in DB.', 500); } @@ -68,11 +75,12 @@ class Database public function checkFileBlacklist(array $FILE_INFO): void { try { - $q = $this->DB->prepare('SELECT hash, COUNT(*) AS count FROM blacklist WHERE hash = (:hash)'); + $q = $this->DB->prepare('SELECT * FROM blacklist WHERE EXISTS + (SELECT hash FROM blacklist WHERE hash = (:hash)) LIMIT 1'); $q->bindValue(':hash', $FILE_INFO['SHA1']); $q->execute(); $result = $q->fetch(); - if ($result['count'] > 0) { + if ($result) { throw new Exception('File blacklisted!', 415); } } catch (Exception) { @@ -91,12 +99,13 @@ class Database { try { $q = $this->DB->prepare( - 'SELECT filename, COUNT(*) AS count FROM files WHERE hash = (:hash)', + 'SELECT * FROM files WHERE EXISTS + (SELECT filename FROM files WHERE hash = (:hash)) LIMIT 1', ); $q->bindValue(':hash', $hash); $q->execute(); $result = $q->fetch(); - if ($result['count'] > 0) { + if ($result) { return [ 'result' => true, 'name' => $result['filename'], @@ -142,52 +151,81 @@ class Database * Creates a new row in the database with the information provided * * @param $fingerPrintInfo array + * + * @throws \Exception */ 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(); + try { + $q = $this->DB->prepare( + 'INSERT INTO ratelimit (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(); + } catch (Exception $e) { + throw new Exception(500, $e->getMessage()); + } } /** * 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 $iStamp bool A boolean value that determines whether or not to update the timestamp. * @param $fingerPrintInfo array An array containing the following keys: + * + * @throws \Exception */ 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)', - ); + try { + 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(); + } catch (Exception $e) { + throw new Exception(500, $e->getMessage()); } - $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 + * @throws \Exception + */ + public function compareTime(int $timestamp, int $seconds_d): bool + { + $dateTime_end = new DateTime('now', new DateTimeZone('Europe/Stockholm')); + $dateTime_start = new DateTime(); + $dateTime_start->setTimestamp($timestamp); + $diff = strtotime($dateTime_end->format('Y-m-d H:i:s')) - strtotime($dateTime_start->format('Y-m-d H:i:s')); + if ($diff > $seconds_d) { + return true; + } + return false; + } + + /** + * 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. + * @throws \Exception */ - public function checkRateLimit(array $fingerPrintInfo): bool + public function checkRateLimit(array $fingerPrintInfo, int $rateTimeout, int $fileLimit): bool { $q = $this->DB->prepare( 'SELECT files, time, iphash, COUNT(*) AS count FROM ratelimit WHERE iphash = (:iphash)', @@ -195,24 +233,30 @@ class Database $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; + + //If there is no other match a record does not exist, create one. + if (!$result['count'] > 0) { + $this->createRateLimit($fingerPrintInfo); + return false; } + + // Apply rate-limit when file count reached and timeout not reached. + if ($result['files'] === $fileLimit and !$this->compareTime($result['time'], $rateTimeout)) { + return true; + } + + // Update timestamp if timeout reached. + if ($this->compareTime($result['time'], $rateTimeout)) { + $this->updateRateLimit($fingerPrintInfo['files_amount'], true, $fingerPrintInfo); + return false; + } + + // Add filecount, timeout not reached. + if ($result['files'] < $fileLimit and !$this->compareTime($result['time'], $rateTimeout)) { + $this->updateRateLimit($result['files'] + $fingerPrintInfo['files_amount'], false, $fingerPrintInfo); + return false; + } + return false; } } diff --git a/src/Classes/Response.php b/src/Classes/Response.php index 678dff6..6d7a9f7 100644 --- a/src/Classes/Response.php +++ b/src/Classes/Response.php @@ -1,4 +1,5 @@ . */ - + namespace Pomf\Uguu\Classes; - - class Response + +class Response +{ + public string $type; + + /** + * Takes a string as an argument and sets the header to the appropriate content type + * + * @param $response_type string The type of response you want to return. + * Valid options are: csv, html, json, text. + */ + public function __construct(string $response_type) { - public string $type; - - /** - * Takes a string as an argument and sets the header to the appropriate content type - * - * @param $response_type string The type of response you want to return. Valid options are: csv, html, json, text. - */ - public function __construct(string $response_type) - { - switch ($response_type) { - case 'csv': - header('Content-Type: text/csv; charset=UTF-8'); - $this->type = $response_type; - break; - case 'html': - header('Content-Type: text/html; charset=UTF-8'); - $this->type = $response_type; - break; - case 'json': - header('Content-Type: application/json; charset=UTF-8'); - $this->type = $response_type; - break; - case 'gyazo': - header('Content-Type: text/plain; charset=UTF-8'); - $this->type = 'text'; - break; - case 'text': - header('Content-Type: text/plain; charset=UTF-8'); - $this->type = $response_type; - break; - default: - header('Content-Type: application/json; charset=UTF-8'); - $this->type = 'json'; - break; - } - } - - /** - * Returns a string based on the type of response requested - * - * @param $code mixed The HTTP status code to return. - * @param $desc string The description of the error. - */ - public function error(mixed $code, string $desc):void - { - $response = match ($this->type) { - 'csv' => $this->csvError($desc), - 'html' => $this->htmlError($code, $desc), - 'json' => $this->jsonError($code, $desc), - 'text' => $this->textError($code, $desc), - }; - http_response_code($code); - echo $response; - } - - /* Returning a string that contains the error message. */ - private static function csvError(string $description):string - { - return '"error"' . "\r\n" . "\"$description\"" . "\r\n"; - } - - /** - * Returns a string containing an HTML paragraph element with the error code and description - * - * @param $code int|string The error code. - * @param $description string The description of the error. - * - * @return string A string. - */ - private static function htmlError(int|string $code, string $description):string - { - return '

ERROR: (' . $code . ') ' . $description . '

'; - } - - /** - * Returns a JSON string with the error code and description - * - * @param $code int|string The error code. - * @param $description string The description of the error. - * - * @return bool|string A JSON string - */ - private static function jsonError(int|string $code, string $description):bool|string - { - return json_encode([ - 'success' => false, - 'errorcode' => $code, - 'description' => $description, - ], JSON_PRETTY_PRINT); - } - - /** - * Returns a string that contains the error code and description - * - * @param $code int|string The error code. - * @param $description string The description of the error. - * - * @return string A string with the error code and description. - */ - private static function textError(int|string $code, string $description):string - { - return 'ERROR: (' . $code . ') ' . $description; - } - - /** - * "If the type is csv, then call the csvSuccess function, if the type is html, then call the htmlSuccess function, etc." - * - * The `match` keyword is a new feature in PHP 8. It's a lot like a switch statement, but it's more powerful - * - * @param $files array An array of file objects. - */ - public function send(array $files):void - { - $response = match ($this->type) { - 'csv' => $this->csvSuccess($files), - 'html' => $this->htmlSuccess($files), - 'json' => $this->jsonSuccess($files), - 'text' => $this->textSuccess($files), - }; - http_response_code(200); // "200 OK". Success. - echo $response; - } - - /** - * Takes an array of files and returns a CSV string - * - * @param $files array An array of files that have been uploaded. - * - * @return string A string of the files in the array. - */ - private static function csvSuccess(array $files):string - { - $result = '"name","url","hash","size"' . "\r\n"; - foreach ($files as $file) { - $result .= '"' . $file['name'] . '"' . ',' . - '"' . $file['url'] . '"' . ',' . - '"' . $file['hash'] . '"' . ',' . - '"' . $file['size'] . '"' . "\r\n"; - } - return $result; - } - - /** - * Takes an array of files and returns a string of HTML links - * - * @param $files array An array of files to be uploaded. - * - * @return string the result of the foreach loop. - */ - private static function htmlSuccess(array $files):string - { - $result = ''; - foreach ($files as $file) { - $result .= '' . $file['url'] . '
'; - } - return $result; - } - - /** - * Returns a JSON string that contains a success message and the files that were uploaded - * - * @param $files array The files to be uploaded. - * - * @return bool|string A JSON string - */ - private static function jsonSuccess(array $files):bool|string - { - return json_encode([ - 'success' => true, - 'files' => $files, - ], JSON_PRETTY_PRINT); - } - - /** - * Takes an array of files and returns a string of URLs - * - * @param $files array The files to be uploaded. - * - * @return string the url of the file. - */ - private static function textSuccess(array $files):string - { - $result = ''; - foreach ($files as $file) { - $result .= $file['url'] . "\n"; - } - return $result; + switch ($response_type) { + case 'csv': + header('Content-Type: text/csv; charset=UTF-8'); + $this->type = $response_type; + break; + case 'html': + header('Content-Type: text/html; charset=UTF-8'); + $this->type = $response_type; + break; + case 'json': + header('Content-Type: application/json; charset=UTF-8'); + $this->type = $response_type; + break; + case 'gyazo': + header('Content-Type: text/plain; charset=UTF-8'); + $this->type = 'text'; + break; + case 'text': + header('Content-Type: text/plain; charset=UTF-8'); + $this->type = $response_type; + break; + default: + header('Content-Type: application/json; charset=UTF-8'); + $this->type = 'json'; + break; } } + + /** + * Returns a string based on the type of response requested + * + * @param $code mixed The HTTP status code to return. + * @param $desc string The description of the error. + */ + public function error(int $code, string $desc): void + { + $response = match ($this->type) { + 'csv' => $this->csvError($desc), + 'html' => $this->htmlError($code, $desc), + 'json' => $this->jsonError($code, $desc), + 'text' => $this->textError($code, $desc), + }; + http_response_code($code); + echo $response; + } + + /* Returning a string that contains the error message. */ + private static function csvError(string $description): string + { + return '"error"' . "\r\n" . "\"$description\"" . "\r\n"; + } + + /** + * Returns a string containing an HTML paragraph element with the error code and description + * + * @param $code int|string The error code. + * @param $description string The description of the error. + * + * @return string A string. + */ + private static function htmlError(int|string $code, string $description): string + { + return '

ERROR: (' . $code . ') ' . $description . '

'; + } + + /** + * Returns a JSON string with the error code and description + * + * @param $code int|string The error code. + * @param $description string The description of the error. + * + * @return bool|string A JSON string + */ + private static function jsonError(int|string $code, string $description): bool|string + { + return json_encode([ + 'success' => false, + 'errorcode' => $code, + 'description' => $description, + ], JSON_PRETTY_PRINT); + } + + /** + * Returns a string that contains the error code and description + * + * @param $code int|string The error code. + * @param $description string The description of the error. + * + * @return string A string with the error code and description. + */ + private static function textError(int|string $code, string $description): string + { + return 'ERROR: (' . $code . ') ' . $description; + } + + /** + * "If the type is csv, then call the csvSuccess function, + * if the type is html, then call the htmlSuccess function, etc." + * + * The `match` keyword is a new feature in PHP 8. It's a lot like a switch statement, but it's more powerful + * + * @param $files array An array of file objects. + */ + public function send(array $files): void + { + $response = match ($this->type) { + 'csv' => $this->csvSuccess($files), + 'html' => $this->htmlSuccess($files), + 'json' => $this->jsonSuccess($files), + 'text' => $this->textSuccess($files), + }; + http_response_code(200); // "200 OK". Success. + echo $response; + } + + /** + * Takes an array of files and returns a CSV string + * + * @param $files array An array of files that have been uploaded. + * + * @return string A string of the files in the array. + */ + private static function csvSuccess(array $files): string + { + $result = '"name","url","hash","size"' . "\r\n"; + foreach ($files as $file) { + $result .= '"' . $file['name'] . '"' . ',' . + '"' . $file['url'] . '"' . ',' . + '"' . $file['hash'] . '"' . ',' . + '"' . $file['size'] . '"' . "\r\n"; + } + return $result; + } + + /** + * Takes an array of files and returns a string of HTML links + * + * @param $files array An array of files to be uploaded. + * + * @return string the result of the foreach loop. + */ + private static function htmlSuccess(array $files): string + { + $result = ''; + foreach ($files as $file) { + $result .= '' . $file['url'] . '
'; + } + return $result; + } + + /** + * Returns a JSON string that contains a success message and the files that were uploaded + * + * @param $files array The files to be uploaded. + * + * @return bool|string A JSON string + */ + private static function jsonSuccess(array $files): bool|string + { + return json_encode([ + 'success' => true, + 'files' => $files, + ], JSON_PRETTY_PRINT); + } + + /** + * Takes an array of files and returns a string of URLs + * + * @param $files array The files to be uploaded. + * + * @return string the url of the file. + */ + private static function textSuccess(array $files): string + { + $result = ''; + foreach ($files as $file) { + $result .= $file['url'] . "\n"; + } + return $result; + } +} diff --git a/src/Classes/Upload.php b/src/Classes/Upload.php index 3739712..6479912 100644 --- a/src/Classes/Upload.php +++ b/src/Classes/Upload.php @@ -126,7 +126,16 @@ class Upload extends Response { switch (true) { case $this->Connector->CONFIG['RATE_LIMIT']: - $this->Connector->checkRateLimit($this->fingerPrintInfo); + if ( + $this->Connector->checkRateLimit( + $this->fingerPrintInfo, + (int) $this->Connector->CONFIG['RATE_LIMIT_TIMEOUT'], + (int) $this->Connector->CONFIG['RATE_LIMIT_FILES'] + ) + ) { + throw new Exception('Rate limit, please wait ' . $this->Connector->CONFIG['RATE_LIMIT_TIMEOUT'] . + ' seconds before uploading again.', 500); + } // Continue case $this->Connector->CONFIG['BLACKLIST_DB']: $this->Connector->checkFileBlacklist($this->FILE_INFO); @@ -274,7 +283,6 @@ class Upload extends Response * 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 @@ -293,7 +301,7 @@ class Upload extends Response if (!empty($extension)) { $NEW_NAME .= '.' . $extension; } - } while ($this->Connector->dbCheckNameExists($NEW_NAME) > 0); + } while ($this->Connector->dbCheckNameExists($NEW_NAME)); return $NEW_NAME; } } diff --git a/src/config.json b/src/config.json index 801ebd2..39f5839 100755 --- a/src/config.json +++ b/src/config.json @@ -3,7 +3,7 @@ "allowErrors": false }, "dest": "dist", - "pkgVersion": "1.6.4", + "pkgVersion": "1.6.5", "pages": [ "index.ejs", "faq.ejs", @@ -34,6 +34,8 @@ "BLACKLIST_DB": true, "FILTER_MODE": true, "RATE_LIMIT": false, + "RATE_LIMIT_TIMEOUT": 60, + "RATE_LIMIT_FILES": 100, "FILES_ROOT": "/var/www/files/", "FILES_RETRIES": 15, "FILES_URL": "https://files.domain.com", diff --git a/src/static/php/upload.php b/src/static/php/upload.php index b3f38c9..698cf1e 100644 --- a/src/static/php/upload.php +++ b/src/static/php/upload.php @@ -1,8 +1,5 @@ . */ + require_once __DIR__ . '/../vendor/autoload.php'; - + use Pomf\Uguu\Classes\Upload; use Pomf\Uguu\Classes\Response; @@ -40,10 +38,11 @@ */ function handleFile(string $outputFormat, array $files): void { + $fCount = count($files['size']); $upload = new Upload($outputFormat); $files = $upload->reFiles($files); try { - $upload->fingerPrint(count($files)); + $upload->fingerPrint($fCount); $res = []; foreach ($files as $ignored) { $res[] = $upload->uploadFile(); @@ -52,10 +51,10 @@ function handleFile(string $outputFormat, array $files): void $upload->send($res); } } catch (Exception $e) { - $upload->error($e->getCode(), $e->getMessage()); + $upload->error(500, $e->getMessage()); } } - + $response = new Response('json'); if (!isset($_FILES['files']) or empty($_FILES['files'])) {