eeprom/src/Uploads/UploadsRoutes.php

231 lines
8.1 KiB
PHP

<?php
namespace EEPROM\Uploads;
use RuntimeException;
use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,IRouter,IRouteHandler};
use EEPROM\Apps\AppsContext;
use EEPROM\Auth\AuthInfo;
use EEPROM\Blacklist\BlacklistContext;
use EEPROM\Uploads\UploadsContext;
class UploadsRoutes implements IRouteHandler {
public function __construct(
private AuthInfo $authInfo,
private AppsContext $appsCtx,
private UploadsContext $uploadsCtx,
private BlacklistContext $blacklistCtx,
private bool $isApiDomain
) {}
public function registerRoutes(IRouter $router): void {
if($this->isApiDomain) {
HandlerAttribute::register($router, $this);
} else {
$router->options('/', $this->getUpload(...));
$router->get('/([A-Za-z0-9\-_]+)(?:\.(t))?', $this->getUpload(...));
}
}
#[HttpOptions('/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?')]
#[HttpGet('/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?')]
public function getUpload($response, $request, string $fileId, string $fileExt = '') {
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;
$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;
}
$blInfo = $this->blacklistCtx->getBlacklistEntry($uploadInfo);
if($blInfo !== null) {
$response->setContent(match($blInfo->getReason()) {
'copyright' => 'File is unavailable for copyright reasons.',
'rules' => 'File was in violation of the rules.',
default => 'File was removed for reasons beyond understanding.',
});
return $blInfo->isCopyrightTakedown() ? 451 : 410;
}
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->updateUpload(
$uploadInfo,
accessedAt: true,
expiresAt: $uploadInfo->getBumpAmountForUpdate(),
);
$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));
}
#[HttpOptions('/uploads')]
#[HttpPost('/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((string)$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);
$blInfo = $this->blacklistCtx->getBlacklistEntry(hex2bin($hash));
if($blInfo !== null)
return 451;
$fileName = $file->getSuggestedFileName();
$uploadInfo = $uploadsData->getUpload(appInfo: $appInfo, userInfo: $userInfo, hashString: $hash);
if($uploadInfo === null) {
$uploadInfo = $uploadsData->createUpload(
$appInfo, $userInfo, $_SERVER['REMOTE_ADDR'],
$fileName, 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->updateUpload(
$uploadInfo,
fileName: $fileName,
expiresAt: $uploadInfo->getBumpAmountForUpdate(),
);
}
$response->setStatusCode(201);
$response->setHeader('Content-Type', 'application/json; charset=utf-8');
return $this->uploadsCtx->convertToClientJsonV1($uploadInfo, [
'name' => $fileName,
]);
}
#[HttpDelete('/uploads/([A-Za-z0-9\-_]+)')]
public function deleteUpload($response, $request, string $fileId) {
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');
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->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;
}
}