eeprom/public/index.php

414 lines
14 KiB
PHP

<?php
namespace EEPROM;
use RuntimeException;
use Index\Http\HttpFx;
require_once __DIR__ . '/../eeprom.php';
set_exception_handler(function(\Throwable $ex) {
\Sentry\captureException($ex);
ob_clean();
http_response_code(500);
if(PRM_DEBUG) {
header('Content-Type: text/plain; charset=utf-8');
echo (string)$ex;
} else echo '500';
exit;
});
function eepromOriginAllowed(string $origin): bool {
global $cfg;
$origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
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();