2
0
Fork 0
forked from flashii/eeprom

Updated database code structure to match other projects.

This commit is contained in:
flash 2023-11-09 18:56:48 +00:00
parent 2f47bef354
commit 868c443d71
18 changed files with 879 additions and 696 deletions

26
composer.lock generated
View file

@ -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": [],

View file

@ -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);
}

View file

@ -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));
});

View file

@ -1,62 +0,0 @@
<?php
namespace EEPROM;
use RuntimeException;
use Index\Data\IDbConnection;
final class Application {
public function __construct(
private int $id = 0,
private string $name = '',
private int $created = 0,
private int $sizeLimit = -1,
private bool $allowSizeMultiplier = false,
private int $expiry = -1,
) {}
public function getId(): int {
return $this->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),
);
}
}

51
src/Apps/AppInfo.php Normal file
View file

@ -0,0 +1,51 @@
<?php
namespace EEPROM\Apps;
use Index\DateTime;
use Index\Data\IDbResult;
class AppInfo {
private string $id;
private string $name;
private int $created;
private int $sizeLimit;
private bool $allowSizeMultiplier;
private int $expiry;
public function __construct(IDbResult $result) {
$this->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;
}
}

25
src/Apps/AppsContext.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace EEPROM\Apps;
use RuntimeException;
use Index\Data\IDbConnection;
class AppsContext {
private AppsData $appsData;
public function __construct(IDbConnection $dbConn) {
$this->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;
}
}

24
src/Apps/AppsData.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace EEPROM\Apps;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
// this system is to be deprecated and replaced by pools system
class AppsData {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

21
src/FFMPEG.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace EEPROM;
use Index\IO\Stream;
use Index\IO\ProcessStream;
final class FFMPEG {
public static function grabVideoFrame(string $path): Stream {
return new ProcessStream(
sprintf('ffmpeg -i %s -ss 00:00:01.000 -vframes 1 -c:v png -f image2pipe -', escapeshellarg($path)),
'rb'
);
}
public static function grabAudioCover(string $path): Stream {
return new ProcessStream(
sprintf('ffmpeg -i %s -an -vcodec copy -f image2pipe -', escapeshellarg($path)),
'rb'
);
}
}

View file

@ -1,428 +0,0 @@
<?php
namespace EEPROM;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use Imagick;
use Index\XString;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
final class Upload {
public function __construct(
private string $id = '',
private int $userId = 0,
private int $appId = 0,
private string $type = 'text/plain',
private string $name = '',
private int $size = 0,
private string $hash = '0000000000000000000000000000000000000000000000000000000000000000',
private int $created = 0,
private int $accessed = 0,
private int $expires = 0,
private int $deleted = 0,
private int $dmca = 0,
private int $bump = 0,
private string $ipAddress = '::1',
) {}
public function getId(): string {
return $this->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);
}
}

165
src/Uploads/UploadInfo.php Normal file
View file

@ -0,0 +1,165 @@
<?php
namespace EEPROM\Uploads;
use Index\DateTime;
use Index\MediaType;
use Index\Data\IDbResult;
use Index\Net\IPAddress;
class UploadInfo {
private string $id;
private string $userId;
private string $appId;
private string $hash;
private string $remoteAddr;
private int $created;
private ?int $accessed;
private ?int $expires;
private ?int $deleted;
private ?int $dmca;
private int $bump;
private string $name;
private string $type;
private int $size;
public function __construct(IDbResult $result) {
$this->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;
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace EEPROM\Uploads;
use Imagick;
use Index\Data\IDbConnection;
use Syokuhou\IConfig;
use EEPROM\FFMPEG;
class UploadsContext {
private IConfig $config;
private UploadsData $uploadsData;
public function __construct(IConfig $config, IDbConnection $dbConn) {
$this->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);
}
}

164
src/Uploads/UploadsData.php Normal file
View file

@ -0,0 +1,164 @@
<?php
namespace EEPROM\Uploads;
use Index\XString;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use EEPROM\Apps\AppInfo;
use EEPROM\Users\UserInfo;
class UploadsData {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->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();
}
}

View file

@ -1,75 +0,0 @@
<?php
namespace EEPROM;
use RuntimeException;
use Index\Data\IDbConnection;
class User {
private static $active;
public static function hasActive(): bool {
return !empty(self::$active);
}
public static function active(): self {
return self::$active;
}
public function __construct(
private $id = 0,
private $sizeMultiplier = 0,
private $created = 0,
private $restricted = 0,
) {}
public function __destruct() {
if($this === self::$active)
self::$active = null;
}
public function setActive(): self {
self::$active = $this;
return $this;
}
public function getId(): int {
return $this->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),
);
}
}

47
src/Users/UserInfo.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace EEPROM\Users;
use Index\DateTime;
use Index\Data\IDbResult;
class UserInfo {
private string $id;
private int $created;
private ?int $restricted;
private int $sizeMultiplier;
public function __construct(IDbResult $result) {
$this->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;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace EEPROM\Users;
use Index\Data\IDbConnection;
class UsersContext {
private UsersData $usersData;
public function __construct(IDbConnection $dbConn) {
$this->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);
}
}

31
src/Users/UsersData.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace EEPROM\Users;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
// restrictions and permissions should be checked with misuzu or cached or something
// size multiplier should also just be replaced with an alternate max size value for premioids, maybe based on rank?
class UsersData {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->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();
}
}