466 lines
13 KiB
PHP
466 lines
13 KiB
PHP
|
<?php
|
||
|
namespace EEPROM;
|
||
|
|
||
|
require_once __DIR__ . '/../startup.php';
|
||
|
|
||
|
$reqMethod = $_SERVER['REQUEST_METHOD'];
|
||
|
$reqPath = '/' . trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
|
||
|
|
||
|
header('X-Powered-By: EEPROM');
|
||
|
|
||
|
function eepromOriginAllowed(string $origin): bool {
|
||
|
$origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
|
||
|
|
||
|
if($origin === $_SERVER['HTTP_HOST'])
|
||
|
return true;
|
||
|
|
||
|
$allowed = Config::get('CORS', 'origins', []);
|
||
|
if(empty($allowed))
|
||
|
return true;
|
||
|
|
||
|
return in_array($origin, $allowed);
|
||
|
}
|
||
|
|
||
|
function eepromByteSymbol(int $bytes, bool $decimal = true, array $symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']): string {
|
||
|
if($bytes < 1) {
|
||
|
return '0 B';
|
||
|
}
|
||
|
|
||
|
$divider = $decimal ? 1000 : 1024;
|
||
|
$exp = floor(log($bytes) / log($divider));
|
||
|
$bytes = $bytes / pow($divider, floor($exp));
|
||
|
$symbol = $symbols[$exp];
|
||
|
|
||
|
return sprintf("%.2f %s%sB", $bytes, $symbol, $symbol !== '' && !$decimal ? 'i' : '');
|
||
|
}
|
||
|
|
||
|
if($_SERVER['HTTP_HOST'] === Config::get('Uploads', 'short_domain')) {
|
||
|
$reqMethod = 'GET'; // short domain is read only, prevent deleting
|
||
|
$reqPath = '/uploads/' . trim($reqPath, '/');
|
||
|
$isShortDomain = true;
|
||
|
}
|
||
|
|
||
|
if(!empty($_SERVER['HTTP_ORIGIN'])) {
|
||
|
if(!eepromOriginAllowed($_SERVER['HTTP_ORIGIN'])) {
|
||
|
http_response_code(403);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
|
||
|
header('Vary: Origin');
|
||
|
}
|
||
|
|
||
|
if($reqMethod === 'OPTIONS') {
|
||
|
http_response_code(204);
|
||
|
if(isset($isShortDomain))
|
||
|
header('Access-Control-Allow-Methods: OPTIONS, GET');
|
||
|
else {
|
||
|
header('Access-Control-Allow-Credentials: true');
|
||
|
header('Access-Control-Allow-Headers: Authorization');
|
||
|
header('Access-Control-Allow-Methods: OPTIONS, GET, POST, DELETE');
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
function connectMszDatabase(): \PDO {
|
||
|
global $mszPdo;
|
||
|
|
||
|
if($mszPdo)
|
||
|
return $mszPdo;
|
||
|
|
||
|
$configPath = Config::get('Misuzu', 'config', '');
|
||
|
|
||
|
if(!is_file($configPath))
|
||
|
throw new \Exception('Cannot find Misuzu configuration.');
|
||
|
|
||
|
$config = parse_ini_file($configPath, true)['Database'];
|
||
|
$dsn = ($config['driver'] ?? 'mysql') . ':';
|
||
|
|
||
|
foreach($config as $key => $value) {
|
||
|
if($key === 'driver' || $key === 'username' || $key === 'password')
|
||
|
continue;
|
||
|
if($key === 'database')
|
||
|
$key = 'dbname';
|
||
|
|
||
|
$dsn .= $key . '=' . $value . ';';
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$mszPdo = new \PDO($dsn, $config['username'], $config['password'], DB::FLAGS);
|
||
|
} catch(\PDOException $ex) {
|
||
|
throw new \Exception('Unable to connect to Misuzu database.');
|
||
|
}
|
||
|
|
||
|
return $mszPdo;
|
||
|
}
|
||
|
|
||
|
function checkSockChatAuth(string $token): int {
|
||
|
if(strpos($token, '_') === false)
|
||
|
return -1;
|
||
|
|
||
|
$mszPdo = connectMszDatabase();
|
||
|
$tokenParts = explode('_', $token, 2);
|
||
|
$userId = intval($tokenParts[0] ?? 0);
|
||
|
$chatToken = strval($tokenParts[1] ?? '');
|
||
|
|
||
|
$getUserId = $mszPdo->prepare('
|
||
|
SELECT `user_id`
|
||
|
FROM `msz_user_chat_tokens`
|
||
|
WHERE `user_id` = :user
|
||
|
AND `token_string` = :token
|
||
|
AND `token_created` > NOW() - INTERVAL 1 WEEK
|
||
|
');
|
||
|
$getUserId->bindValue('user', $userId);
|
||
|
$getUserId->bindValue('token', $chatToken);
|
||
|
$getUserId->execute();
|
||
|
|
||
|
return (int)$getUserId->fetchColumn();
|
||
|
}
|
||
|
|
||
|
function checkMszAuth(string $token): int {
|
||
|
$packed = Base64::decode($token, true);
|
||
|
$packed = str_pad($packed, 37, "\x00");
|
||
|
$unpacked = unpack('Cversion/Nuser/H64token', $packed);
|
||
|
|
||
|
if($unpacked['version'] !== 1)
|
||
|
return -1;
|
||
|
|
||
|
$getUserId = connectMszDatabase()->prepare('
|
||
|
SELECT `user_id`
|
||
|
FROM `msz_sessions`
|
||
|
WHERE `user_id` = :user
|
||
|
AND `session_key` = :token
|
||
|
AND `session_expires` > NOW()
|
||
|
');
|
||
|
$getUserId->bindValue('user', $unpacked['user']);
|
||
|
$getUserId->bindValue('token', $unpacked['token']);
|
||
|
$getUserId->execute();
|
||
|
|
||
|
return (int)$getUserId->fetchColumn();
|
||
|
}
|
||
|
|
||
|
if(!isset($isShortDomain) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
|
||
|
$authParts = explode(' ', $_SERVER['HTTP_AUTHORIZATION'], 2);
|
||
|
$authMethod = strval($authParts[0] ?? '');
|
||
|
$authToken = strval($authParts[1] ?? '');
|
||
|
|
||
|
switch($authMethod) {
|
||
|
case 'SockChat':
|
||
|
$authUserId = checkSockChatAuth($authToken);
|
||
|
break;
|
||
|
case 'Misuzu':
|
||
|
$authUserId = checkMszAuth($authToken);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if(isset($authUserId) && $authUserId > 0)
|
||
|
User::byId($authUserId)->setActive();
|
||
|
}
|
||
|
|
||
|
if(preg_match('#^/uploads/([a-zA-Z0-9-_]{32})(\.t)?/?$#', $reqPath, $matches)) {
|
||
|
$getNormal = empty($matches[2]);
|
||
|
$getThumbnail = isset($matches[2]) && $matches[2] === '.t';
|
||
|
|
||
|
try {
|
||
|
$uploadInfo = Upload::byId($matches[1]);
|
||
|
} catch(UploadNotFoundException $ex) {
|
||
|
http_response_code(404);
|
||
|
echo 'File not found.';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($uploadInfo->isDMCA()) {
|
||
|
http_response_code(451);
|
||
|
echo 'File is unavailable for copyright reasons.';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
|
||
|
http_response_code(404);
|
||
|
echo 'File not found.';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($reqMethod === 'DELETE') {
|
||
|
if(!User::hasActive()) {
|
||
|
http_response_code(401);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(User::active()->isRestricted()
|
||
|
|| User::active()->getId() !== $uploadInfo->getUserId()) {
|
||
|
http_response_code(403);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
http_response_code(204);
|
||
|
$uploadInfo->delete(false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(!is_file($uploadInfo->getPath())) {
|
||
|
http_response_code(404);
|
||
|
echo 'Data is missing.';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($getNormal) {
|
||
|
$uploadInfo->bumpAccess();
|
||
|
$uploadInfo->bumpExpiry();
|
||
|
}
|
||
|
|
||
|
$contentType = $uploadInfo->getType();
|
||
|
|
||
|
if($contentType === 'application/octet-stream' || substr($contentType, 0, 5) === 'text/')
|
||
|
$contentType = 'text/plain';
|
||
|
|
||
|
$sourceDir = basename($getThumbnail ? PRM_THUMBS : PRM_UPLOADS);
|
||
|
|
||
|
if($getThumbnail) {
|
||
|
if(substr($contentType, 0, 6) !== 'image/') {
|
||
|
http_response_code(404);
|
||
|
echo 'Thumbnails are not supported for this filetype.';
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$imagick = new \Imagick($uploadInfo->getPath());
|
||
|
$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;
|
||
|
}
|
||
|
|
||
|
$imagick->resizeImage(
|
||
|
$resizeWidth, $resizeHeight,
|
||
|
\Imagick::FILTER_GAUSSIAN, 0.7
|
||
|
);
|
||
|
|
||
|
$imagick->cropImage(
|
||
|
$thumbRes,
|
||
|
$thumbRes,
|
||
|
($resizeWidth - $thumbRes) / 2,
|
||
|
($resizeHeight - $thumbRes) / 2
|
||
|
);
|
||
|
|
||
|
$imagick->writeImage($uploadInfo->getThumbPath());
|
||
|
} catch(\Exception $ex) {}
|
||
|
}
|
||
|
|
||
|
header(sprintf('X-Accel-Redirect: /%s/%s', $sourceDir, $uploadInfo->getId()));
|
||
|
header(sprintf('Content-Type: %s', $contentType));
|
||
|
header(sprintf('Content-Disposition: inline; filename="%s"', addslashes($uploadInfo->getName())));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(preg_match('#^/uploads/([a-zA-Z0-9-_]{32})\.json/?$#', $reqPath, $matches)) {
|
||
|
if(isset($isShortDomain)) {
|
||
|
http_response_code(404);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$uploadInfo = Upload::byId($matches[1]);
|
||
|
} catch(UploadNotFoundException $ex) {
|
||
|
http_response_code(404);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
header('Content-Type: application/json; charset=utf-8');
|
||
|
echo json_encode($uploadInfo);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
header('Content-Type: text/plain; charset=us-ascii');
|
||
|
|
||
|
if($reqPath === '/' || $reqPath === '/stats' || $reqPath === '/html') {
|
||
|
$fileCount = 0;
|
||
|
$userCount = 0;
|
||
|
$totalSize = 0;
|
||
|
$uniqueTypes = 0;
|
||
|
|
||
|
$getUploadStats = DB::prepare('
|
||
|
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
|
||
|
');
|
||
|
$getUploadStats->execute();
|
||
|
$uploadStats = $getUploadStats->execute() ? $getUploadStats->fetchObject() : null;
|
||
|
|
||
|
if(!empty($uploadStats)) {
|
||
|
$fileCount = intval($uploadStats->amount);
|
||
|
$totalSize = intval($uploadStats->size ?? 0);
|
||
|
$uniqueTypes = intval($uploadStats->types ?? 0);
|
||
|
}
|
||
|
|
||
|
$getUserStats = DB::prepare('
|
||
|
SELECT COUNT(`user_id`) AS `amount`
|
||
|
FROM `prm_users`
|
||
|
WHERE `user_restricted` IS NULL
|
||
|
');
|
||
|
$getUserStats->execute();
|
||
|
$userStats = $getUserStats->execute() ? $getUserStats->fetchObject() : null;
|
||
|
|
||
|
if(!empty($userStats)) {
|
||
|
$userCount = intval($userStats->amount);
|
||
|
}
|
||
|
|
||
|
if($reqPath === '/stats') {
|
||
|
header('Content-Type: application/json; charset=utf-8');
|
||
|
echo json_encode([
|
||
|
'files_size' => $totalSize,
|
||
|
'files_count' => $fileCount,
|
||
|
'files_types' => $uniqueTypes,
|
||
|
'users_count' => $userCount,
|
||
|
]);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$totalSizeFmt = eepromByteSymbol($totalSize);
|
||
|
|
||
|
if($reqPath === '/html') {
|
||
|
header('Content-Type: text/html; charset=utf-8');
|
||
|
echo <<<HTML
|
||
|
<!doctype html>
|
||
|
<html>
|
||
|
<head>
|
||
|
<meta charset="utf-8"/>
|
||
|
<title>Flashii EEPROM</title>
|
||
|
<style type="text/css">
|
||
|
</style>
|
||
|
</head>
|
||
|
<body>
|
||
|
<pre>
|
||
|
________________ ____ ____ __ ___
|
||
|
/ ____/ ____/ __ \/ __ \/ __ \/ |/ /
|
||
|
/ __/ / __/ / /_/ / /_/ / / / / /|_/ /
|
||
|
/ /___/ /___/ ____/ _, _/ /_/ / / / /
|
||
|
/_____/_____/_/ /_/ |_|\____/_/ /_/
|
||
|
|
||
|
Currently serving {$totalSizeFmt} ({$totalSize} bytes) of {$fileCount} files in {$uniqueTypes} unique file types from {$userCount} users.
|
||
|
</pre>
|
||
|
</body>
|
||
|
</html>
|
||
|
HTML;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
echo <<<ASCII
|
||
|
________________ ____ ____ __ ___
|
||
|
/ ____/ ____/ __ \/ __ \/ __ \/ |/ /
|
||
|
/ __/ / __/ / /_/ / /_/ / / / / /|_/ /
|
||
|
/ /___/ /___/ ____/ _, _/ /_/ / / / /
|
||
|
/_____/_____/_/ /_/ |_|\____/_/ /_/
|
||
|
|
||
|
Currently serving {$totalSizeFmt} ({$totalSize} bytes) of {$fileCount} files in {$uniqueTypes} unique file types from {$userCount} users.
|
||
|
\r\n
|
||
|
ASCII;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($reqPath === '/uploads') {
|
||
|
if($reqMethod !== 'POST') {
|
||
|
http_response_code(405);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$appInfo = Application::byId(
|
||
|
filter_input(INPUT_POST, 'src', FILTER_VALIDATE_INT)
|
||
|
);
|
||
|
} catch(ApplicationNotFoundException $ex) {
|
||
|
http_response_code(404);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(!User::hasActive()) {
|
||
|
http_response_code(401);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$userInfo = User::active();
|
||
|
|
||
|
if($userInfo->isRestricted()) {
|
||
|
http_response_code(403);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(empty($_FILES['file']['tmp_name']) || !is_file($_FILES['file']['tmp_name'])) {
|
||
|
http_response_code(400);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$maxFileSize = $appInfo->getSizeLimit();
|
||
|
if($appInfo->allowSizeMultiplier())
|
||
|
$maxFileSize *= $userInfo->getSizeMultiplier();
|
||
|
|
||
|
$fileSize = filesize($_FILES['file']['tmp_name']);
|
||
|
|
||
|
if($_FILES['file']['size'] !== $fileSize || $fileSize > $maxFileSize) {
|
||
|
http_response_code(413);
|
||
|
header('Access-Control-Expose-Headers: X-EEPROM-Max-Size');
|
||
|
header('X-EEPROM-Max-Size: ' . $maxFileSize);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$hash = hash_file('sha256', $_FILES['file']['tmp_name']);
|
||
|
$fileInfo = Upload::byHash($hash);
|
||
|
|
||
|
if($fileInfo !== null) {
|
||
|
if($fileInfo->isDMCA()) {
|
||
|
http_response_code(451);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($fileInfo->getUserId() !== $userInfo->getId()
|
||
|
|| $fileInfo->getApplicationId() !== $appInfo->getId())
|
||
|
unset($fileInfo);
|
||
|
}
|
||
|
|
||
|
if(!empty($fileInfo)) {
|
||
|
if($fileInfo->isDeleted())
|
||
|
$fileInfo->restore();
|
||
|
} else {
|
||
|
try {
|
||
|
$fileInfo = Upload::create(
|
||
|
$appInfo, $userInfo,
|
||
|
$_FILES['file']['name'],
|
||
|
mime_content_type($_FILES['file']['tmp_name']),
|
||
|
$fileSize, $hash,
|
||
|
$appInfo->getExpiry(), true
|
||
|
);
|
||
|
} catch(UploadCreationFailedException $ex) {
|
||
|
http_response_code(500);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(!move_uploaded_file($_FILES['file']['tmp_name'], $fileInfo->getPath())) {
|
||
|
http_response_code(500);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
http_response_code(201);
|
||
|
header('Content-Type: application/json; charset=utf-8');
|
||
|
echo json_encode($fileInfo);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(is_file('../_manage.php') && include_once '../_manage.php')
|
||
|
return;
|
||
|
|
||
|
http_response_code(404);
|