commit 0f23bb2106c593cacd86e6dda72c351abc9210e6 Author: flashwave Date: Fri May 8 22:53:21 2020 +0000 Import EEPROM. diff --git a/.debug b/.debug new file mode 100644 index 0000000..e69de29 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74737cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +public/data/* +public/thumb/* +config.ini diff --git a/_manage.php b/_manage.php new file mode 100644 index 0000000..7023f69 --- /dev/null +++ b/_manage.php @@ -0,0 +1,456 @@ + 0) + User::byId($mszUserId)->setActive(); + } + + if(!User::hasActive() || User::active()->getId() !== 1) { + http_response_code(403); + header('Content-Type: text/html; charset=utf-8'); + echo << + + + + Access Denied + + + +

Access Denied

+

Access Denied

+ + +HTML; + exit; + } +} + +if($reqPath === '/flash') { + mszDieIfNotAuth(); + header('Content-Type: text/html; charset=utf-8'); + + echo << + + + + + EEPROM Flash + + + + +
+
+

EEPROM Flash

+ +
+
+
+ + + +
+ + + +HTML; + return true; +} + +if($reqPath === '/flash/stats.json') { + mszDieIfNotAuth(); + header('Content-Type: application/json; charset=utf-8'); + $getStats = DB::prepare(' + SELECT ( + SELECT COUNT(`app_id`) + FROM `prm_applications` + ) AS `applications`, ( + SELECT COUNT(`upload_id`) + FROM `prm_uploads` + ) AS `uploads`, ( + SELECT COUNT(`user_id`) + FROM `prm_users` + ) AS `users` + '); + $getStats->execute(); + echo json_encode($getStats->fetchObject()); + return true; +} + +if($reqPath === '/flash/users.json') { + mszDieIfNotAuth(); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(User::all(10, intval(filter_input(INPUT_GET, 'after', FILTER_SANITIZE_NUMBER_INT)))); + return true; +} +if($reqPath === '/flash/uploads.json') { + mszDieIfNotAuth(); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(Upload::all(20, strval(filter_input(INPUT_GET, 'after')))); + return true; +} +if($reqPath === '/flash/applications.json') { + mszDieIfNotAuth(); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(Application::all(10, intval(filter_input(INPUT_GET, 'after', FILTER_SANITIZE_NUMBER_INT)))); + return true; +} + +if($reqPath === '/flash/style.css') { + mszDieIfNotAuth(); + header('Content-Type: text/css; charset=utf-8'); + echo << + + +
+    ________________  ____  ____  __  ___
+   / ____/ ____/ __ \/ __ \/ __ \/  |/  /
+  / __/ / __/ / /_/ / /_/ / / / / /|_/ /
+ / /___/ /___/ ____/ _, _/ /_/ / /  / /
+/_____/_____/_/   /_/ |_|\____/_/  /_/
+
+Currently serving {$totalSizeFmt} ({$totalSize} bytes) of {$fileCount} files in {$uniqueTypes} unique file types from {$userCount} users.
+
+ + +HTML; + return; + } + + echo <<isRestricted()) { + http_response_code(403); + return; + } + + if(empty($_FILES['file']['tmp_name']) || !is_file($_FILES['file']['tmp_name'])) { + http_response_code(400); + return; + } + + $maxFileSize = $appInfo->getSizeLimit(); + if($appInfo->allowSizeMultiplier()) + $maxFileSize *= $userInfo->getSizeMultiplier(); + + $fileSize = filesize($_FILES['file']['tmp_name']); + + if($_FILES['file']['size'] !== $fileSize || $fileSize > $maxFileSize) { + http_response_code(413); + header('Access-Control-Expose-Headers: X-EEPROM-Max-Size'); + header('X-EEPROM-Max-Size: ' . $maxFileSize); + return; + } + + $hash = hash_file('sha256', $_FILES['file']['tmp_name']); + $fileInfo = Upload::byHash($hash); + + if($fileInfo !== null) { + if($fileInfo->isDMCA()) { + http_response_code(451); + return; + } + + if($fileInfo->getUserId() !== $userInfo->getId() + || $fileInfo->getApplicationId() !== $appInfo->getId()) + unset($fileInfo); + } + + if(!empty($fileInfo)) { + if($fileInfo->isDeleted()) + $fileInfo->restore(); + } else { + try { + $fileInfo = Upload::create( + $appInfo, $userInfo, + $_FILES['file']['name'], + mime_content_type($_FILES['file']['tmp_name']), + $fileSize, $hash, + $appInfo->getExpiry(), true + ); + } catch(UploadCreationFailedException $ex) { + http_response_code(500); + return; + } + + if(!move_uploaded_file($_FILES['file']['tmp_name'], $fileInfo->getPath())) { + http_response_code(500); + return; + } + } + + http_response_code(201); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($fileInfo); + return; +} + +if(is_file('../_manage.php') && include_once '../_manage.php') + return; + +http_response_code(404); diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 0000000..821b279 --- /dev/null +++ b/src/Application.php @@ -0,0 +1,89 @@ +app_id ?? 0; + } + + public function getName(): string { + return $this->app_name ?? ''; + } + + public function getCreated(): int { + return $this->app_created ?? 0; + } + + public function getSizeLimit(): int { + return $this->app_size_limit ?? -1; + } + + public function allowSizeMultiplier(): bool { + return !empty($this->app_allow_size_multiplier); + } + + public function getExpiry(): int { + return $this->app_expiry ?? -1; + } + + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'name' => $this->getName(), + 'size_limit' => $this->getSizeLimit(), + 'size_multi' => $this->allowSizeMultiplier(), + 'expiry' => $this->getExpiry(), + 'created' => date('c', $this->getCreated()), + ]; + } + + public static function byId(int $appId): self { + if($appId < 1) + throw new ApplicationNotFoundException; + + $getApplication = DB::prepare(' + SELECT `app_id`, `app_name`, `app_size_limit`, `app_expiry`, `app_allow_size_multiplier`, + UNIX_TIMESTAMP(`app_created`) AS `app_created` + FROM `prm_applications` + WHERE `app_id` = :app + '); + $getApplication->bindValue('app', $appId); + $getApplication->execute(); + $application = $getApplication->fetchObject(self::class); + + if($application === false) + throw new ApplicationNotFoundException; + + return $application; + } + + public static function all(int $limit = 0, int $after = 0): array { + $query = ' + SELECT `app_id`, `app_name`, `app_size_limit`, `app_expiry`, `app_allow_size_multiplier`, + UNIX_TIMESTAMP(`app_created`) AS `app_created` + FROM `prm_applications` + '; + + if($after > 0) + $query .= sprintf(' WHERE `app_id` > %d', $after); + + $query .= ' ORDER BY `app_id`'; + + if($limit > 0) + $query .= sprintf(' LIMIT %d', $limit); + + $getAppls = DB::prepare($query); + $getAppls->execute(); + $out = []; + + while($appl = $getAppls->fetchObject(self::class)) + $out[] = $appl; + + return $out; + } +} diff --git a/src/Base64.php b/src/Base64.php new file mode 100644 index 0000000..476ce30 --- /dev/null +++ b/src/Base64.php @@ -0,0 +1,28 @@ + PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::MYSQL_ATTR_INIT_COMMAND => " + SET SESSION + sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION', + time_zone = '+00:00'; + ", + ]; + + public static $instance = null; + + public static function init(...$args) { + self::$instance = new PDO(...$args); + } + + public static function __callStatic(string $name, array $args) { + return self::$instance->{$name}(...$args); + } +} diff --git a/src/Upload.php b/src/Upload.php new file mode 100644 index 0000000..f6a90c4 --- /dev/null +++ b/src/Upload.php @@ -0,0 +1,390 @@ +upload_id; + } + + public function getPath(): string { + return PRM_UPLOADS . '/' . $this->getId(); + } + public function getThumbPath(): string { + return PRM_THUMBS . '/' . $this->getId(); + } + public function getRemotePath(): string { + return '/uploads/' . $this->getId(); + } + public function getPublicUrl(bool $forceReal = false): string { + if(!$forceReal && Config::has('Uploads', 'short_domain')) + return '//' . Config::get('Uploads', 'short_domain') . '/' . $this->getId(); + return '//' . $_SERVER['HTTP_HOST'] . $this->getRemotePath(); + } + public function getPublicThumbUrl(bool $forceReal = false): string { + return $this->getPublicUrl($forceReal) . '.t'; + } + + public function getUserId(): int { + return $this->user_id ?? 0; + } + public function setUserId(int $userId): void { + $this->user_id = $userId; + } + public function getUser(): User { + return User::byId($this->getUserId()); + } + public function setUser(User $user): void { + $this->setUserId($user->getId()); + } + + public function getApplicationId(): int { + return $this->app_id ?? 0; + } + public function setApplicationId(int $appId): void { + $this->app_id = $appId; + + $updateAppl = DB::prepare(' + UPDATE `prm_uploads` + SET `app_id` = :app + WHERE `upload_id` = :upload + '); + $updateAppl->bindValue('app', $appId); + $updateAppl->bindValue('upload', $this->getId()); + $updateAppl->execute(); + } + public function getApplication(): User { + return Application::byId($this->getApplicationId()); + } + public function setApplication(Application $app): void { + $this->setApplicationId($app->getId()); + } + + public function getType(): string { + return $this->upload_type ?? 'text/plain'; + } + + public function getName(): string { + return $this->upload_name ?? ''; + } + + public function getSize(): int { + return $this->upload_size ?? 0; + } + + public function getHash(): string { + return $this->upload_hash ?? str_pad('', 64, '0'); + } + + public function getCreated(): int { + return $this->upload_created; + } + + public function getLastAccessed(): int { + return $this->upload_accessed ?? 0; + } + public function bumpAccess(): void { + if(empty($this->getId())) + return; + $this->upload_accessed = time(); + $bumpAccess = DB::prepare(' + UPDATE `prm_uploads` + SET `upload_accessed` = NOW() + WHERE `upload_id` = :upload + '); + $bumpAccess->bindValue('upload', $this->getId()); + $bumpAccess->execute(); + } + + public function getExpires(): int { + return $this->upload_expires ?? 0; + } + public function hasExpired(): bool { + return $this->getExpires() > 1 && $this->getExpires() <= time(); + } + + public function getDeleted(): int { + return $this->upload_deleted ?? 0; + } + public function isDeleted(): bool { + return $this->getDeleted() > 0; + } + + public function getDMCA(): int { + return $this->upload_dmca ?? 0; + } + public function isDMCA(): bool { + return $this->getDMCA() > 0; + } + + public function getExpiryBump(): int { + return $this->upload_bump ?? 0; + } + + public function bumpExpiry(): void { + if(empty($this->getId()) || $this->getExpires() < 1) + return; + $bumpSeconds = $this->getExpiryBump(); + + if($bumpSeconds < 1) + return; + $this->upload_expires = time() + $bumpSeconds; + + $bumpExpiry = DB::prepare(' + UPDATE `prm_uploads` + SET `upload_expires` = NOW() + INTERVAL :seconds SECOND + WHERE `upload_id` = :upload + '); + $bumpExpiry->bindValue('seconds', $bumpSeconds); + $bumpExpiry->bindValue('upload', $this->getId()); + $bumpExpiry->execute(); + } + + public function restore(): void { + $this->upload_deleted = null; + $restore = DB::prepare(' + UPDATE `prm_uploads` + SET `upload_deleted` = NULL + WHERE `upload_id` = :id + '); + $restore->bindValue('id', $this->getId()); + $restore->execute(); + } + + public function delete(bool $hard): void { + $this->upload_deleted = time(); + + if($hard) { + if(is_file($this->getPath())) + unlink($this->getPath()); + if(is_file($this->getThumbPath())) + unlink($this->getThumbPath()); + + if($this->getDMCA() < 1) { + $delete = DB::prepare(' + DELETE FROM `prm_uploads` + WHERE `upload_id` = :id + '); + $delete->bindValue('id', $this->getId()); + $delete->execute(); + } + } else { + $delete = DB::prepare(' + UPDATE `prm_uploads` + SET `upload_deleted` = NOW() + WHERE `upload_id` = :id + '); + $delete->bindValue('id', $this->getId()); + $delete->execute(); + } + } + + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'url' => $this->getPublicUrl(), + 'urlf' => $this->getPublicUrl(true), + 'thumb' => $this->getPublicThumbUrl(), + 'name' => $this->getName(), + 'type' => $this->getType(), + 'size' => $this->getSize(), + 'user' => $this->getUserId(), + 'appl' => $this->getApplicationId(), + 'hash' => $this->getHash(), + 'created' => date('c', $this->getCreated()), + 'accessed' => $this->getLastAccessed() < 1 ? null : date('c', $this->getLastAccessed()), + 'expires' => $this->getExpires() < 1 ? null : date('c', $this->getExpires()), + 'deleted' => $this->getDeleted() < 1 ? null : date('c', $this->getDeleted()), + 'dmca' => $this->getDMCA() < 1 ? null : date('c', $this->getDMCA()), + ]; + } + + public static function generateId(int $length = 32): string { + $token = random_bytes($length); + $chars = strlen(self::ID_CHARS); + + for($i = 0; $i < $length; $i++) + $token[$i] = self::ID_CHARS[ord($token[$i]) % $chars]; + + return $token; + } + + public static function create( + Application $app, User $user, + string $fileName, string $fileType, + string $fileSize, string $fileHash, + int $fileExpiry, bool $bumpExpiry + ): self { + $appId = $app->getId(); + $userId = $user->getId(); + + if(strpos($fileType, '/') === false || $fileSize < 1 || strlen($fileHash) !== 64 || $fileExpiry < 0) + throw new UploadCreationFailedException('Bad args.'); + + $id = self::generateId(); + $create = DB::prepare(' + INSERT INTO `prm_uploads` ( + `upload_id`, `app_id`, `user_id`, `upload_name`, + `upload_type`, `upload_size`, `upload_hash`, `upload_ip`, + `upload_expires`, `upload_bump` + ) VALUES ( + :id, :app, :user, :name, :type, :size, + UNHEX(:hash), INET6_ATON(:ip), + FROM_UNIXTIME(:expire), :bump + ) + '); + $create->bindValue('id', $id); + $create->bindValue('app', $appId < 1 ? null : $appId); + $create->bindValue('user', $userId < 1 ? null : $userId); + $create->bindValue('ip', $_SERVER['REMOTE_ADDR']); + $create->bindValue('name', $fileName); + $create->bindValue('type', $fileType); + $create->bindValue('hash', $fileHash); + $create->bindValue('size', $fileSize); + $create->bindValue('expire', $fileExpiry > 0 ? (time() + $fileExpiry) : 0); + $create->bindValue('bump', $bumpExpiry ? $fileExpiry : 0); + $create->execute(); + + try { + return self::byId($id); + } catch(UploadNotFoundException $ex) { + throw new UploadCreationFailedException; + } + } + + public static function byId(string $id): self { + $getUpload = DB::prepare(' + SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`, + UNIX_TIMESTAMP(`upload_created`) AS `upload_created`, + UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`, + UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`, + UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`, + UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`, + INET6_NTOA(`upload_ip`) AS `upload_ip`, + LOWER(HEX(`upload_hash`)) AS `upload_hash` + FROM `prm_uploads` + WHERE `upload_id` = :id + AND `upload_deleted` IS NULL + '); + $getUpload->bindValue('id', $id); + $upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false; + + if(!$upload) + throw new UploadNotFoundException; + + return $upload; + } + + public static function byHash(string $hash): ?self { + $getUpload = DB::prepare(' + SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`, + UNIX_TIMESTAMP(`upload_created`) AS `upload_created`, + UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`, + UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`, + UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`, + UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`, + INET6_NTOA(`upload_ip`) AS `upload_ip`, + LOWER(HEX(`upload_hash`)) AS `upload_hash` + FROM `prm_uploads` + WHERE `upload_hash` = UNHEX(:hash) + '); + $getUpload->bindValue('hash', $hash); + $upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false; + return $upload ? $upload : null; + } + + public static function deleted(): array { + $getDeleted = DB::prepare(' + SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`, + UNIX_TIMESTAMP(`upload_created`) AS `upload_created`, + UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`, + UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`, + UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`, + UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`, + INET6_NTOA(`upload_ip`) AS `upload_ip`, + LOWER(HEX(`upload_hash`)) AS `upload_hash` + FROM `prm_uploads` + WHERE `upload_deleted` IS NOT NULL + OR `upload_dmca` IS NOT NULL + OR `user_id` IS NULL + OR `app_id` IS NULL + '); + if(!$getDeleted->execute()) + return []; + + $deleted = []; + + while($upload = $getDeleted->fetchObject(self::class)) + $deleted[] = $upload; + + return $deleted; + } + + public static function expired(): array { + $getExpired = DB::prepare(' + SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`, + UNIX_TIMESTAMP(`upload_created`) AS `upload_created`, + UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`, + UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`, + UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`, + UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`, + INET6_NTOA(`upload_ip`) AS `upload_ip`, + LOWER(HEX(`upload_hash`)) AS `upload_hash` + FROM `prm_uploads` + WHERE `upload_expires` IS NOT NULL + AND `upload_expires` <= NOW() + AND `upload_dmca` IS NULL + '); + if(!$getExpired->execute()) + return []; + + $deleted = []; + + while($upload = $getExpired->fetchObject(self::class)) + $deleted[] = $upload; + + return $deleted; + } + + public static function all(int $limit = 0, string $after = ''): array { + $query = ' + SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`, + UNIX_TIMESTAMP(`upload_created`) AS `upload_created`, + UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`, + UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`, + UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`, + UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`, + INET6_NTOA(`upload_ip`) AS `upload_ip`, + LOWER(HEX(`upload_hash`)) AS `upload_hash` + FROM `prm_uploads` + '; + + if(!empty($after)) + $query .= ' WHERE `upload_id` > :after'; + + $query .= ' ORDER BY `upload_id`'; + + if($limit > 0) + $query .= sprintf(' LIMIT %d', $limit); + + $getUploads = DB::prepare($query); + + if(!empty($after)) + $getUploads->bindValue('after', $after); + + $getUploads->execute(); + $out = []; + + while($upload = $getUploads->fetchObject(self::class)) + $out[] = $upload; + + return $out; + } +} diff --git a/src/User.php b/src/User.php new file mode 100644 index 0000000..84955c8 --- /dev/null +++ b/src/User.php @@ -0,0 +1,109 @@ +user_id ?? 0; + } + + public function getSizeMultiplier(): int { + return $this->user_size_multiplier ?? 0; + } + + public function getCreated(): int { + return $this->user_created ?? 0; + } + + public function getRestricted(): int { + return $this->user_restricted ?? 0; + } + public function isRestricted(): bool { + return $this->getRestricted() > 0; + } + + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'size_multi' => $this->getSizeMultiplier(), + 'created' => date('c', $this->getCreated()), + 'restricted' => $this->getRestricted() < 1 ? null : date('c', $this->getRestricted()), + ]; + } + + public static function byId(int $userId): self { + if($userId < 1) + throw new UserNotFoundException; + + $createUser = DB::prepare('INSERT IGNORE INTO `prm_users` (`user_id`) VALUES (:id)'); + $createUser->bindValue('id', $userId); + $createUser->execute(); + + $getUser = DB::prepare(' + SELECT `user_id`, `user_size_multiplier`, + UNIX_TIMESTAMP(`user_created`) AS `user_created`, + UNIX_TIMESTAMP(`user_restricted`) AS `user_restricted` + FROM `prm_users` + WHERE `user_id` = :user + '); + $getUser->bindValue('user', $userId); + $getUser->execute(); + $user = $getUser->fetchObject(self::class); + + if($user === false) + throw new UserNotFoundException; + + return $user; + } + + public static function all(int $limit = 0, int $after = 0): array { + $query = ' + SELECT `user_id`, `user_size_multiplier`, + UNIX_TIMESTAMP(`user_created`) AS `user_created`, + UNIX_TIMESTAMP(`user_restricted`) AS `user_restricted` + FROM `prm_users` + '; + + if($after > 0) + $query .= sprintf(' WHERE `user_id` > %d', $after); + + $query .= ' ORDER BY `user_id`'; + + if($limit > 0) + $query .= sprintf(' LIMIT %d', $limit); + + $getUsers = DB::prepare($query); + $getUsers->execute(); + $out = []; + + while($user = $getUsers->fetchObject(self::class)) + $out[] = $user; + + return $out; + } +} diff --git a/startup.php b/startup.php new file mode 100644 index 0000000..5111b5c --- /dev/null +++ b/startup.php @@ -0,0 +1,66 @@ +