Import EEPROM.
This commit is contained in:
commit
0f23bb2106
13 changed files with 1691 additions and 0 deletions
0
.debug
Normal file
0
.debug
Normal file
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
public/data/*
|
||||
public/thumb/*
|
||||
config.ini
|
456
_manage.php
Normal file
456
_manage.php
Normal 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
29
cron.php
Normal 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
465
public/index.php
Normal 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
89
src/Application.php
Normal 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
28
src/Base64.php
Normal 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
23
src/Config.php
Normal 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
32
src/DB.php
Normal 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
390
src/Upload.php
Normal 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
109
src/User.php
Normal 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
66
startup.php
Normal 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
|
||||
);
|
Loading…
Reference in a new issue