forked from flashii/eeprom
Project structure overhaul.
This commit is contained in:
parent
868c443d71
commit
906fa8a02d
11 changed files with 469 additions and 396 deletions
5
cron.php
5
cron.php
|
@ -31,6 +31,11 @@ try {
|
|||
$uploadsCtx->deleteUploadData($uploadInfo);
|
||||
$uploadsData->nukeUpload($uploadInfo);
|
||||
}
|
||||
|
||||
// Ensure local data of DMCA'd files is gone
|
||||
$deleted = $uploadsData->getUploads(dmca: true);
|
||||
foreach($deleted as $uploadInfo)
|
||||
$uploadsCtx->deleteUploadData($uploadInfo);
|
||||
} finally {
|
||||
sem_release($semaphore);
|
||||
}
|
||||
|
|
395
public/index.php
395
public/index.php
|
@ -19,396 +19,9 @@ set_exception_handler(function(\Throwable $ex) {
|
|||
exit;
|
||||
});
|
||||
|
||||
function eepromOriginAllowed(string $origin): bool {
|
||||
global $cfg;
|
||||
ob_start();
|
||||
|
||||
$origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
|
||||
$request = \Index\Http\HttpRequest::fromRequest();
|
||||
$isApiDomain = $request->getHeaderLine('Host') === $cfg->getString('domain:api');
|
||||
|
||||
if($origin === $_SERVER['HTTP_HOST'])
|
||||
return true;
|
||||
|
||||
$allowed = $cfg->getArray('cors:origins');
|
||||
if(empty($allowed))
|
||||
return true;
|
||||
|
||||
return in_array($origin, $allowed);
|
||||
}
|
||||
|
||||
function eepromUploadInfo(Uploads\UploadInfo $uploadInfo): array {
|
||||
global $eeprom;
|
||||
|
||||
$uploadsCtx = $eeprom->getUploadsContext();
|
||||
|
||||
return [
|
||||
'id' => $uploadInfo->getId(),
|
||||
'url' => $uploadsCtx->getFileUrlV1($uploadInfo),
|
||||
'urlf' => $uploadsCtx->getFileUrlV1($uploadInfo, true),
|
||||
'thumb' => $uploadsCtx->getThumbnailUrlV1($uploadInfo),
|
||||
'name' => $uploadInfo->getName(),
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
$isApiDomain = $_SERVER['HTTP_HOST'] === $cfg->getString('domain:api');
|
||||
$router = new HttpFx;
|
||||
|
||||
$router->use('/', function($response) {
|
||||
$response->setPoweredBy('EEPROM');
|
||||
});
|
||||
|
||||
$router->use('/', function($response, $request) {
|
||||
$origin = $request->getHeaderLine('Origin');
|
||||
|
||||
if(!empty($origin)) {
|
||||
if(!eepromOriginAllowed($origin))
|
||||
return 403;
|
||||
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origin);
|
||||
$response->setHeader('Vary', 'Origin');
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
$router->use('/', function($response, $request) {
|
||||
if($request->getMethod() === 'OPTIONS') {
|
||||
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, DELETE');
|
||||
return 204;
|
||||
}
|
||||
});
|
||||
|
||||
$router->use('/', function($response, $request) use ($db, $cfg) {
|
||||
global $userInfo, $eeprom;
|
||||
|
||||
$auth = $request->getHeaderLine('Authorization');
|
||||
if(empty($auth)) {
|
||||
$mszAuth = (string)$request->getCookie('msz_auth');
|
||||
if(!empty($mszAuth))
|
||||
$auth = 'Misuzu ' . $mszAuth;
|
||||
}
|
||||
|
||||
if(!empty($auth)) {
|
||||
$authParts = explode(' ', $auth, 2);
|
||||
$authMethod = strval($authParts[0] ?? '');
|
||||
$authToken = strval($authParts[1] ?? '');
|
||||
|
||||
$authClients = $cfg->getArray('auth:clients');
|
||||
|
||||
foreach($authClients as $client) {
|
||||
$client = new $client;
|
||||
if($client->getName() !== $authMethod)
|
||||
continue;
|
||||
$authUserId = $client->verifyToken($authToken);
|
||||
break;
|
||||
}
|
||||
|
||||
if(isset($authUserId) && $authUserId > 0)
|
||||
$userInfo = $eeprom->getUsersContext()->getUser($authUserId);
|
||||
}
|
||||
});
|
||||
|
||||
$router->get('/eeprom.js', function($response) {
|
||||
$response->accelRedirect('/js/eeprom-v1.0.js');
|
||||
$response->setContentType('application/javascript; charset=utf-8');
|
||||
});
|
||||
|
||||
$router->get('/stats.json', function() use ($db) {
|
||||
$fileCount = 0;
|
||||
$userCount = 0;
|
||||
$totalSize = 0;
|
||||
$uniqueTypes = 0;
|
||||
|
||||
$uploadStats = $db->query('SELECT COUNT(`upload_id`) AS `amount`, SUM(`upload_size`) AS `size`, COUNT(DISTINCT `upload_type`) AS `types` FROM `prm_uploads` WHERE `upload_deleted` IS NULL AND `upload_dmca` IS NULL');
|
||||
|
||||
if($uploadStats->next()) {
|
||||
$fileCount = $uploadStats->getInteger(0);
|
||||
$totalSize = $uploadStats->getInteger(1);
|
||||
$uniqueTypes = $uploadStats->getInteger(2);
|
||||
}
|
||||
|
||||
$userStats = $db->query('SELECT COUNT(`user_id`) AS `amount` FROM `prm_users` WHERE `user_restricted` IS NULL');
|
||||
|
||||
if($userStats->next())
|
||||
$userCount = $userStats->getInteger(0);
|
||||
|
||||
return [
|
||||
'size' => $totalSize,
|
||||
'files' => $fileCount,
|
||||
'types' => $uniqueTypes,
|
||||
'members' => $userCount,
|
||||
];
|
||||
});
|
||||
|
||||
$router->get('/', function($response) {
|
||||
$response->accelRedirect('/index.html');
|
||||
$response->setContentType('text/html; charset=utf-8');
|
||||
});
|
||||
|
||||
$router->post('/uploads', function($response, $request) use ($db) {
|
||||
global $userInfo, $eeprom;
|
||||
|
||||
if(!$request->isFormContent())
|
||||
return 400;
|
||||
|
||||
$content = $request->getContent();
|
||||
|
||||
try {
|
||||
$appInfo = $eeprom->getAppsContext()->getApp($content->getParam('src', FILTER_VALIDATE_INT));
|
||||
} catch(RuntimeException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($userInfo === null)
|
||||
return 401;
|
||||
|
||||
if($userInfo->isRestricted())
|
||||
return 403;
|
||||
|
||||
try {
|
||||
$file = $content->getUploadedFile('file');
|
||||
} catch(RuntimeException $ex) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
$maxFileSize = $appInfo->getDataSizeLimit();
|
||||
if($appInfo->allowSizeMultiplier())
|
||||
$maxFileSize *= $userInfo->getDataSizeMultiplier();
|
||||
|
||||
$localFile = $file->getLocalFileName();
|
||||
$fileSize = filesize($localFile);
|
||||
|
||||
if($file->getSize() !== $fileSize || $fileSize > $maxFileSize) {
|
||||
$response->setHeader('Access-Control-Expose-Headers', 'X-EEPROM-Max-Size');
|
||||
$response->setHeader('X-EEPROM-Max-Size', $maxFileSize);
|
||||
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 = $uploadsData->getUpload(appInfo: $appInfo, userInfo: $userInfo, hashString: $hash)
|
||||
?? $uploadsData->getUpload(hashString: $hash);
|
||||
|
||||
if($uploadInfo !== null) {
|
||||
if($uploadInfo->isCopyrightTakedown())
|
||||
return 451;
|
||||
|
||||
if($uploadInfo->getUserId() !== $userInfo->getId()
|
||||
|| $uploadInfo->getAppId() !== $appInfo->getId())
|
||||
unset($uploadInfo);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$uploadsData->bumpUploadExpires($uploadInfo);
|
||||
}
|
||||
|
||||
$response->setStatusCode(201);
|
||||
$response->setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
return eepromUploadInfo($uploadInfo);
|
||||
});
|
||||
|
||||
$router->delete('/uploads/:fileid', function($response, $request, $fileId) use ($db) {
|
||||
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->isCopyrightTakedown()) {
|
||||
$response->setContent('File is unavailable for copyright reasons.');
|
||||
return 451;
|
||||
}
|
||||
|
||||
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($userInfo->isRestricted() || $userInfo->getId() !== $uploadInfo->getUserId())
|
||||
return 403;
|
||||
|
||||
$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'] ?? '';
|
||||
$isThumbnail = $fileExt === 't';
|
||||
$isJson = $fileExt === 'json';
|
||||
|
||||
if($fileExt !== '' && $fileExt !== 't' && $fileExt !== 'json')
|
||||
return 404;
|
||||
|
||||
$uploadsCtx = $eeprom->getUploadsContext();
|
||||
$uploadsData = $uploadsCtx->getUploadsData();
|
||||
|
||||
$uploadInfo = $uploadsData->getUpload(uploadId: $fileId);
|
||||
if($uploadInfo === null) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($uploadInfo->isCopyrightTakedown()) {
|
||||
$response->setContent('File is unavailable for copyright reasons.');
|
||||
return 451;
|
||||
}
|
||||
|
||||
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($isJson)
|
||||
return eepromUploadInfo($uploadInfo);
|
||||
|
||||
$filePath = $uploadsCtx->getFileDataPath($uploadInfo);
|
||||
if(!is_file($filePath)) {
|
||||
$response->setContent('Data is missing.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$isThumbnail) {
|
||||
$uploadsData->bumpUploadAccess($uploadInfo);
|
||||
$uploadsData->bumpUploadExpires($uploadInfo);
|
||||
}
|
||||
|
||||
$fileName = $uploadInfo->getName();
|
||||
$contentType = $uploadInfo->getMediaTypeString();
|
||||
|
||||
if($contentType === 'application/octet-stream' || str_starts_with($contentType, 'text/'))
|
||||
$contentType = 'text/plain';
|
||||
|
||||
if($isThumbnail) {
|
||||
if(!$uploadsCtx->supportsThumbnailing($uploadInfo))
|
||||
return 404;
|
||||
|
||||
$contentType = 'image/jpeg';
|
||||
$accelRedirectPath = $uploadsCtx->getThumbnailDataRedirectPathOrCreate($uploadInfo);
|
||||
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '-thumb.jpg';
|
||||
} else {
|
||||
$accelRedirectPath = $uploadsCtx->getFileDataRedirectPath($uploadInfo);
|
||||
}
|
||||
|
||||
$response->accelRedirect($accelRedirectPath);
|
||||
$response->setContentType($contentType);
|
||||
$response->setFileName(addslashes($fileName));
|
||||
});
|
||||
} else {
|
||||
$router->use('/', function($response, $request) {
|
||||
if($request->getMethod() === 'OPTIONS') {
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
|
||||
return 204;
|
||||
}
|
||||
});
|
||||
|
||||
$router->get('/:filename', function($response, $request, $fileName) use ($db) {
|
||||
global $eeprom;
|
||||
|
||||
$pathInfo = pathinfo($fileName);
|
||||
$fileId = $pathInfo['filename'];
|
||||
$fileExt = $pathInfo['extension'] ?? '';
|
||||
$isThumbnail = $fileExt === 't';
|
||||
|
||||
if($fileExt !== '' && $fileExt !== 't')
|
||||
return 404;
|
||||
|
||||
$uploadsCtx = $eeprom->getUploadsContext();
|
||||
$uploadsData = $uploadsCtx->getUploadsData();
|
||||
|
||||
$uploadInfo = $uploadsData->getUpload(uploadId: $fileId);
|
||||
if($uploadInfo === null) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($uploadInfo->isCopyrightTakedown()) {
|
||||
$response->setContent('File is unavailable for copyright reasons.');
|
||||
return 451;
|
||||
}
|
||||
|
||||
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
$filePath = $uploadsCtx->getFileDataPath($uploadInfo);
|
||||
if(!is_file($filePath)) {
|
||||
$response->setContent('Data is missing.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$isThumbnail) {
|
||||
$uploadsData->bumpUploadAccess($uploadInfo);
|
||||
$uploadsData->bumpUploadExpires($uploadInfo);
|
||||
}
|
||||
|
||||
$fileName = $uploadInfo->getName();
|
||||
$contentType = $uploadInfo->getMediaTypeString();
|
||||
|
||||
if($contentType === 'application/octet-stream' || str_starts_with($contentType, 'text/'))
|
||||
$contentType = 'text/plain';
|
||||
|
||||
if($isThumbnail) {
|
||||
if(!$uploadsCtx->supportsThumbnailing($uploadInfo))
|
||||
return 404;
|
||||
|
||||
$contentType = 'image/jpeg';
|
||||
$accelRedirectPath = $uploadsCtx->getThumbnailDataRedirectPathOrCreate($uploadInfo);
|
||||
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '-thumb.jpg';
|
||||
} else {
|
||||
$accelRedirectPath = $uploadsCtx->getFileDataRedirectPath($uploadInfo);
|
||||
}
|
||||
|
||||
$response->accelRedirect($accelRedirectPath);
|
||||
$response->setContentType($contentType);
|
||||
$response->setFileName(addslashes($fileName));
|
||||
});
|
||||
}
|
||||
|
||||
$router->dispatch();
|
||||
$eeprom->createRouting($isApiDomain)->dispatch($request);
|
||||
|
|
|
@ -110,8 +110,7 @@ EEPROM.EEPROMDeleteTask = function(authorization, fileInfo) {
|
|||
|
||||
obj.start = function() {
|
||||
xhr.open('DELETE', obj.fileInfo.urlf);
|
||||
if(obj.authorization) xhr.setRequestHeader('Authorization', obj.authorization);
|
||||
else xhr.withCredentials = true;
|
||||
xhr.setRequestHeader('Authorization', obj.authorization);
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
|
@ -204,8 +203,7 @@ EEPROM.EEPROMUploadTask = function(srcId, endpoint, authorization, file) {
|
|||
|
||||
obj.start = function() {
|
||||
xhr.open('POST', obj.endpoint);
|
||||
if(obj.authorization) xhr.setRequestHeader('Authorization', obj.authorization);
|
||||
else xhr.withCredentials = true;
|
||||
xhr.setRequestHeader('Authorization', obj.authorization);
|
||||
xhr.send(fd);
|
||||
};
|
||||
|
||||
|
|
34
src/Auth/AuthInfo.php
Normal file
34
src/Auth/AuthInfo.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
namespace EEPROM\Auth;
|
||||
|
||||
use EEPROM\Users\UserInfo;
|
||||
|
||||
class AuthInfo {
|
||||
private ?UserInfo $userInfo;
|
||||
|
||||
public function __construct() {
|
||||
$this->setInfo();
|
||||
}
|
||||
|
||||
public function setInfo(
|
||||
?UserInfo $userInfo = null
|
||||
): void {
|
||||
$this->userInfo = $userInfo;
|
||||
}
|
||||
|
||||
public function removeInfo(): void {
|
||||
$this->setInfo();
|
||||
}
|
||||
|
||||
public function isLoggedIn(): bool {
|
||||
return $this->userInfo !== null;
|
||||
}
|
||||
|
||||
public function getUserId(): ?string {
|
||||
return $this->userInfo?->getId();
|
||||
}
|
||||
|
||||
public function getUserInfo(): ?UserInfo {
|
||||
return $this->userInfo;
|
||||
}
|
||||
}
|
40
src/Auth/AuthRoutes.php
Normal file
40
src/Auth/AuthRoutes.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
namespace EEPROM\Auth;
|
||||
|
||||
use stdClass;
|
||||
use Index\Routing\Route;
|
||||
use Index\Routing\RouteHandler;
|
||||
use Syokuhou\IConfig;
|
||||
use EEPROM\Users\UsersContext;
|
||||
|
||||
class AuthRoutes extends RouteHandler {
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private AuthInfo $authInfo,
|
||||
private UsersContext $usersCtx
|
||||
) {}
|
||||
|
||||
#[Route('/')]
|
||||
public function getIndex($response, $request) {
|
||||
$auth = $request->getHeaderLine('Authorization');
|
||||
|
||||
if(!empty($auth)) {
|
||||
$authParts = explode(' ', $auth, 2);
|
||||
$authMethod = strval($authParts[0] ?? '');
|
||||
$authToken = strval($authParts[1] ?? '');
|
||||
|
||||
$authClients = $this->config->getArray('clients');
|
||||
|
||||
foreach($authClients as $client) {
|
||||
$client = new $client;
|
||||
if($client->getName() !== $authMethod)
|
||||
continue;
|
||||
$authUserId = $client->verifyToken($authToken);
|
||||
break;
|
||||
}
|
||||
|
||||
if(isset($authUserId) && $authUserId > 0)
|
||||
$this->authInfo->setInfo($this->usersCtx->getUser($authUserId));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,11 +3,14 @@ namespace EEPROM;
|
|||
|
||||
use Index\Data\IDbConnection;
|
||||
use Syokuhou\IConfig;
|
||||
use EEPROM\Auth\AuthInfo;
|
||||
|
||||
class EEPROMContext {
|
||||
private IConfig $config;
|
||||
private DatabaseContext $dbCtx;
|
||||
|
||||
private AuthInfo $authInfo;
|
||||
|
||||
private Apps\AppsContext $appsCtx;
|
||||
private Uploads\UploadsContext $uploadsCtx;
|
||||
private Users\UsersContext $usersCtx;
|
||||
|
@ -16,6 +19,8 @@ class EEPROMContext {
|
|||
$this->config = $config;
|
||||
$this->dbCtx = new DatabaseContext($dbConn);
|
||||
|
||||
$this->authInfo = new AuthInfo;
|
||||
|
||||
$this->appsCtx = new Apps\AppsContext($dbConn);
|
||||
$this->uploadsCtx = new Uploads\UploadsContext($config, $dbConn);
|
||||
$this->usersCtx = new Users\UsersContext($dbConn);
|
||||
|
@ -29,6 +34,10 @@ class EEPROMContext {
|
|||
return $this->dbCtx;
|
||||
}
|
||||
|
||||
public function getAuthInfo(): AuthInfo {
|
||||
return $this->authInfo;
|
||||
}
|
||||
|
||||
public function getAppsContext(): Apps\AppsContext {
|
||||
return $this->appsCtx;
|
||||
}
|
||||
|
@ -40,4 +49,27 @@ class EEPROMContext {
|
|||
public function getUsersContext(): Users\UsersContext {
|
||||
return $this->usersCtx;
|
||||
}
|
||||
|
||||
public function createRouting(bool $isApiDomain): RoutingContext {
|
||||
$routingCtx = new RoutingContext($this->config->scopeTo('cors'));
|
||||
|
||||
if($isApiDomain) {
|
||||
$routingCtx->register(new Auth\AuthRoutes(
|
||||
$this->config->scopeTo('auth'),
|
||||
$this->authInfo,
|
||||
$this->usersCtx
|
||||
));
|
||||
|
||||
$routingCtx->register(new LandingRoutes($this->dbCtx));
|
||||
}
|
||||
|
||||
$routingCtx->register(new Uploads\UploadsRoutes(
|
||||
$this->authInfo,
|
||||
$this->appsCtx,
|
||||
$this->uploadsCtx,
|
||||
$isApiDomain
|
||||
));
|
||||
|
||||
return $routingCtx;
|
||||
}
|
||||
}
|
||||
|
|
48
src/LandingRoutes.php
Normal file
48
src/LandingRoutes.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
namespace EEPROM;
|
||||
|
||||
use stdClass;
|
||||
use Index\Routing\Route;
|
||||
use Index\Routing\RouteHandler;
|
||||
|
||||
class LandingRoutes extends RouteHandler {
|
||||
public function __construct(
|
||||
private DatabaseContext $dbCtx
|
||||
) {}
|
||||
|
||||
#[Route('GET', '/')]
|
||||
public function getIndex($response) {
|
||||
$response->accelRedirect('/index.html');
|
||||
$response->setContentType('text/html; charset=utf-8');
|
||||
}
|
||||
|
||||
#[Route('GET', '/stats.json')]
|
||||
public function getStats() {
|
||||
$dbConn = $this->dbCtx->getConnection();
|
||||
|
||||
$stats = new stdClass;
|
||||
$stats->files = 0;
|
||||
$stats->size = 0;
|
||||
$stats->types = 0;
|
||||
$stats->members = 0;
|
||||
|
||||
$result = $dbConn->query('SELECT COUNT(upload_id), SUM(upload_size), COUNT(DISTINCT upload_type) FROM prm_uploads WHERE upload_deleted IS NULL AND upload_dmca IS NULL');
|
||||
if($result->next()) {
|
||||
$stats->files = $result->getInteger(0);
|
||||
$stats->size = $result->getInteger(1);
|
||||
$stats->types = $result->getInteger(2);
|
||||
}
|
||||
|
||||
$result = $dbConn->query('SELECT COUNT(user_id) FROM prm_users WHERE user_restricted IS NULL');
|
||||
if($result->next())
|
||||
$stats->members = $result->getInteger(0);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
#[Route('GET', '/eeprom.js')]
|
||||
public function getEepromJs($response) {
|
||||
$response->accelRedirect('/js/eeprom-v1.0.js');
|
||||
$response->setContentType('application/javascript; charset=utf-8');
|
||||
}
|
||||
}
|
55
src/RoutingContext.php
Normal file
55
src/RoutingContext.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
namespace EEPROM;
|
||||
|
||||
use Index\Http\HttpFx;
|
||||
use Index\Http\HttpRequest;
|
||||
use Index\Routing\IRouter;
|
||||
use Index\Routing\IRouteHandler;
|
||||
use Syokuhou\IConfig;
|
||||
|
||||
class RoutingContext {
|
||||
private HttpFx $router;
|
||||
|
||||
public function __construct(private IConfig $config) {
|
||||
$this->router = new HttpFx;
|
||||
$this->router->use('/', $this->middleware(...));
|
||||
}
|
||||
|
||||
private function middleware($response, $request) {
|
||||
$response->setPoweredBy('EEPROM');
|
||||
|
||||
$origin = $request->getHeaderLine('Origin');
|
||||
|
||||
if(!empty($origin)) {
|
||||
$originHost = parse_url($origin, PHP_URL_HOST);
|
||||
if(empty($originHost))
|
||||
return 403;
|
||||
|
||||
if($originHost !== $request->getHeaderLine('Host')) {
|
||||
$allowedOrigins = $this->config->getArray('origins');
|
||||
|
||||
if(!empty($allowedOrigins)) {
|
||||
$originHost = strtolower($originHost);
|
||||
|
||||
if(!in_array($originHost, $allowedOrigins))
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origin);
|
||||
$response->setHeader('Vary', 'Origin');
|
||||
}
|
||||
}
|
||||
|
||||
public function getRouter(): IRouter {
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
public function register(IRouteHandler $handler): void {
|
||||
$this->router->register($handler);
|
||||
}
|
||||
|
||||
public function dispatch(?HttpRequest $request = null): void {
|
||||
$this->router->dispatch($request);
|
||||
}
|
||||
}
|
|
@ -65,6 +65,28 @@ class UploadsContext {
|
|||
return sprintf('%s.t', $this->getFileUrlV1($uploadInfo, $forceApiDomain));
|
||||
}
|
||||
|
||||
public function convertToClientJsonV1(UploadInfo $uploadInfo): array {
|
||||
return [
|
||||
'id' => $uploadInfo->getId(),
|
||||
'url' => $this->getFileUrlV1($uploadInfo),
|
||||
'urlf' => $this->getFileUrlV1($uploadInfo, true),
|
||||
'thumb' => $this->getThumbnailUrlV1($uploadInfo),
|
||||
'name' => $uploadInfo->getName(),
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsThumbnailing(UploadInfo $uploadInfo): bool {
|
||||
return $uploadInfo->isImage()
|
||||
|| $uploadInfo->isAudio()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace EEPROM\Uploads;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\XString;
|
||||
use Index\Data\DbStatementCache;
|
||||
use Index\Data\IDbConnection;
|
||||
|
@ -92,7 +93,7 @@ class UploadsData {
|
|||
string $remoteAddr,
|
||||
string $fileName,
|
||||
string $fileType,
|
||||
string $fileSize,
|
||||
int $fileSize,
|
||||
string $fileHash,
|
||||
int $fileExpiry,
|
||||
bool $bumpExpiry
|
||||
|
|
225
src/Uploads/UploadsRoutes.php
Normal file
225
src/Uploads/UploadsRoutes.php
Normal file
|
@ -0,0 +1,225 @@
|
|||
<?php
|
||||
namespace EEPROM\Uploads;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Routing\IRouter;
|
||||
use Index\Routing\IRouteHandler;
|
||||
use Index\Routing\Route;
|
||||
use EEPROM\Apps\AppsContext;
|
||||
use EEPROM\Auth\AuthInfo;
|
||||
use EEPROM\Uploads\UploadsContext;
|
||||
|
||||
class UploadsRoutes implements IRouteHandler {
|
||||
public function __construct(
|
||||
private AuthInfo $authInfo,
|
||||
private AppsContext $appsCtx,
|
||||
private UploadsContext $uploadsCtx,
|
||||
private bool $isApiDomain
|
||||
) {}
|
||||
|
||||
public function registerRoutes(IRouter $router): void {
|
||||
if($this->isApiDomain) {
|
||||
Route::handleAttributes($router, $this);
|
||||
} else {
|
||||
$router->options('/', $this->getUpload(...));
|
||||
$router->get('/:filename', $this->getUpload(...));
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('OPTIONS', '/uploads/:filename')]
|
||||
#[Route('GET', '/uploads/:filename')]
|
||||
public function getUpload($response, $request, string $fileName) {
|
||||
if($this->isApiDomain) {
|
||||
if($request->hasHeader('Origin'))
|
||||
$response->setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, DELETE');
|
||||
} else {
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET');
|
||||
}
|
||||
|
||||
if($request->getMethod() === 'OPTIONS')
|
||||
return 204;
|
||||
|
||||
$pathInfo = pathinfo($fileName);
|
||||
$fileId = $pathInfo['filename'];
|
||||
$fileExt = $pathInfo['extension'] ?? '';
|
||||
$isData = $fileExt === '';
|
||||
$isThumbnail = $fileExt === 't';
|
||||
$isJson = $this->isApiDomain && $fileExt === 'json';
|
||||
|
||||
if(!$isData && !$isThumbnail && !$isJson)
|
||||
return 404;
|
||||
|
||||
$uploadsData = $this->uploadsCtx->getUploadsData();
|
||||
$uploadInfo = $uploadsData->getUpload(uploadId: $fileId);
|
||||
if($uploadInfo === null) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($uploadInfo->isCopyrightTakedown()) {
|
||||
$response->setContent('File is unavailable for copyright reasons.');
|
||||
return 451;
|
||||
}
|
||||
|
||||
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($isJson)
|
||||
return $this->uploadsCtx->convertToClientJsonV1($uploadInfo);
|
||||
|
||||
$filePath = $this->uploadsCtx->getFileDataPath($uploadInfo);
|
||||
if(!is_file($filePath)) {
|
||||
$response->setContent('Data is missing.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$isThumbnail) {
|
||||
$uploadsData->bumpUploadAccess($uploadInfo);
|
||||
$uploadsData->bumpUploadExpires($uploadInfo);
|
||||
}
|
||||
|
||||
$fileName = $uploadInfo->getName();
|
||||
$contentType = $uploadInfo->getMediaTypeString();
|
||||
|
||||
if($contentType === 'application/octet-stream' || str_starts_with($contentType, 'text/'))
|
||||
$contentType = 'text/plain';
|
||||
|
||||
if($isThumbnail) {
|
||||
if(!$this->uploadsCtx->supportsThumbnailing($uploadInfo))
|
||||
return 404;
|
||||
|
||||
$contentType = 'image/jpeg';
|
||||
$accelRedirectPath = $this->uploadsCtx->getThumbnailDataRedirectPathOrCreate($uploadInfo);
|
||||
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '-thumb.jpg';
|
||||
} else {
|
||||
$accelRedirectPath = $this->uploadsCtx->getFileDataRedirectPath($uploadInfo);
|
||||
}
|
||||
|
||||
$response->accelRedirect($accelRedirectPath);
|
||||
$response->setContentType($contentType);
|
||||
$response->setFileName(addslashes($fileName));
|
||||
}
|
||||
|
||||
#[Route('OPTIONS', '/uploads')]
|
||||
#[Route('POST', '/uploads')]
|
||||
public function postUpload($response, $request) {
|
||||
if($request->hasHeader('Origin'))
|
||||
$response->setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST');
|
||||
|
||||
if($request->getMethod() === 'OPTIONS')
|
||||
return 204;
|
||||
|
||||
if(!$request->isFormContent())
|
||||
return 400;
|
||||
|
||||
$content = $request->getContent();
|
||||
|
||||
try {
|
||||
$appInfo = $this->appsCtx->getApp($content->getParam('src', FILTER_VALIDATE_INT));
|
||||
} catch(RuntimeException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if(!$this->authInfo->isLoggedIn())
|
||||
return 401;
|
||||
|
||||
$userInfo = $this->authInfo->getUserInfo();
|
||||
if($userInfo->isRestricted())
|
||||
return 403;
|
||||
|
||||
try {
|
||||
$file = $content->getUploadedFile('file');
|
||||
} catch(RuntimeException $ex) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
$maxFileSize = $appInfo->getDataSizeLimit();
|
||||
if($appInfo->allowSizeMultiplier())
|
||||
$maxFileSize *= $userInfo->getDataSizeMultiplier();
|
||||
|
||||
$localFile = $file->getLocalFileName();
|
||||
$fileSize = filesize($localFile);
|
||||
|
||||
if($file->getSize() !== $fileSize || $fileSize > $maxFileSize) {
|
||||
$response->setHeader('Access-Control-Expose-Headers', 'X-EEPROM-Max-Size');
|
||||
$response->setHeader('X-EEPROM-Max-Size', $maxFileSize);
|
||||
return 413;
|
||||
}
|
||||
|
||||
$uploadsData = $this->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 = $uploadsData->getUpload(appInfo: $appInfo, userInfo: $userInfo, hashString: $hash)
|
||||
?? $uploadsData->getUpload(hashString: $hash);
|
||||
|
||||
if($uploadInfo !== null) {
|
||||
if($uploadInfo->isCopyrightTakedown())
|
||||
return 451;
|
||||
|
||||
if($uploadInfo->getUserId() !== $userInfo->getId()
|
||||
|| $uploadInfo->getAppId() !== $appInfo->getId())
|
||||
$uploadInfo = null;
|
||||
}
|
||||
|
||||
if($uploadInfo === null) {
|
||||
$uploadInfo = $uploadsData->createUpload(
|
||||
$appInfo, $userInfo, $_SERVER['REMOTE_ADDR'],
|
||||
$file->getSuggestedFileName(), mime_content_type($localFile),
|
||||
$fileSize, $hash, $appInfo->getBumpAmount(), true
|
||||
);
|
||||
$filePath = $this->uploadsCtx->getFileDataPath($uploadInfo);
|
||||
$file->moveTo($filePath);
|
||||
} else {
|
||||
$filePath = $this->uploadsCtx->getFileDataPath($uploadInfo);
|
||||
if($uploadInfo->isDeleted())
|
||||
$uploadsData->restoreUpload($uploadInfo);
|
||||
|
||||
$uploadsData->bumpUploadExpires($uploadInfo);
|
||||
}
|
||||
|
||||
$response->setStatusCode(201);
|
||||
$response->setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
return $this->uploadsCtx->convertToClientJsonV1($uploadInfo);
|
||||
}
|
||||
|
||||
#[Route('DELETE', '/uploads/:fileid')]
|
||||
public function deleteUpload($response, $request, string $fileId) {
|
||||
if(!$this->authInfo->isLoggedIn())
|
||||
return 401;
|
||||
|
||||
$uploadsData = $this->uploadsCtx->getUploadsData();
|
||||
|
||||
$uploadInfo = $uploadsData->getUpload(uploadId: $fileId);
|
||||
if($uploadInfo === null) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
if($uploadInfo->isCopyrightTakedown()) {
|
||||
$response->setContent('File is unavailable for copyright reasons.');
|
||||
return 451;
|
||||
}
|
||||
|
||||
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
|
||||
$response->setContent('File not found.');
|
||||
return 404;
|
||||
}
|
||||
|
||||
$userInfo = $this->authInfo->getUserInfo();
|
||||
if($userInfo->isRestricted() || $userInfo->getId() !== $uploadInfo->getUserId())
|
||||
return 403;
|
||||
|
||||
$uploadsData->deleteUpload($uploadInfo);
|
||||
return 204;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue