Import EEPROM.

This commit is contained in:
flash 2020-05-08 22:53:21 +00:00
commit 0f23bb2106
13 changed files with 1691 additions and 0 deletions

0
.debug Normal file
View file

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
public/data/*
public/thumb/*
config.ini

456
_manage.php Normal file
View file

@ -0,0 +1,456 @@
<?php
namespace EEPROM;
function mszDieIfNotAuth(): void {
if(!User::hasActive()) {
$mszUserId = checkMszAuth(strval(filter_input(INPUT_COOKIE, 'msz_auth')));
if($mszUserId > 0)
User::byId($mszUserId)->setActive();
}
if(!User::hasActive() || User::active()->getId() !== 1) {
http_response_code(403);
header('Content-Type: text/html; charset=utf-8');
echo <<<HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Access Denied</title>
<style>
body {
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
background-color: #111;
text-align: center;
color: #fff;
}
h1 { margin: 2em; }
</style>
</head>
<body>
<h1>Access Denied</h1>
<p><img src="//static.flash.moe/images/access-denied-mkt.png" alt="Access Denied"/></p>
</body>
</html>
HTML;
exit;
}
}
if($reqPath === '/flash') {
mszDieIfNotAuth();
header('Content-Type: text/html; charset=utf-8');
echo <<<HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>EEPROM Flash</title>
<link href="/flash/style.css" type="text/css" rel="stylesheet"/>
</head>
<body class="initial">
<noscript><div class="nojs">Enable Javascript!</div></noscript>
<div class="header">
<div>
<h1>EEPROM Flash</h1>
<nav>
<div id="_nav_users">Users <span>...</span></div>
<div id="_nav_appls">Applications <span>...</span></div>
<div id="_nav_uplds">Uploads <span>...</span></div>
</nav>
</div>
</div>
<div class="wrapper">
<div class="hidden" id="_cnt_users">
<h2>Users</h2>
<div id="_cnt_users_list" class="list users-list">
<div class="list-item users-list-item">
<div></div>
<div>ID</div>
<div>Size Multiplier</div>
<div>Created</div>
<div>Restricted</div>
</div>
</div>
</div>
<div class="hidden" id="_cnt_appls">
<h2>Applications</h2>
<div id="_cnt_appls_list" class="list appls-list">
<div class="list-item appls-list-item">
<div></div>
<div>ID</div>
<div>Name</div>
<div>Size Limit</div>
<div>Size Multiplier Allowed</div>
<div>File Lifetime (Seconds)</div>
<div>Created</div>
</div>
</div>
</div>
<div class="hidden" id="_cnt_uplds">
<h2>Uploads</h2>
<div id="_cnt_uplds_list" class="list uplds-list">
<div class="list-item uplds-list-item">
<div></div>
<div>ID</div>
<div>Application</div>
<div>Uploader</div>
<div>Name</div>
<div>Size</div>
<div>Type</div>
<div>Created</div>
<div>Deleted</div>
<div>DMCA'd</div>
<div>Accessed</div>
<div>Expires</div>
</div>
</div>
</div>
</div>
<script src="/flash/script.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
HTML;
return true;
}
if($reqPath === '/flash/stats.json') {
mszDieIfNotAuth();
header('Content-Type: application/json; charset=utf-8');
$getStats = DB::prepare('
SELECT (
SELECT COUNT(`app_id`)
FROM `prm_applications`
) AS `applications`, (
SELECT COUNT(`upload_id`)
FROM `prm_uploads`
) AS `uploads`, (
SELECT COUNT(`user_id`)
FROM `prm_users`
) AS `users`
');
$getStats->execute();
echo json_encode($getStats->fetchObject());
return true;
}
if($reqPath === '/flash/users.json') {
mszDieIfNotAuth();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(User::all(10, intval(filter_input(INPUT_GET, 'after', FILTER_SANITIZE_NUMBER_INT))));
return true;
}
if($reqPath === '/flash/uploads.json') {
mszDieIfNotAuth();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(Upload::all(20, strval(filter_input(INPUT_GET, 'after'))));
return true;
}
if($reqPath === '/flash/applications.json') {
mszDieIfNotAuth();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(Application::all(10, intval(filter_input(INPUT_GET, 'after', FILTER_SANITIZE_NUMBER_INT))));
return true;
}
if($reqPath === '/flash/style.css') {
mszDieIfNotAuth();
header('Content-Type: text/css; charset=utf-8');
echo <<<STYLE
* {
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
outline-style: none !important;
}
html, body {
width: 100%;
height: 100%;
}
body {
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
background-color: #111;
color: #fff;
margin-top: -1px;
padding-top: 1px;
}
.nojs {
border: 5px solid #f00;
background-color: #c00;
color: #fff;
font-weight: 700;
font-size: 1.5em;
margin: 10px;
padding: 10px;
}
.hidden {
display: none !important;
visibility: hidden !important;
}
.header {
display: flex;
flex-direction: column;
justify-content: center;
background-color: #191919;
position: absolute;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100px;
padding: 0 10px;
transition: height .5s;
}
.initial .header {
width: 100%;
height: 100%;
}
.header h1 {
margin: 10px;
}
.header nav {
display: flex;
margin: 0 5px;
}
.header nav > div {
flex: 0 0 auto;
padding: 2px 10px;
margin: 1px;
background-color: #222;
border-radius: 20px;
cursor: pointer;
transition: background-color .2s;
}
.header nav > div > span {
font-weight: 700;
}
.header nav > div:hover,
.header nav > div:focus {
background-color: #444;
}
.header nav > div:active,
.header nav > .active {
background-color: #292929;
}
.wrapper {
padding-top: 100px;
}
.wrapper h2 {
margin: 10px;
}
.list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
}
.list-item > div {
flex: 1 1 auto;
}
STYLE;
return true;
}
if($reqPath === '/flash/script.js') {
mszDieIfNotAuth();
header('Content-Type: application/javascript; charset=utf-8');
echo <<<SCRIPT
var objStats,
navActive, cntActive,
navUsers, cntUsers,
navAppls, cntAppls,
navUplds, cntUplds;
function switchContainer(target, button) {
if(cntActive)
cntActive.classList.add('hidden');
if(navActive)
navActive.classList.remove('active');
cntActive = target;
cntActive.classList.remove('hidden');
navActive = button;
navActive.classList.add('active');
document.body.classList.remove('initial');
}
function renderUserList(list) {
var target = document.getElementById('_cnt_users_list');
while(target.children.length > 1)
target.removeChild(target.lastElementChild);
for(var i = 0; i < list.length; ++i) {
var info = list[i],
elem = target.appendChild(document.createElement('div'));
elem.className = 'list-item users-list-item';
var actField = elem.appendChild(document.createElement('div'));
actField.textContent = 'Actions';
var idField = elem.appendChild(document.createElement('div'));
idField.textContent = info.id.toString();
var smField = elem.appendChild(document.createElement('div'));
smField.textContent = info.size_multi.toString();
var crField = elem.appendChild(document.createElement('div'));
crField.textContent = info.created.toString();
var rsField = elem.appendChild(document.createElement('div'));
rsField.textContent = !info.restricted ? 'No' : info.restricted.toString();
}
}
function renderApplList(list) {
var target = document.getElementById('_cnt_appls_list');
while(target.children.length > 1)
target.removeChild(target.lastElementChild);
for(var i = 0; i < list.length; ++i) {
var info = list[i],
elem = target.appendChild(document.createElement('div'));
elem.className = 'list-item appls-list-item';
var actField = elem.appendChild(document.createElement('div'));
actField.textContent = 'Actions';
var idField = elem.appendChild(document.createElement('div'));
idField.textContent = info.id.toString();
var nmField = elem.appendChild(document.createElement('div'));
nmField.textContent = info.name.toString();
var slField = elem.appendChild(document.createElement('div'));
slField.textContent = info.size_limit.toString();
var smField = elem.appendChild(document.createElement('div'));
smField.textContent = info.size_multi ? 'Yes' : 'No';
var flField = elem.appendChild(document.createElement('div'));
flField.textContent = info.expiry.toString();
var crField = elem.appendChild(document.createElement('div'));
crField.textContent = info.created.toString();
}
}
function renderUpldList(list) {
var target = document.getElementById('_cnt_uplds_list');
while(target.children.length > 1)
target.removeChild(target.lastElementChild);
for(var i = 0; i < list.length; ++i) {
var info = list[i],
elem = target.appendChild(document.createElement('div'));
elem.className = 'list-item uplds-list-item';
var actField = elem.appendChild(document.createElement('div'));
actField.textContent = 'Actions';
var idField = elem.appendChild(document.createElement('div'));
idField.textContent = info.id.toString();
var apField = elem.appendChild(document.createElement('div'));
apField.textContent = info.appl.toString();
var usField = elem.appendChild(document.createElement('div'));
usField.textContent = info.user.toString();
var nmField = elem.appendChild(document.createElement('div'));
nmField.textContent = info.name.toString();
var szField = elem.appendChild(document.createElement('div'));
szField.textContent = info.size.toString();
var ftField = elem.appendChild(document.createElement('div'));
ftField.textContent = info.type.toString();
var crField = elem.appendChild(document.createElement('div'));
crField.textContent = info.created.toString();
var dlField = elem.appendChild(document.createElement('div'));
dlField.textContent = !info.deleted ? 'No' : info.deleted.toString();
var dmField = elem.appendChild(document.createElement('div'));
dmField.textContent = !info.dmca ? 'No' : info.dmca.toString();
var acField = elem.appendChild(document.createElement('div'));
acField.textContent = !info.accessed ? 'No' : info.accessed.toString();
var exField = elem.appendChild(document.createElement('div'));
exField.textContent = !info.expires ? 'No' : info.expires.toString();
}
}
function switchContUsers(ev) {
switchContainer(cntUsers, this);
loadPageData('users', null, renderUserList);
}
function switchContAppls(ev) {
switchContainer(cntAppls, this);
loadPageData('applications', null, renderApplList);
}
function switchContUplds(ev) {
switchContainer(cntUplds, this);
loadPageData('uploads', null, renderUpldList);
}
function loadPageData(src, after, callback) {
var url = '/flash/' + encodeURI(src.toString()) + '.json';
if(after)
url += '?after=' + encodeURIComponent(after.toString());
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function() {
if(xhr.readyState !== 4)
return;
var json = JSON.parse(xhr.responseText);
console.log(json);
if(json && callback)
callback(json);
};
xhr.open('GET', url);
xhr.send();
}
function refreshNavStats(callback) {
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function() {
if(xhr.readyState !== 4)
return;
var stats = JSON.parse(xhr.responseText);
if(!stats)
return;
objStats = stats;
if(callback)
callback.call(this);
navUsers.querySelector('span').textContent = objStats.users;
navAppls.querySelector('span').textContent = objStats.applications;
navUplds.querySelector('span').textContent = objStats.uploads;
}.bind(this);
xhr.open('GET', '/flash/stats.json');
xhr.send();
};
window.addEventListener('load', function() {
setInterval(refreshNavStats, 30000);
refreshNavStats(function() {
navUsers = document.getElementById('_nav_users');
cntUsers = document.getElementById('_cnt_users');
navUsers.onclick = switchContUsers;
navAppls = document.getElementById('_nav_appls');
cntAppls = document.getElementById('_cnt_appls');
navAppls.onclick = switchContAppls;
navUplds = document.getElementById('_nav_uplds');
cntUplds = document.getElementById('_cnt_uplds');
navUplds.onclick = switchContUplds;
}.bind(this));
});
SCRIPT;
return true;
}

29
cron.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace EEPROM;
if(!defined('EEPROM_SEM_NAME'))
define('EEPROM_SEM_NAME', 'c');
if(!defined('EEPROM_SFM_PATH'))
define('EEPROM_SFM_PATH', sys_get_temp_dir() . DIRECTORY_SEPARATOR . '{6bf19abb-ae7e-4a1d-85f9-00dfb7c90264}');
if(!is_file(EEPROM_SFM_PATH))
touch(EEPROM_SFM_PATH);
$ftok = ftok(EEPROM_SFM_PATH, EEPROM_SEM_NAME);
$semaphore = sem_get($ftok, 1);
if(!sem_acquire($semaphore))
die('Failed to acquire semaphore.' . PHP_EOL);
require_once __DIR__ . '/startup.php';
// Mark expired as deleted
$expired = Upload::expired();
foreach($expired as $upload)
$upload->delete(false);
// Hard delete soft deleted files
$deleted = Upload::deleted();
foreach($deleted as $upload)
$upload->delete(true);
sem_release($semaphore);

465
public/index.php Normal file
View file

@ -0,0 +1,465 @@
<?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);

89
src/Application.php Normal file
View file

@ -0,0 +1,89 @@
<?php
namespace EEPROM;
use Exception;
use JsonSerializable;
class ApplicationNotFoundException extends Exception {}
final class Application implements JsonSerializable {
public function getId(): int {
return $this->app_id ?? 0;
}
public function getName(): string {
return $this->app_name ?? '';
}
public function getCreated(): int {
return $this->app_created ?? 0;
}
public function getSizeLimit(): int {
return $this->app_size_limit ?? -1;
}
public function allowSizeMultiplier(): bool {
return !empty($this->app_allow_size_multiplier);
}
public function getExpiry(): int {
return $this->app_expiry ?? -1;
}
public function jsonSerialize() {
return [
'id' => $this->getId(),
'name' => $this->getName(),
'size_limit' => $this->getSizeLimit(),
'size_multi' => $this->allowSizeMultiplier(),
'expiry' => $this->getExpiry(),
'created' => date('c', $this->getCreated()),
];
}
public static function byId(int $appId): self {
if($appId < 1)
throw new ApplicationNotFoundException;
$getApplication = DB::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` = :app
');
$getApplication->bindValue('app', $appId);
$getApplication->execute();
$application = $getApplication->fetchObject(self::class);
if($application === false)
throw new ApplicationNotFoundException;
return $application;
}
public static function all(int $limit = 0, int $after = 0): array {
$query = '
SELECT `app_id`, `app_name`, `app_size_limit`, `app_expiry`, `app_allow_size_multiplier`,
UNIX_TIMESTAMP(`app_created`) AS `app_created`
FROM `prm_applications`
';
if($after > 0)
$query .= sprintf(' WHERE `app_id` > %d', $after);
$query .= ' ORDER BY `app_id`';
if($limit > 0)
$query .= sprintf(' LIMIT %d', $limit);
$getAppls = DB::prepare($query);
$getAppls->execute();
$out = [];
while($appl = $getAppls->fetchObject(self::class))
$out[] = $appl;
return $out;
}
}

28
src/Base64.php Normal file
View file

@ -0,0 +1,28 @@
<?php
namespace EEPROM;
final class Base64 {
public static function encode(string $data, bool $url = false): string {
$data = base64_encode($data);
if($url)
$data = rtrim(strtr($data, '+/', '-_'), '=');
return $data;
}
public static function decode(string $data, bool $url = false): string {
if($url)
$data = str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT);
return base64_decode($data);
}
public static function jsonEncode($data): string {
return self::encode(json_encode($data), true);
}
public static function jsonDecode(string $data) {
return json_decode(self::decode($data, true));
}
}

23
src/Config.php Normal file
View file

@ -0,0 +1,23 @@
<?php
namespace EEPROM;
final class Config {
private static array $config = [];
public static function load(string $path): void {
$config = parse_ini_file($path, true, INI_SCANNER_TYPED);
if(!empty($config))
self::$config = array_merge(self::$config, $config);
}
public static function get(string $section, string $key, $default = null) {
if(!self::has($section, $key))
return $default;
return self::$config[$section][$key];
}
public static function has(string $section, string $key) {
return array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section]) && !empty(self::$config[$section][$key]);
}
}

32
src/DB.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace EEPROM;
use PDO;
use PDOStatement;
use PDOException;
final class DB {
public const FLAGS = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_INIT_COMMAND => "
SET SESSION
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
time_zone = '+00:00';
",
];
public static $instance = null;
public static function init(...$args) {
self::$instance = new PDO(...$args);
}
public static function __callStatic(string $name, array $args) {
return self::$instance->{$name}(...$args);
}
}

390
src/Upload.php Normal file
View file

@ -0,0 +1,390 @@
<?php
namespace EEPROM;
use Exception;
use JsonSerializable;
class UploadNotFoundException extends Exception {};
class UploadCreationFailedException extends Exception {};
final class Upload implements JsonSerializable {
private const ID_CHARS = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789-_';
public function getId(): string {
return $this->upload_id;
}
public function getPath(): string {
return PRM_UPLOADS . '/' . $this->getId();
}
public function getThumbPath(): string {
return PRM_THUMBS . '/' . $this->getId();
}
public function getRemotePath(): string {
return '/uploads/' . $this->getId();
}
public function getPublicUrl(bool $forceReal = false): string {
if(!$forceReal && Config::has('Uploads', 'short_domain'))
return '//' . Config::get('Uploads', 'short_domain') . '/' . $this->getId();
return '//' . $_SERVER['HTTP_HOST'] . $this->getRemotePath();
}
public function getPublicThumbUrl(bool $forceReal = false): string {
return $this->getPublicUrl($forceReal) . '.t';
}
public function getUserId(): int {
return $this->user_id ?? 0;
}
public function setUserId(int $userId): void {
$this->user_id = $userId;
}
public function getUser(): User {
return User::byId($this->getUserId());
}
public function setUser(User $user): void {
$this->setUserId($user->getId());
}
public function getApplicationId(): int {
return $this->app_id ?? 0;
}
public function setApplicationId(int $appId): void {
$this->app_id = $appId;
$updateAppl = DB::prepare('
UPDATE `prm_uploads`
SET `app_id` = :app
WHERE `upload_id` = :upload
');
$updateAppl->bindValue('app', $appId);
$updateAppl->bindValue('upload', $this->getId());
$updateAppl->execute();
}
public function getApplication(): User {
return Application::byId($this->getApplicationId());
}
public function setApplication(Application $app): void {
$this->setApplicationId($app->getId());
}
public function getType(): string {
return $this->upload_type ?? 'text/plain';
}
public function getName(): string {
return $this->upload_name ?? '';
}
public function getSize(): int {
return $this->upload_size ?? 0;
}
public function getHash(): string {
return $this->upload_hash ?? str_pad('', 64, '0');
}
public function getCreated(): int {
return $this->upload_created;
}
public function getLastAccessed(): int {
return $this->upload_accessed ?? 0;
}
public function bumpAccess(): void {
if(empty($this->getId()))
return;
$this->upload_accessed = time();
$bumpAccess = DB::prepare('
UPDATE `prm_uploads`
SET `upload_accessed` = NOW()
WHERE `upload_id` = :upload
');
$bumpAccess->bindValue('upload', $this->getId());
$bumpAccess->execute();
}
public function getExpires(): int {
return $this->upload_expires ?? 0;
}
public function hasExpired(): bool {
return $this->getExpires() > 1 && $this->getExpires() <= time();
}
public function getDeleted(): int {
return $this->upload_deleted ?? 0;
}
public function isDeleted(): bool {
return $this->getDeleted() > 0;
}
public function getDMCA(): int {
return $this->upload_dmca ?? 0;
}
public function isDMCA(): bool {
return $this->getDMCA() > 0;
}
public function getExpiryBump(): int {
return $this->upload_bump ?? 0;
}
public function bumpExpiry(): void {
if(empty($this->getId()) || $this->getExpires() < 1)
return;
$bumpSeconds = $this->getExpiryBump();
if($bumpSeconds < 1)
return;
$this->upload_expires = time() + $bumpSeconds;
$bumpExpiry = DB::prepare('
UPDATE `prm_uploads`
SET `upload_expires` = NOW() + INTERVAL :seconds SECOND
WHERE `upload_id` = :upload
');
$bumpExpiry->bindValue('seconds', $bumpSeconds);
$bumpExpiry->bindValue('upload', $this->getId());
$bumpExpiry->execute();
}
public function restore(): void {
$this->upload_deleted = null;
$restore = DB::prepare('
UPDATE `prm_uploads`
SET `upload_deleted` = NULL
WHERE `upload_id` = :id
');
$restore->bindValue('id', $this->getId());
$restore->execute();
}
public function delete(bool $hard): void {
$this->upload_deleted = time();
if($hard) {
if(is_file($this->getPath()))
unlink($this->getPath());
if(is_file($this->getThumbPath()))
unlink($this->getThumbPath());
if($this->getDMCA() < 1) {
$delete = DB::prepare('
DELETE FROM `prm_uploads`
WHERE `upload_id` = :id
');
$delete->bindValue('id', $this->getId());
$delete->execute();
}
} else {
$delete = DB::prepare('
UPDATE `prm_uploads`
SET `upload_deleted` = NOW()
WHERE `upload_id` = :id
');
$delete->bindValue('id', $this->getId());
$delete->execute();
}
}
public function jsonSerialize() {
return [
'id' => $this->getId(),
'url' => $this->getPublicUrl(),
'urlf' => $this->getPublicUrl(true),
'thumb' => $this->getPublicThumbUrl(),
'name' => $this->getName(),
'type' => $this->getType(),
'size' => $this->getSize(),
'user' => $this->getUserId(),
'appl' => $this->getApplicationId(),
'hash' => $this->getHash(),
'created' => date('c', $this->getCreated()),
'accessed' => $this->getLastAccessed() < 1 ? null : date('c', $this->getLastAccessed()),
'expires' => $this->getExpires() < 1 ? null : date('c', $this->getExpires()),
'deleted' => $this->getDeleted() < 1 ? null : date('c', $this->getDeleted()),
'dmca' => $this->getDMCA() < 1 ? null : date('c', $this->getDMCA()),
];
}
public static function generateId(int $length = 32): string {
$token = random_bytes($length);
$chars = strlen(self::ID_CHARS);
for($i = 0; $i < $length; $i++)
$token[$i] = self::ID_CHARS[ord($token[$i]) % $chars];
return $token;
}
public static function create(
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 || $fileSize < 1 || strlen($fileHash) !== 64 || $fileExpiry < 0)
throw new UploadCreationFailedException('Bad args.');
$id = self::generateId();
$create = DB::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 (
:id, :app, :user, :name, :type, :size,
UNHEX(:hash), INET6_ATON(:ip),
FROM_UNIXTIME(:expire), :bump
)
');
$create->bindValue('id', $id);
$create->bindValue('app', $appId < 1 ? null : $appId);
$create->bindValue('user', $userId < 1 ? null : $userId);
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
$create->bindValue('name', $fileName);
$create->bindValue('type', $fileType);
$create->bindValue('hash', $fileHash);
$create->bindValue('size', $fileSize);
$create->bindValue('expire', $fileExpiry > 0 ? (time() + $fileExpiry) : 0);
$create->bindValue('bump', $bumpExpiry ? $fileExpiry : 0);
$create->execute();
try {
return self::byId($id);
} catch(UploadNotFoundException $ex) {
throw new UploadCreationFailedException;
}
}
public static function byId(string $id): self {
$getUpload = DB::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` = :id
AND `upload_deleted` IS NULL
');
$getUpload->bindValue('id', $id);
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
if(!$upload)
throw new UploadNotFoundException;
return $upload;
}
public static function byHash(string $hash): ?self {
$getUpload = DB::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(:hash)
');
$getUpload->bindValue('hash', $hash);
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
return $upload ? $upload : null;
}
public static function deleted(): array {
$getDeleted = DB::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_deleted` IS NOT NULL
OR `upload_dmca` IS NOT NULL
OR `user_id` IS NULL
OR `app_id` IS NULL
');
if(!$getDeleted->execute())
return [];
$deleted = [];
while($upload = $getDeleted->fetchObject(self::class))
$deleted[] = $upload;
return $deleted;
}
public static function expired(): array {
$getExpired = DB::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_expires` IS NOT NULL
AND `upload_expires` <= NOW()
AND `upload_dmca` IS NULL
');
if(!$getExpired->execute())
return [];
$deleted = [];
while($upload = $getExpired->fetchObject(self::class))
$deleted[] = $upload;
return $deleted;
}
public static function all(int $limit = 0, string $after = ''): array {
$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`
';
if(!empty($after))
$query .= ' WHERE `upload_id` > :after';
$query .= ' ORDER BY `upload_id`';
if($limit > 0)
$query .= sprintf(' LIMIT %d', $limit);
$getUploads = DB::prepare($query);
if(!empty($after))
$getUploads->bindValue('after', $after);
$getUploads->execute();
$out = [];
while($upload = $getUploads->fetchObject(self::class))
$out[] = $upload;
return $out;
}
}

109
src/User.php Normal file
View file

@ -0,0 +1,109 @@
<?php
namespace EEPROM;
use Exception;
use JsonSerializable;
class UserNotFoundException extends Exception {}
class User implements JsonSerializable {
private static $active;
public static function hasActive(): bool {
return !empty(self::$active);
}
public static function active(): self {
return self::$active;
}
public function __construct() {
}
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->user_id ?? 0;
}
public function getSizeMultiplier(): int {
return $this->user_size_multiplier ?? 0;
}
public function getCreated(): int {
return $this->user_created ?? 0;
}
public function getRestricted(): int {
return $this->user_restricted ?? 0;
}
public function isRestricted(): bool {
return $this->getRestricted() > 0;
}
public function jsonSerialize() {
return [
'id' => $this->getId(),
'size_multi' => $this->getSizeMultiplier(),
'created' => date('c', $this->getCreated()),
'restricted' => $this->getRestricted() < 1 ? null : date('c', $this->getRestricted()),
];
}
public static function byId(int $userId): self {
if($userId < 1)
throw new UserNotFoundException;
$createUser = DB::prepare('INSERT IGNORE INTO `prm_users` (`user_id`) VALUES (:id)');
$createUser->bindValue('id', $userId);
$createUser->execute();
$getUser = DB::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` = :user
');
$getUser->bindValue('user', $userId);
$getUser->execute();
$user = $getUser->fetchObject(self::class);
if($user === false)
throw new UserNotFoundException;
return $user;
}
public static function all(int $limit = 0, int $after = 0): array {
$query = '
SELECT `user_id`, `user_size_multiplier`,
UNIX_TIMESTAMP(`user_created`) AS `user_created`,
UNIX_TIMESTAMP(`user_restricted`) AS `user_restricted`
FROM `prm_users`
';
if($after > 0)
$query .= sprintf(' WHERE `user_id` > %d', $after);
$query .= ' ORDER BY `user_id`';
if($limit > 0)
$query .= sprintf(' LIMIT %d', $limit);
$getUsers = DB::prepare($query);
$getUsers->execute();
$out = [];
while($user = $getUsers->fetchObject(self::class))
$out[] = $user;
return $out;
}
}

66
startup.php Normal file
View file

@ -0,0 +1,66 @@
<?php
namespace EEPROM;
define('PRM_STARTUP', microtime(true));
define('PRM_ROOT', __DIR__);
define('PRM_CLI', PHP_SAPI === 'cli');
define('PRM_DEBUG', is_file(PRM_ROOT . '/.debug'));
define('PRM_PHP_MIN_VER', '7.4.0');
define('PRM_PUBLIC', PRM_ROOT . '/public');
define('PRM_SOURCE', PRM_ROOT . '/src');
define('PRM_UPLOADS', PRM_PUBLIC . '/data');
define('PRM_THUMBS', PRM_PUBLIC . '/thumb');
if(version_compare(PHP_VERSION, PRM_PHP_MIN_VER, '<'))
die('EEPROM required at least PHP ' . PRM_PHP_MIN_VER . '.');
error_reporting(PRM_DEBUG ? -1 : 0);
ini_set('display_errors', PRM_DEBUG ? 'On' : 'Off');
mb_internal_encoding('utf-8');
date_default_timezone_set('utc');
set_exception_handler(function(\Throwable $ex) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
if(PRM_DEBUG)
echo (string)$ex;
exit;
});
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline) {
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
return true;
}, -1);
spl_autoload_register(function(string $className) {
if(substr($className, 0, 7) !== 'EEPROM\\')
return;
$classPath = PRM_SOURCE . str_replace('\\', '/', substr($className, 6)) . '.php';
if(is_file($classPath))
require_once $classPath;
});
if(!is_dir(PRM_UPLOADS))
mkdir(PRM_UPLOADS, 0775, true);
if(!is_dir(PRM_THUMBS))
mkdir(PRM_THUMBS, 0775, true);
$configPath = PRM_ROOT . '/config.ini';
if(!is_file($configPath))
die('EEPROM configuration is missing.');
Config::load($configPath);
if(!Config::has('PDO', 'dsn') || !Config::has('PDO', 'username'))
die('EEPROM database is not configured.');
DB::init(
Config::get('PDO', 'dsn'),
Config::get('PDO', 'username'),
Config::get('PDO', 'password', ''),
DB::FLAGS
);