From 868c443d71c2c7bbc3bfe49ff71c1e4269495a31 Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 9 Nov 2023 18:56:48 +0000 Subject: [PATCH] Updated database code structure to match other projects. --- composer.lock | 26 +- cron.php | 27 ++- public/index.php | 180 ++++++++------ src/Application.php | 62 ----- src/Apps/AppInfo.php | 51 ++++ src/Apps/AppsContext.php | 25 ++ src/Apps/AppsData.php | 24 ++ src/Auth/MisuzuAuth.php | 67 +++--- src/EEPROMContext.php | 20 ++ src/FFMPEG.php | 21 ++ src/Upload.php | 428 --------------------------------- src/Uploads/UploadInfo.php | 165 +++++++++++++ src/Uploads/UploadsContext.php | 141 +++++++++++ src/Uploads/UploadsData.php | 164 +++++++++++++ src/User.php | 75 ------ src/Users/UserInfo.php | 47 ++++ src/Users/UsersContext.php | 21 ++ src/Users/UsersData.php | 31 +++ 18 files changed, 879 insertions(+), 696 deletions(-) delete mode 100644 src/Application.php create mode 100644 src/Apps/AppInfo.php create mode 100644 src/Apps/AppsContext.php create mode 100644 src/Apps/AppsData.php create mode 100644 src/FFMPEG.php delete mode 100644 src/Upload.php create mode 100644 src/Uploads/UploadInfo.php create mode 100644 src/Uploads/UploadsContext.php create mode 100644 src/Uploads/UploadsData.php delete mode 100644 src/User.php create mode 100644 src/Users/UserInfo.php create mode 100644 src/Users/UsersContext.php create mode 100644 src/Users/UsersData.php diff --git a/composer.lock b/composer.lock index ea5927c..d63fbbc 100644 --- a/composer.lock +++ b/composer.lock @@ -78,7 +78,7 @@ "source": { "type": "git", "url": "https://git.flash.moe/flash/index.git", - "reference": "82a350a5c719cc83aa22382201683a68a2629f2a" + "reference": "c563bb20e8dfc046ca3cb4bbcbd682b28f87a88f" }, "require": { "ext-mbstring": "*", @@ -116,7 +116,7 @@ ], "description": "Composer package for the common library for my projects.", "homepage": "https://railgun.sh/index", - "time": "2023-09-15T22:44:36+00:00" + "time": "2023-11-09T14:04:39+00:00" }, { "name": "flashwave/syokuhou", @@ -803,16 +803,16 @@ }, { "name": "php-http/promise", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "ef4905bfb492ff389eb7f12e26925a0f20073050" + "reference": "44a67cb59f708f826f3bec35f22030b3edb90119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/ef4905bfb492ff389eb7f12e26925a0f20073050", - "reference": "ef4905bfb492ff389eb7f12e26925a0f20073050", + "url": "https://api.github.com/repos/php-http/promise/zipball/44a67cb59f708f826f3bec35f22030b3edb90119", + "reference": "44a67cb59f708f826f3bec35f22030b3edb90119", "shasum": "" }, "require": { @@ -849,9 +849,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.2.0" + "source": "https://github.com/php-http/promise/tree/1.2.1" }, - "time": "2023-10-24T09:20:26+00:00" + "time": "2023-11-08T12:57:08+00:00" }, { "name": "psr/container", @@ -1790,16 +1790,16 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.40", + "version": "1.10.41", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d" + "reference": "c6174523c2a69231df55bdc65b61655e72876d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93c84b5bf7669920d823631e39904d69b9c7dc5d", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6174523c2a69231df55bdc65b61655e72876d76", + "reference": "c6174523c2a69231df55bdc65b61655e72876d76", "shasum": "" }, "require": { @@ -1848,7 +1848,7 @@ "type": "tidelift" } ], - "time": "2023-10-30T14:48:31+00:00" + "time": "2023-11-05T12:57:57+00:00" } ], "aliases": [], diff --git a/cron.php b/cron.php index d663f40..213d07b 100644 --- a/cron.php +++ b/cron.php @@ -14,16 +14,23 @@ $semaphore = sem_get($ftok, 1); if(!sem_acquire($semaphore)) die('Failed to acquire semaphore.' . PHP_EOL); -require_once __DIR__ . '/eeprom.php'; +try { + require_once __DIR__ . '/eeprom.php'; -// Mark expired as deleted -$expired = Upload::expired($db); -foreach($expired as $upload) - $upload->delete($db, false); + $uploadsCtx = $eeprom->getUploadsContext(); + $uploadsData = $uploadsCtx->getUploadsData(); -// Hard delete soft deleted files -$deleted = Upload::deleted($db); -foreach($deleted as $upload) - $upload->delete($db, true); + // Mark expired as deleted + $expired = $uploadsData->getUploads(expired: true, deleted: false, dmca: false); + foreach($expired as $uploadInfo) + $uploadsData->deleteUpload($uploadInfo); -sem_release($semaphore); + // Hard delete soft deleted files + $deleted = $uploadsData->getUploads(deleted: true, dmca: false); + foreach($deleted as $uploadInfo) { + $uploadsCtx->deleteUploadData($uploadInfo); + $uploadsData->nukeUpload($uploadInfo); + } +} finally { + sem_release($semaphore); +} diff --git a/public/index.php b/public/index.php index 82325ad..0bb0051 100644 --- a/public/index.php +++ b/public/index.php @@ -34,23 +34,29 @@ function eepromOriginAllowed(string $origin): bool { return in_array($origin, $allowed); } -function eepromUploadInfo(Upload $uploadInfo): array { +function eepromUploadInfo(Uploads\UploadInfo $uploadInfo): array { + global $eeprom; + + $uploadsCtx = $eeprom->getUploadsContext(); + return [ 'id' => $uploadInfo->getId(), - 'url' => $uploadInfo->getPublicUrl(), - 'urlf' => $uploadInfo->getPublicUrl(true), - 'thumb' => $uploadInfo->getPublicThumbUrl(), + 'url' => $uploadsCtx->getFileUrlV1($uploadInfo), + 'urlf' => $uploadsCtx->getFileUrlV1($uploadInfo, true), + 'thumb' => $uploadsCtx->getThumbnailUrlV1($uploadInfo), 'name' => $uploadInfo->getName(), - 'type' => $uploadInfo->getType(), - 'size' => $uploadInfo->getSize(), - 'user' => $uploadInfo->getUserId(), - 'appl' => $uploadInfo->getApplicationId(), - 'hash' => $uploadInfo->getHash(), - 'created' => date('c', $uploadInfo->getCreated()), - 'accessed' => $uploadInfo->hasBeenAccessed() ? date('c', $uploadInfo->getLastAccessed()) : null, - 'expires' => $uploadInfo->hasExpired() ? date('c', $uploadInfo->getExpires()) : null, - 'deleted' => $uploadInfo->isDeleted() ? date('c', $uploadInfo->getDeleted()) : null, - 'dmca' => $uploadInfo->isDMCA() ? date('c', $uploadInfo->getDMCA()) : null, + 'type' => $uploadInfo->getMediaTypeString(), + 'size' => $uploadInfo->getDataSize(), + 'user' => (int)$uploadInfo->getUserId(), + 'appl' => (int)$uploadInfo->getAppId(), + 'hash' => $uploadInfo->getHashString(), + 'created' => str_replace('+00:00', 'Z', $uploadInfo->getCreatedAt()->format(\DateTime::ATOM)), + 'accessed' => $uploadInfo->hasBeenAccessed() ? str_replace('+00:00', 'Z', $uploadInfo->getAccessedAt()->format(\DateTime::ATOM)) : null, + 'expires' => $uploadInfo->hasExpired() ? str_replace('+00:00', 'Z', $uploadInfo->getExpiredAt()->format(\DateTime::ATOM)) : null, + + // These can never be reached, and in situation where they technically could it's because of an outdated local record + 'deleted' => null, + 'dmca' => null, ]; } @@ -74,6 +80,9 @@ $router->use('/', function($response, $request) { }); if($isApiDomain) { + // this is illegal, don't do this + $userInfo = null; + $router->use('/', function($response, $request) { if($request->hasHeader('Origin')) $response->setHeader('Access-Control-Allow-Credentials', 'true'); @@ -88,6 +97,8 @@ if($isApiDomain) { }); $router->use('/', function($response, $request) use ($db, $cfg) { + global $userInfo, $eeprom; + $auth = $request->getHeaderLine('Authorization'); if(empty($auth)) { $mszAuth = (string)$request->getCookie('msz_auth'); @@ -111,7 +122,7 @@ if($isApiDomain) { } if(isset($authUserId) && $authUserId > 0) - User::byId($db, $authUserId)->setActive(); + $userInfo = $eeprom->getUsersContext()->getUser($authUserId); } }); @@ -153,22 +164,22 @@ if($isApiDomain) { }); $router->post('/uploads', function($response, $request) use ($db) { + global $userInfo, $eeprom; + if(!$request->isFormContent()) return 400; $content = $request->getContent(); try { - $appInfo = Application::byId($db, (int)$content->getParam('src', FILTER_VALIDATE_INT)); + $appInfo = $eeprom->getAppsContext()->getApp($content->getParam('src', FILTER_VALIDATE_INT)); } catch(RuntimeException $ex) { return 404; } - if(!User::hasActive()) + if($userInfo === null) return 401; - $userInfo = User::active(); - if($userInfo->isRestricted()) return 403; @@ -178,9 +189,9 @@ if($isApiDomain) { return 400; } - $maxFileSize = $appInfo->getSizeLimit(); + $maxFileSize = $appInfo->getDataSizeLimit(); if($appInfo->allowSizeMultiplier()) - $maxFileSize *= $userInfo->getSizeMultiplier(); + $maxFileSize *= $userInfo->getDataSizeMultiplier(); $localFile = $file->getLocalFileName(); $fileSize = filesize($localFile); @@ -191,34 +202,38 @@ if($isApiDomain) { return 413; } + $uploadsCtx = $eeprom->getUploadsContext(); + $uploadsData = $uploadsCtx->getUploadsData(); + $hash = hash_file('sha256', $localFile); // this is stupid: dmca status is stored as a file record rather than in a separate table requiring this hack ass garbage - $uploadInfo = Upload::byAppUserHash($db, $appInfo, $userInfo, $hash) ?? Upload::byHash($db, $hash); + $uploadInfo = $uploadsData->getUpload(appInfo: $appInfo, userInfo: $userInfo, hashString: $hash) + ?? $uploadsData->getUpload(hashString: $hash); if($uploadInfo !== null) { - if($uploadInfo->isDMCA()) + if($uploadInfo->isCopyrightTakedown()) return 451; if($uploadInfo->getUserId() !== $userInfo->getId() - || $uploadInfo->getApplicationId() !== $appInfo->getId()) + || $uploadInfo->getAppId() !== $appInfo->getId()) unset($uploadInfo); } - if(!empty($uploadInfo)) { - if($uploadInfo->isDeleted()) - $uploadInfo->restore($db); - $uploadInfo->bumpExpiry($db); - } else { - $uploadInfo = Upload::create( - $db, $appInfo, $userInfo, - $file->getSuggestedFileName(), - mime_content_type($localFile), - $fileSize, $hash, - $appInfo->getExpiry(), true + if(empty($uploadInfo)) { + $uploadInfo = $uploadsData->createUpload( + $appInfo, $userInfo, $_SERVER['REMOTE_ADDR'], + $file->getSuggestedFileName(), mime_content_type($localFile), + $fileSize, $hash, $appInfo->getBumpAmount(), true ); + $filePath = $uploadsCtx->getFileDataPath($uploadInfo); + $file->moveTo($filePath); + } else { + $filePath = $uploadsCtx->getFileDataPath($uploadInfo); + if($uploadInfo->isDeleted()) + $uploadsData->restoreUpload($uploadInfo); - $file->moveTo($uploadInfo->getPath()); + $uploadsData->bumpUploadExpires($uploadInfo); } $response->setStatusCode(201); @@ -228,14 +243,20 @@ if($isApiDomain) { }); $router->delete('/uploads/:fileid', function($response, $request, $fileId) use ($db) { - try { - $uploadInfo = Upload::byId($db, $fileId); - } catch(RuntimeException $ex) { + global $userInfo, $eeprom; + + if($userInfo === null) + return 401; + + $uploadsData = $eeprom->getUploadsContext()->getUploadsData(); + + $uploadInfo = $uploadsData->getUpload(uploadId: $fileId); + if($uploadInfo === null) { $response->setContent('File not found.'); return 404; } - if($uploadInfo->isDMCA()) { + if($uploadInfo->isCopyrightTakedown()) { $response->setContent('File is unavailable for copyright reasons.'); return 451; } @@ -245,17 +266,16 @@ if($isApiDomain) { return 404; } - if(!User::hasActive()) - return 401; - - if(User::active()->isRestricted() || User::active()->getId() !== $uploadInfo->getUserId()) + if($userInfo->isRestricted() || $userInfo->getId() !== $uploadInfo->getUserId()) return 403; - $uploadInfo->delete($db, false); + $uploadsData->deleteUpload($uploadInfo); return 204; }); $router->get('/uploads/:filename', function($response, $request, $fileName) use ($db) { + global $eeprom; + $pathInfo = pathinfo($fileName); $fileId = $pathInfo['filename']; $fileExt = $pathInfo['extension'] ?? ''; @@ -265,17 +285,16 @@ if($isApiDomain) { if($fileExt !== '' && $fileExt !== 't' && $fileExt !== 'json') return 404; - try { - $uploadInfo = Upload::byId($db, $fileId); - } catch(RuntimeException $ex) { + $uploadsCtx = $eeprom->getUploadsContext(); + $uploadsData = $uploadsCtx->getUploadsData(); + + $uploadInfo = $uploadsData->getUpload(uploadId: $fileId); + if($uploadInfo === null) { $response->setContent('File not found.'); return 404; } - if($isJson) - return eepromUploadInfo($uploadInfo); - - if($uploadInfo->isDMCA()) { + if($uploadInfo->isCopyrightTakedown()) { $response->setContent('File is unavailable for copyright reasons.'); return 451; } @@ -285,32 +304,38 @@ if($isApiDomain) { return 404; } - if(!is_file($uploadInfo->getPath())) { + if($isJson) + return eepromUploadInfo($uploadInfo); + + $filePath = $uploadsCtx->getFileDataPath($uploadInfo); + if(!is_file($filePath)) { $response->setContent('Data is missing.'); return 404; } if(!$isThumbnail) { - $uploadInfo->bumpAccess($db); - $uploadInfo->bumpExpiry($db); + $uploadsData->bumpUploadAccess($uploadInfo); + $uploadsData->bumpUploadExpires($uploadInfo); } $fileName = $uploadInfo->getName(); - $contentType = $uploadInfo->getType(); + $contentType = $uploadInfo->getMediaTypeString(); if($contentType === 'application/octet-stream' || str_starts_with($contentType, 'text/')) $contentType = 'text/plain'; - $sourceDir = basename($isThumbnail ? PRM_THUMBS : PRM_UPLOADS); + if($isThumbnail) { + if(!$uploadsCtx->supportsThumbnailing($uploadInfo)) + return 404; - if($isThumbnail && $uploadInfo->supportsThumbnail()) { - if(!is_file($uploadInfo->getThumbPath())) - $uploadInfo->createThumbnail(); $contentType = 'image/jpeg'; + $accelRedirectPath = $uploadsCtx->getThumbnailDataRedirectPathOrCreate($uploadInfo); $fileName = pathinfo($fileName, PATHINFO_FILENAME) . '-thumb.jpg'; + } else { + $accelRedirectPath = $uploadsCtx->getFileDataRedirectPath($uploadInfo); } - $response->accelRedirect(sprintf('/%s/%s', $sourceDir, $uploadInfo->getId())); + $response->accelRedirect($accelRedirectPath); $response->setContentType($contentType); $response->setFileName(addslashes($fileName)); }); @@ -323,6 +348,8 @@ if($isApiDomain) { }); $router->get('/:filename', function($response, $request, $fileName) use ($db) { + global $eeprom; + $pathInfo = pathinfo($fileName); $fileId = $pathInfo['filename']; $fileExt = $pathInfo['extension'] ?? ''; @@ -331,14 +358,16 @@ if($isApiDomain) { if($fileExt !== '' && $fileExt !== 't') return 404; - try { - $uploadInfo = Upload::byId($db, $fileId); - } catch(RuntimeException $ex) { + $uploadsCtx = $eeprom->getUploadsContext(); + $uploadsData = $uploadsCtx->getUploadsData(); + + $uploadInfo = $uploadsData->getUpload(uploadId: $fileId); + if($uploadInfo === null) { $response->setContent('File not found.'); return 404; } - if($uploadInfo->isDMCA()) { + if($uploadInfo->isCopyrightTakedown()) { $response->setContent('File is unavailable for copyright reasons.'); return 451; } @@ -348,32 +377,35 @@ if($isApiDomain) { return 404; } - if(!is_file($uploadInfo->getPath())) { + $filePath = $uploadsCtx->getFileDataPath($uploadInfo); + if(!is_file($filePath)) { $response->setContent('Data is missing.'); return 404; } if(!$isThumbnail) { - $uploadInfo->bumpAccess($db); - $uploadInfo->bumpExpiry($db); + $uploadsData->bumpUploadAccess($uploadInfo); + $uploadsData->bumpUploadExpires($uploadInfo); } $fileName = $uploadInfo->getName(); - $contentType = $uploadInfo->getType(); + $contentType = $uploadInfo->getMediaTypeString(); if($contentType === 'application/octet-stream' || str_starts_with($contentType, 'text/')) $contentType = 'text/plain'; - $sourceDir = basename($isThumbnail ? PRM_THUMBS : PRM_UPLOADS); + if($isThumbnail) { + if(!$uploadsCtx->supportsThumbnailing($uploadInfo)) + return 404; - if($isThumbnail && $uploadInfo->supportsThumbnail()) { - if(!is_file($uploadInfo->getThumbPath())) - $uploadInfo->createThumbnail(); $contentType = 'image/jpeg'; + $accelRedirectPath = $uploadsCtx->getThumbnailDataRedirectPathOrCreate($uploadInfo); $fileName = pathinfo($fileName, PATHINFO_FILENAME) . '-thumb.jpg'; + } else { + $accelRedirectPath = $uploadsCtx->getFileDataRedirectPath($uploadInfo); } - $response->accelRedirect(sprintf('/%s/%s', $sourceDir, $uploadInfo->getId())); + $response->accelRedirect($accelRedirectPath); $response->setContentType($contentType); $response->setFileName(addslashes($fileName)); }); diff --git a/src/Application.php b/src/Application.php deleted file mode 100644 index 46feea0..0000000 --- a/src/Application.php +++ /dev/null @@ -1,62 +0,0 @@ -id; - } - - public function getName(): string { - return $this->name; - } - - public function getCreated(): int { - return $this->created; - } - - public function getSizeLimit(): int { - return $this->sizeLimit; - } - - public function allowSizeMultiplier(): bool { - return $this->allowSizeMultiplier; - } - - public function getExpiry(): int { - return $this->expiry; - } - - public static function byId(IDbConnection $conn, int $appId): self { - $get = $conn->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` = ?' - ); - $get->addParameter(1, $appId); - $get->execute(); - $result = $get->getResult(); - - if(!$result->next()) - throw new RuntimeException('Application $appId not found.'); - - return new static( - $result->getInteger(0), - $result->getString(1), - $result->getInteger(5), - $result->getInteger(2), - $result->getInteger(4) !== 0, - $result->getInteger(3), - ); - } -} diff --git a/src/Apps/AppInfo.php b/src/Apps/AppInfo.php new file mode 100644 index 0000000..e5960ba --- /dev/null +++ b/src/Apps/AppInfo.php @@ -0,0 +1,51 @@ +id = $result->getString(0); + $this->name = $result->getString(1); + $this->created = $result->getInteger(2); + $this->sizeLimit = $result->getInteger(3); + $this->allowSizeMultiplier = $result->getBoolean(4); + $this->expiry = $result->getInteger(5); + } + + public function getId(): string { + return $this->id; + } + + public function getName(): string { + return $this->name; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function getDataSizeLimit(): int { + return $this->sizeLimit; + } + + public function allowSizeMultiplier(): bool { + return $this->allowSizeMultiplier; + } + + public function getBumpAmount(): int { + return $this->expiry; + } +} diff --git a/src/Apps/AppsContext.php b/src/Apps/AppsContext.php new file mode 100644 index 0000000..5d85abd --- /dev/null +++ b/src/Apps/AppsContext.php @@ -0,0 +1,25 @@ +appsData = new AppsData($dbConn); + } + + public function getAppsData(): AppsData { + return $this->appsData; + } + + public function getApp(string $appId): AppInfo { + $appInfo = $this->appsData->getApp($appId); + if($appInfo === null) + throw new RuntimeException('Not application with this ID exists.'); + + return $appInfo; + } +} diff --git a/src/Apps/AppsData.php b/src/Apps/AppsData.php new file mode 100644 index 0000000..206fd8e --- /dev/null +++ b/src/Apps/AppsData.php @@ -0,0 +1,24 @@ +cache = new DbStatementCache($dbConn); + } + + public function getApp(string $appId): ?AppInfo { + $stmt = $this->cache->get('SELECT app_id, app_name, UNIX_TIMESTAMP(app_created), app_size_limit, app_allow_size_multiplier, app_expiry FROM prm_applications WHERE app_id = ?'); + $stmt->addParameter(1, $appId); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? new AppInfo($result) : null; + } +} diff --git a/src/Auth/MisuzuAuth.php b/src/Auth/MisuzuAuth.php index 9b2e0cc..f18f2bc 100644 --- a/src/Auth/MisuzuAuth.php +++ b/src/Auth/MisuzuAuth.php @@ -18,42 +18,41 @@ class MisuzuAuth implements IAuth { public function getName(): string { return 'Misuzu'; } public function verifyToken(string $token): int { - if(!empty($token)) { - $method = 'Misuzu'; - $signature = sprintf('verify#%s#%s#%s', $method, $token, $_SERVER['REMOTE_ADDR']); - $signature = hash_hmac('sha256', $signature, $this->secretKey); + if(empty($token)) + return 0; - $login = curl_init($this->endPoint); - curl_setopt_array($login, [ - CURLOPT_AUTOREFERER => false, - CURLOPT_FAILONERROR => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_HEADER => false, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query([ - 'method' => $method, - 'token' => $token, - 'ipaddr' => $_SERVER['REMOTE_ADDR'], - ], '', '&', PHP_QUERY_RFC3986), - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TCP_FASTOPEN => true, - CURLOPT_CONNECTTIMEOUT => 2, - CURLOPT_MAXREDIRS => 2, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_TIMEOUT => 5, - CURLOPT_USERAGENT => 'Flashii EEPROM', - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/x-www-form-urlencoded', - 'X-SharpChat-Signature: ' . $signature, - ], - ]); - $rawUserInfo = curl_exec($login); - $userInfo = json_decode($rawUserInfo); - curl_close($login); + $method = 'Misuzu'; + $signature = sprintf('verify#%s#%s#%s', $method, $token, $_SERVER['REMOTE_ADDR']); + $signature = hash_hmac('sha256', $signature, $this->secretKey); - return empty($userInfo->success) ? 0 : $userInfo->user_id; - } + $login = curl_init($this->endPoint); + curl_setopt_array($login, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HEADER => false, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query([ + 'method' => $method, + 'token' => $token, + 'ipaddr' => $_SERVER['REMOTE_ADDR'], + ], '', '&', PHP_QUERY_RFC3986), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_MAXREDIRS => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Flashii EEPROM', + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/x-www-form-urlencoded', + 'X-SharpChat-Signature: ' . $signature, + ], + ]); + $rawUserInfo = curl_exec($login); + $userInfo = json_decode($rawUserInfo); + curl_close($login); - return 0; + return empty($userInfo->success) || empty($userInfo->user_id) ? 0 : $userInfo->user_id; } } diff --git a/src/EEPROMContext.php b/src/EEPROMContext.php index 8911e14..3805c61 100644 --- a/src/EEPROMContext.php +++ b/src/EEPROMContext.php @@ -8,9 +8,17 @@ class EEPROMContext { private IConfig $config; private DatabaseContext $dbCtx; + private Apps\AppsContext $appsCtx; + private Uploads\UploadsContext $uploadsCtx; + private Users\UsersContext $usersCtx; + public function __construct(IConfig $config, IDbConnection $dbConn) { $this->config = $config; $this->dbCtx = new DatabaseContext($dbConn); + + $this->appsCtx = new Apps\AppsContext($dbConn); + $this->uploadsCtx = new Uploads\UploadsContext($config, $dbConn); + $this->usersCtx = new Users\UsersContext($dbConn); } public function getConfig(): IConfig { @@ -20,4 +28,16 @@ class EEPROMContext { public function getDatabase(): DatabaseContext { return $this->dbCtx; } + + public function getAppsContext(): Apps\AppsContext { + return $this->appsCtx; + } + + public function getUploadsContext(): Uploads\UploadsContext { + return $this->uploadsCtx; + } + + public function getUsersContext(): Users\UsersContext { + return $this->usersCtx; + } } diff --git a/src/FFMPEG.php b/src/FFMPEG.php new file mode 100644 index 0000000..484a85a --- /dev/null +++ b/src/FFMPEG.php @@ -0,0 +1,21 @@ +id; - } - - public function getPath(): string { - return PRM_UPLOADS . '/' . $this->id; - } - public function getThumbPath(): string { - return PRM_THUMBS . '/' . $this->id; - } - public function getRemotePath(): string { - return '/uploads/' . $this->id; - } - public function getPublicUrl(bool $forceReal = false): string { - global $cfg; - - if(!$forceReal && $cfg->hasValues('domain:short')) - return '//' . $cfg->getString('domain:short') . '/' . $this->id; - return '//' . $_SERVER['HTTP_HOST'] . $this->getRemotePath(); - } - public function getPublicThumbUrl(bool $forceReal = false): string { - return $this->getPublicUrl($forceReal) . '.t'; - } - - public function getUserId(): int { - return $this->userId; - } - - public function getApplicationId(): int { - return $this->appId; - } - - public function getType(): string { - return $this->type; - } - public function isImage(): bool { - return str_starts_with($this->type, 'image/'); - } - public function isVideo(): bool { - return str_starts_with($this->type, 'video/'); - } - public function isAudio(): bool { - return str_starts_with($this->type, 'audio/'); - } - - public function getName(): string { - return $this->name; - } - - public function getSize(): int { - return $this->size; - } - - public function getHash(): string { - return $this->hash; - } - - public function getCreated(): int { - return $this->created; - } - - public function hasBeenAccessed(): bool { - return $this->accessed < 1; - } - - public function getLastAccessed(): int { - return $this->accessed; - } - - public function bumpAccess(IDbConnection $conn): void { - if(empty($this->id)) - return; - $this->accessed = time(); - - $bump = $conn->prepare('UPDATE `prm_uploads` SET `upload_accessed` = NOW() WHERE `upload_id` = ?'); - $bump->addParameter(1, $this->id); - $bump->execute(); - } - - public function getExpires(): int { - return $this->expires; - } - public function hasExpired(): bool { - return $this->expires > 1 && $this->expires <= time(); - } - - public function getDeleted(): int { - return $this->deleted; - } - public function isDeleted(): bool { - return $this->deleted > 0; - } - - public function getDMCA(): int { - return $this->dmca; - } - public function isDMCA(): bool { - return $this->dmca > 0; - } - - public function getExpiryBump(): int { - return $this->bump; - } - - public function getRemoteAddress(): string { - return $this->ipAddress; - } - - public function bumpExpiry(IDbConnection $conn): void { - if(empty($this->id) || $this->expires < 1) - return; - - if($this->bump < 1) - return; - $this->expires = time() + $this->bump; - - $bump = $conn->prepare('UPDATE `prm_uploads` SET `upload_expires` = NOW() + INTERVAL ? SECOND WHERE `upload_id` = ?'); - $bump->addParameter(1, $this->bump); - $bump->addParameter(2, $this->id); - $bump->execute(); - } - - public function restore(IDbConnection $conn): void { - $this->deleted = 0; - - $restore = $conn->prepare('UPDATE `prm_uploads` SET `upload_deleted` = NULL WHERE `upload_id` = ?'); - $restore->addParameter(1, $this->id); - $restore->execute(); - } - - public function delete(IDbConnection $conn, bool $hard): void { - $this->deleted = time(); - - if($hard) { - if(is_file($this->getPath())) - unlink($this->getPath()); - if(is_file($this->getThumbPath())) - unlink($this->getThumbPath()); - - if($this->dmca < 1) { - $delete = $conn->prepare('DELETE FROM `prm_uploads` WHERE `upload_id` = ?'); - $delete->addParameter(1, $this->id); - $delete->execute(); - } - } else { - $delete = $conn->prepare('UPDATE `prm_uploads` SET `upload_deleted` = NOW() WHERE `upload_id` = ?'); - $delete->addParameter(1, $this->id); - $delete->execute(); - } - } - - public static function create( - IDbConnection $conn, - 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) - throw new InvalidArgumentException('$fileType must contain a /'); - if($fileSize < 1) - throw new InvalidArgumentException('$fileSize must be more than 0.'); - if(strlen($fileHash) !== 64) - throw new InvalidArgumentException('$fileHash must be 64 characters.'); - if($fileExpiry < 0) - throw new InvalidArgumentException('$fileExpiry must be a positive integer.'); - - $id = XString::random(32); - $create = $conn->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 (?, ?, ?, ?, ?, ?, UNHEX(?), INET6_ATON(?), FROM_UNIXTIME(?), ?)' - ); - $create->addParameter(1, $id); - $create->addParameter(2, $appId < 1 ? null : $appId); - $create->addParameter(3, $userId < 1 ? null : $userId); - $create->addParameter(4, $fileName); - $create->addParameter(5, $fileType); - $create->addParameter(6, $fileSize); - $create->addParameter(7, $fileHash); - $create->addParameter(8, $_SERVER['REMOTE_ADDR']); - $create->addParameter(9, $fileExpiry > 0 ? (time() + $fileExpiry) : 0); - $create->addParameter(10, $bumpExpiry ? $fileExpiry : 0); - $create->execute(); - - return self::byId($conn, $id); - } - - private static function constructDb(IDbResult $result): self { - return new static( - $result->getString(0), - $result->getInteger(2), - $result->getInteger(1), - $result->getString(4), - $result->getString(3), - $result->getInteger(5), - $result->getString(13), - $result->getInteger(7), - $result->getInteger(8), - $result->getInteger(9), - $result->getInteger(10), - $result->getInteger(11), - $result->getInteger(6), - $result->getString(12), - ); - } - - public static function byId(IDbConnection $conn, string $id): self { - $get = $conn->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` = ? AND `upload_deleted` IS NULL' - ); - $get->addParameter(1, $id); - $get->execute(); - $result = $get->getResult(); - - if(!$result->next()) - throw new RuntimeException('Upload $id not found.'); - - return self::constructDb($result); - } - - public static function byHash(IDbConnection $conn, string $hash): ?self { - $get = $conn->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(?)' - ); - $get->addParameter(1, $hash); - $get->execute(); - $result = $get->getResult(); - - if(!$result->next()) - return null; - - return self::constructDb($result); - } - - public static function byAppUserHash( - IDbConnection $conn, - Application|string $appInfo, - User|string $userInfo, - string $hash - ): ?self { - $get = $conn->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(?) AND `user_id` = ? AND `app_id` = ?' - ); - $get->addParameter(1, $hash); - $get->addParameter(2, $userInfo instanceof User ? $userInfo->getId() : $userInfo); - $get->addParameter(3, $appInfo instanceof Application ? $appInfo->getId() : $appInfo); - $get->execute(); - $result = $get->getResult(); - - if(!$result->next()) - return null; - - return self::constructDb($result); - } - - public static function deleted(IDbConnection $conn): array { - $result = $conn->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`' - . ' WHERE `upload_deleted` IS NOT NULL' - . ' OR `upload_dmca` IS NOT NULL' - . ' OR `user_id` IS NULL' - . ' OR `app_id` IS NULL' - ); - - $deleted = []; - - while($result->next()) - $deleted[] = self::constructDb($result); - - return $deleted; - } - - public static function expired(IDbConnection $conn): array { - $result = $conn->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`' - . ' WHERE `upload_expires` IS NOT NULL' - . ' AND `upload_expires` <= NOW()' - . ' AND `upload_dmca` IS NULL' - ); - - $expired = []; - - while($result->next()) - $expired[] = self::constructDb($result); - - return $expired; - } - - public function supportsThumbnail(): bool { - return $this->isImage() || $this->isAudio() || $this->isVideo(); - } - - public function createThumbnail(): void { - $tmpFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'eeprom' . bin2hex(random_bytes(10)) . '.jpg'; - - try { - if($this->isImage()) - $imagick = new Imagick($this->getPath()); - elseif($this->isAudio()) - $imagick = $this->getCoverFromAudio($tmpFile); - elseif($this->isVideo()) - $imagick = $this->getFrameFromVideo($tmpFile); - - if(!isset($imagick)) - return; - - $imagick->setImageFormat('jpg'); - $imagick->setImageCompressionQuality(40); - - $thumbRes = 100; - $width = $imagick->getImageWidth(); - $height = $imagick->getImageHeight(); - - if ($width > $height) { - $resizeWidth = $width * $thumbRes / $height; - $resizeHeight = $thumbRes; - } else { - $resizeWidth = $thumbRes; - $resizeHeight = $height * $thumbRes / $width; - } - - $resizeWidth = (int)$resizeWidth; - $resizeHeight = (int)$resizeHeight; - - $imagick->resizeImage( - $resizeWidth, $resizeHeight, - Imagick::FILTER_GAUSSIAN, 0.7 - ); - - $imagick->cropImage( - $thumbRes, - $thumbRes, - (int)ceil(($resizeWidth - $thumbRes) / 2), - (int)ceil(($resizeHeight - $thumbRes) / 2) - ); - - $imagick->writeImage($this->getThumbPath()); - } catch(Exception $ex) {} - - if(is_file($tmpFile)) - unlink($tmpFile); - } - - private function getFrameFromVideo(string $path): Imagick { - shell_exec(sprintf('ffmpeg -i %s -ss 00:00:01.000 -vframes 1 %s', $this->getPath(), $path)); - return new Imagick($path); - } - - private function getCoverFromAudio(string $path): Imagick { - shell_exec(sprintf('ffmpeg -i %s -an -vcodec copy %s', $this->getPath(), $path)); - return new Imagick($path); - } -} diff --git a/src/Uploads/UploadInfo.php b/src/Uploads/UploadInfo.php new file mode 100644 index 0000000..660060c --- /dev/null +++ b/src/Uploads/UploadInfo.php @@ -0,0 +1,165 @@ +id = $result->getString(0); + $this->userId = $result->getStringOrNull(1); + $this->appId = $result->getStringOrNull(2); + $this->hash = $result->getString(3); + $this->remoteAddr = $result->getString(4); + $this->created = $result->getInteger(5); + $this->accessed = $result->getIntegerOrNull(6); + $this->expires = $result->getIntegerOrNull(7); + $this->deleted = $result->getIntegerOrNull(8); + $this->dmca = $result->getIntegerOrNull(9); + $this->bump = $result->getInteger(10); + $this->name = $result->getString(11); + $this->type = $result->getString(12); + $this->size = $result->getInteger(13); + } + + public function getId(): string { + return $this->id; + } + + public function hasUserId(): bool { + return $this->userId !== null; + } + + public function getUserId(): ?string { + return $this->userId; + } + + public function hasAppId(): bool { + return $this->appId !== null; + } + + public function getAppId(): ?string { + return $this->appId; + } + + public function getHashString(): string { + return $this->hash; + } + + public function getRemoteAddressRaw(): string { + return $this->remoteAddr; + } + + public function getRemoteAddress(): IPAddress { + return IPAddress::parse($this->remoteAddr); + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function hasBeenAccessed(): bool { + return $this->accessed !== null; + } + + public function getAccessedTime(): ?int { + return $this->accessed; + } + + public function getAccessedAt(): ?DateTime { + return $this->accessed === null ? null : DateTime::fromUnixTimeSeconds($this->accessed); + } + + public function hasExpiryTime(): bool { + return $this->expires !== null; + } + + public function hasExpired(): bool { + return $this->expires !== null && $this->expires <= time(); + } + + public function getExpiredTime(): ?int { + return $this->expires; + } + + public function getExpiredAt(): ?DateTime { + return $this->expires === null ? null : DateTime::fromUnixTimeSeconds($this->expires); + } + + public function isDeleted(): bool { + return $this->deleted !== null; + } + + public function getDeletedTime(): ?int { + return $this->deleted; + } + + public function getDeletedAt(): ?DateTime { + return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted); + } + + public function isCopyrightTakedown(): bool { + return $this->dmca !== null; + } + + public function getCopyrightTakedownTime(): ?int { + return $this->dmca; + } + + public function getCopyrightTakedownAt(): ?DateTime { + return $this->dmca === null ? null : DateTime::fromUnixTimeSeconds($this->dmca); + } + + public function getBumpAmount(): int { + return $this->bump; + } + + public function getName(): string { + return $this->name; + } + + public function getMediaTypeString(): string { + return $this->type; + } + + public function getMediaType(): MediaType { + return MediaType::parse($this->type); + } + + public function isImage(): bool { + return str_starts_with($this->type, 'image/'); + } + + public function isVideo(): bool { + return str_starts_with($this->type, 'video/'); + } + + public function isAudio(): bool { + return str_starts_with($this->type, 'audio/'); + } + + public function getDataSize(): int { + return $this->size; + } +} diff --git a/src/Uploads/UploadsContext.php b/src/Uploads/UploadsContext.php new file mode 100644 index 0000000..2d01293 --- /dev/null +++ b/src/Uploads/UploadsContext.php @@ -0,0 +1,141 @@ +config = $config; + $this->uploadsData = new UploadsData($dbConn); + } + + public function getUploadsData(): UploadsData { + return $this->uploadsData; + } + + public function getFileDataPath(UploadInfo $uploadInfo): string { + return sprintf('%s%s%s', PRM_UPLOADS, DIRECTORY_SEPARATOR, $uploadInfo->getId()); + } + + public function getThumbnailDataPath(UploadInfo $uploadInfo): string { + return sprintf('%s%s%s', PRM_THUMBS, DIRECTORY_SEPARATOR, $uploadInfo->getId()); + } + + public function getFileDataRedirectPath(UploadInfo $uploadInfo): string { + return sprintf('%s%s%s', str_replace(PRM_PUBLIC, '', PRM_UPLOADS), DIRECTORY_SEPARATOR, $uploadInfo->getId()); + } + + public function getThumbnailDataRedirectPath(UploadInfo $uploadInfo): string { + return sprintf('%s%s%s', str_replace(PRM_PUBLIC, '', PRM_THUMBS), DIRECTORY_SEPARATOR, $uploadInfo->getId()); + } + + public function getThumbnailDataPathOrCreate(UploadInfo $uploadInfo): string { + if(!$this->supportsThumbnailing($uploadInfo)) + return ''; + + $thumbPath = $this->getThumbnailDataPath($uploadInfo); + if(is_file($thumbPath)) + return $thumbPath; + + return $this->createThumbnailInternal( + $uploadInfo, + $this->getFileDataPath($uploadInfo), + $thumbPath + ); + } + + public function getThumbnailDataRedirectPathOrCreate(UploadInfo $uploadInfo): string { + return str_replace(PRM_PUBLIC, '', $this->getThumbnailDataPathOrCreate($uploadInfo)); + } + + public function getFileUrlV1(UploadInfo $uploadInfo, bool $forceApiDomain = false): string { + if(!$forceApiDomain && $this->config->hasValues('domain:short')) + return sprintf('//%s/%s', $this->config->getString('domain:short'), $uploadInfo->getId()); + + return sprintf('//%s/uploads/%s', $this->config->getString('domain:api'), $uploadInfo->getId()); + } + + public function getThumbnailUrlV1(UploadInfo $uploadInfo, bool $forceApiDomain = false): string { + return sprintf('%s.t', $this->getFileUrlV1($uploadInfo, $forceApiDomain)); + } + + public function supportsThumbnailing(UploadInfo $uploadInfo): bool { + return $uploadInfo->isImage() + || $uploadInfo->isAudio() + || $uploadInfo->isVideo(); + } + + public function createThumbnail(UploadInfo $uploadInfo): string { + if(!$this->supportsThumbnailing($uploadInfo)) + return ''; + + return $this->createThumbnailInternal( + $uploadInfo, + $this->getFileDataPath($uploadInfo), + $this->getThumbnailDataPath($uploadInfo) + ); + } + + private function createThumbnailInternal(UploadInfo $uploadInfo, string $filePath, string $thumbPath): string { + $imagick = new Imagick; + + if($uploadInfo->isImage()) { + $imagick->readImageBlob(file_get_contents($filePath)); + } elseif($uploadInfo->isAudio()) + $imagick->readImageBlob(FFMPEG::grabAudioCover($filePath)); + elseif($uploadInfo->isVideo()) + $imagick->readImageBlob(FFMPEG::grabVideoFrame($filePath)); + + $imagick->setImageFormat('jpg'); + $imagick->setImageCompressionQuality($this->config->getInteger('thumb:quality', 40)); + + $thumbRes = $this->config->getInteger('thumb:dimensions', 100); + $width = $imagick->getImageWidth(); + $height = $imagick->getImageHeight(); + + if($width === $height) { + $resizeWidth = $resizeHeight = $thumbRes; + } elseif($width > $height) { + $resizeWidth = $width * $thumbRes / $height; + $resizeHeight = $thumbRes; + } else { + $resizeWidth = $thumbRes; + $resizeHeight = $height * $thumbRes / $width; + } + + $resizeWidth = (int)$resizeWidth; + $resizeHeight = (int)$resizeHeight; + + $imagick->resizeImage( + $resizeWidth, $resizeHeight, + Imagick::FILTER_GAUSSIAN, 0.7 + ); + + $imagick->cropImage( + $thumbRes, + $thumbRes, + (int)ceil(($resizeWidth - $thumbRes) / 2), + (int)ceil(($resizeHeight - $thumbRes) / 2) + ); + + $imagick->writeImage($thumbPath); + + return is_file($thumbPath) ? $thumbPath : ''; + } + + public function deleteUploadData(UploadInfo|string $uploadInfo): void { + $filePath = $this->getFileDataPath($uploadInfo); + if(is_file($filePath)) + unlink($filePath); + + $thumbPath = $this->getThumbnailDataPath($uploadInfo); + if(is_file($thumbPath)) + unlink($thumbPath); + } +} diff --git a/src/Uploads/UploadsData.php b/src/Uploads/UploadsData.php new file mode 100644 index 0000000..7325ada --- /dev/null +++ b/src/Uploads/UploadsData.php @@ -0,0 +1,164 @@ +cache = new DbStatementCache($dbConn); + } + + public function getUploads( + ?bool $deleted = null, + ?bool $expired = null, + ?bool $dmca = null + ): array { + $hasDeleted = $deleted !== null; + $hasExpired = $expired !== null; + $hasDMCA = $dmca !== null; + + $args = 0; + $query = 'SELECT upload_id, user_id, app_id, LOWER(HEX(upload_hash)), INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_accessed), UNIX_TIMESTAMP(upload_expires), UNIX_TIMESTAMP(upload_deleted), UNIX_TIMESTAMP(upload_dmca), upload_bump, upload_name, upload_type, upload_size FROM prm_uploads'; + if($hasDeleted) { + ++$args; + $query .= sprintf(' WHERE upload_deleted %s NULL', $deleted ? 'IS NOT' : 'IS'); + } + if($hasExpired) + $query .= sprintf(' %s upload_expires %s NOW()', ++$args > 1 ? 'AND' : 'WHERE', $expired ? '<=' : '>'); + if($hasDMCA) + $query .= sprintf(' %s upload_dmca %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $dmca ? 'IS NOT' : 'IS'); + + $stmt = $this->cache->get($query); + $stmt->execute(); + + $result = $stmt->getResult(); + $uploads = []; + + while($result->next()) + $uploads[] = new UploadInfo($result); + + return $uploads; + } + + public function getUpload( + ?string $uploadId = null, + ?string $hashString = null, + AppInfo|string|null $appInfo = null, + UserInfo|string|null $userInfo = null, + ): ?UploadInfo { + $hasUploadId = $uploadId !== null; + $hasHashString = $hashString !== null; + $hasAppInfo = $appInfo !== null; + $hasUserInfo = $userInfo !== null; + + $args = 0; + $query = 'SELECT upload_id, user_id, app_id, LOWER(HEX(upload_hash)), INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_accessed), UNIX_TIMESTAMP(upload_expires), UNIX_TIMESTAMP(upload_deleted), UNIX_TIMESTAMP(upload_dmca), upload_bump, upload_name, upload_type, upload_size FROM prm_uploads'; + if($hasUploadId) { + ++$args; + $query .= ' WHERE upload_id = ?'; + } + if($hasHashString) + $query .= sprintf(' %s upload_hash = UNHEX(?)', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasAppInfo) + $query .= sprintf(' %s app_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + if($hasUserInfo) + $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE'); + + $args = 0; + $stmt = $this->cache->get($query); + if($hasUploadId) + $stmt->addParameter(++$args, $uploadId); + if($hasHashString) + $stmt->addParameter(++$args, $hashString); + if($hasAppInfo) + $stmt->addParameter(++$args, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo); + if($hasUserInfo) + $stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? new UploadInfo($result) : null; + } + + public function createUpload( + AppInfo|string $appInfo, + UserInfo|string $userInfo, + string $remoteAddr, + string $fileName, + string $fileType, + string $fileSize, + string $fileHash, + int $fileExpiry, + bool $bumpExpiry + ): UploadInfo { + if(strpos($fileType, '/') === false) + throw new InvalidArgumentException('$fileType must contain a /'); + if($fileSize < 1) + throw new InvalidArgumentException('$fileSize must be more than 0.'); + if(strlen($fileHash) !== 64) + throw new InvalidArgumentException('$fileHash must be 64 characters.'); + if($fileExpiry < 0) + throw new InvalidArgumentException('$fileExpiry must be a positive integer.'); + + $uploadId = XString::random(32); + + $stmt = $this->cache->get('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 (?, ?, ?, ?, ?, ?, UNHEX(?), INET6_ATON(?), FROM_UNIXTIME(?), ?)'); + $stmt->addParameter(1, $uploadId); + $stmt->addParameter(2, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo); + $stmt->addParameter(3, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); + $stmt->addParameter(4, $fileName); + $stmt->addParameter(5, $fileType); + $stmt->addParameter(6, $fileSize); + $stmt->addParameter(7, $fileHash); + $stmt->addParameter(8, $remoteAddr); + $stmt->addParameter(9, $fileExpiry > 0 ? (time() + $fileExpiry) : 0); + $stmt->addParameter(10, $bumpExpiry ? $fileExpiry : 0); + $stmt->execute(); + + return $this->getUpload(uploadId: $uploadId); + } + + public function bumpUploadAccess(UploadInfo $uploadInfo): void { + $stmt = $this->cache->get('UPDATE prm_uploads SET upload_accessed = NOW() WHERE upload_id = ?'); + $stmt->addParameter(1, $uploadInfo->getId()); + $stmt->execute(); + } + + public function bumpUploadExpires(UploadInfo|string $uploadInfo): void { + if(!$uploadInfo->hasExpiryTime()) + return; + + $bumpAmount = $uploadInfo->getBumpAmount(); + if($bumpAmount < 1) + return; + + $stmt = $this->cache->get('UPDATE prm_uploads SET upload_expires = NOW() + INTERVAL ? SECOND WHERE upload_id = ?'); + $stmt->addParameter(1, $bumpAmount); + $stmt->addParameter(2, $uploadInfo->getId()); + $stmt->execute(); + } + + public function restoreUpload(UploadInfo|string $uploadInfo): void { + $stmt = $this->cache->get('UPDATE prm_uploads SET upload_deleted = NULL WHERE upload_id = ?'); + $stmt->addParameter(1, $uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo); + $stmt->execute(); + } + + public function deleteUpload(UploadInfo|string $uploadInfo): void { + $stmt = $this->cache->get('UPDATE prm_uploads SET upload_deleted = COALESCE(upload_deleted, NOW()) WHERE upload_id = ?'); + $stmt->addParameter(1, $uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo); + $stmt->execute(); + } + + public function nukeUpload(UploadInfo|string $uploadInfo): void { + $stmt = $this->cache->get('DELETE FROM prm_uploads WHERE upload_id = ? AND upload_dmca IS NULL'); + $stmt->addParameter(1, $uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo); + $stmt->execute(); + } +} diff --git a/src/User.php b/src/User.php deleted file mode 100644 index de3e2b8..0000000 --- a/src/User.php +++ /dev/null @@ -1,75 +0,0 @@ -id; - } - - public function getSizeMultiplier(): int { - return $this->sizeMultiplier; - } - - public function getCreated(): int { - return $this->created; - } - - public function getRestricted(): int { - return $this->restricted; - } - public function isRestricted(): bool { - return $this->restricted > 0; - } - - public static function byId(IDbConnection $conn, int $userId): self { - $create = $conn->prepare('INSERT IGNORE INTO `prm_users` (`user_id`) VALUES (?)'); - $create->addParameter(1, $userId); - $create->execute(); - - $get = $conn->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` = ?' - ); - $get->addParameter(1, $userId); - $get->execute(); - $result = $get->getResult(); - - if(!$result->next()) - throw new RuntimeException('User not found.'); - - return new User( - $result->getInteger(0), - $result->getInteger(1), - $result->getInteger(2), - $result->getInteger(3), - ); - } -} diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php new file mode 100644 index 0000000..06c1f11 --- /dev/null +++ b/src/Users/UserInfo.php @@ -0,0 +1,47 @@ +id = $result->getString(0); + $this->created = $result->getInteger(1); + $this->restricted = $result->getIntegerOrNull(2); + $this->sizeMultiplier = $result->getInteger(3); + } + + public function getId(): string { + return $this->id; + } + + public function getCreatedTime(): int { + return $this->created; + } + + public function getCreatedAt(): DateTime { + return DateTime::fromUnixTimeSeconds($this->created); + } + + public function isRestricted(): bool { + return $this->restricted !== null; + } + + public function getRestrictedTime(): ?int { + return $this->restricted; + } + + public function getRestrictedAt(): ?DateTime { + return $this->restricted === null ? null : DateTime::fromUnixTimeSeconds($this->restricted); + } + + public function getDataSizeMultiplier(): int { + return $this->sizeMultiplier; + } +} diff --git a/src/Users/UsersContext.php b/src/Users/UsersContext.php new file mode 100644 index 0000000..1c830c9 --- /dev/null +++ b/src/Users/UsersContext.php @@ -0,0 +1,21 @@ +usersData = new UsersData($dbConn); + } + + public function getUsersData(): UsersData { + return $this->usersData; + } + + public function getUser(string $userId): UserInfo { + $this->usersData->ensureUserExists($userId); + return $this->usersData->getUser($userId); + } +} diff --git a/src/Users/UsersData.php b/src/Users/UsersData.php new file mode 100644 index 0000000..d2e3d9e --- /dev/null +++ b/src/Users/UsersData.php @@ -0,0 +1,31 @@ +cache = new DbStatementCache($dbConn); + } + + public function getUser(string $userId): ?UserInfo { + $stmt = $this->cache->get('SELECT user_id, UNIX_TIMESTAMP(user_created), UNIX_TIMESTAMP(user_restricted), user_size_multiplier FROM prm_users WHERE user_id = ?'); + $stmt->addParameter(1, $userId); + $stmt->execute(); + + $result = $stmt->getResult(); + return $result->next() ? new UserInfo($result) : null; + } + + public function ensureUserExists(string $userId): void { + $stmt = $this->cache->get('INSERT IGNORE INTO prm_users (user_id) VALUES (?)'); + $stmt->addParameter(1, $userId); + $stmt->execute(); + } +}