Compare commits

..

2 commits

Author SHA1 Message Date
flash 86e455bd5b What if I knew how to markdown? 2023-11-09 21:36:01 +00:00
flash 81ae1976e5 we splitting development 2023-11-09 21:35:32 +00:00
35 changed files with 1553 additions and 1407 deletions

2
.gitignore vendored
View file

@ -3,11 +3,9 @@
.DS_Store
/public/data/*
/public/thumb/*
/public/scripts
/public/robots.txt
/config.cfg
/config.ini
/.debug
/vendor
/.migrating
/node_modules

View file

@ -1,4 +1,4 @@
Copyright (c) 2020-2024, flashwave <me@flash.moe>
Copyright (c) 2020-2023, flashwave <me@flash.moe>
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -1,4 +1,4 @@
# Flashii EEPROM
# ~~Flashii~~ Nabucco EEPROM
> File not found.
EEPROM is the file uploading service for Flashii.
EEPROM is the file uploading service for Flashii but also for Nabucco.

102
build.js
View file

@ -1,102 +0,0 @@
// IMPORTS
const fs = require('fs');
const swc = require('@swc/core');
const path = require('path');
const utils = require('./scripts/utils.js');
const assproc = require('./scripts/assproc.js');
// CONFIG
const rootDir = __dirname;
const srcDir = path.join(rootDir, 'scripts');
const pubDir = path.join(rootDir, 'public');
const pubAssetsDir = path.join(pubDir, 'scripts');
const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
const buildTasks = {
js: [
{ source: 'eepromv1.js', target: '/scripts', name: 'eepromv1.js', es: 'es5' },
{ source: 'eepromv1a.js', target: '/scripts', name: 'eepromv1a.js' },
],
};
// PREP
const swcJscOptions = {
target: 'es2021',
loose: false,
externalHelpers: false,
keepClassNames: true,
preserveAllComments: false,
transform: {},
parser: {
syntax: 'ecmascript',
jsx: false,
dynamicImport: false,
privateMethod: false,
functionBind: false,
exportDefaultFrom: false,
exportNamespaceFrom: false,
decorators: false,
decoratorsBeforeExport: false,
topLevelAwait: true,
importMeta: false,
},
};
// BUILD
(async () => {
const files = {};
console.log('Ensuring assets directory exists...');
fs.mkdirSync(pubAssetsDir, { recursive: true });
console.log();
console.log('JS assets');
for(const info of buildTasks.js) {
console.log(`=> Building ${info.source}...`);
let origTarget = undefined;
if('es' in info) {
origTarget = swcJscOptions.target;
swcJscOptions.target = info.es;
}
const assprocOpts = {
prefix: '#',
entry: info.entry || 'main.js',
};
const swcOpts = {
filename: info.source,
sourceMaps: false,
isModule: false,
minify: !isDebugBuild,
jsc: swcJscOptions,
};
const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts)
.then(output => swc.transform(output, swcOpts))
.then(output => {
const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.code);
return pubName;
});
if(origTarget !== undefined)
swcJscOptions.target = origTarget;
files[info.source] = pubName;
}
console.log();
console.log('Cleaning up old builds...');
assproc.housekeep(pubAssetsDir);
})();

View file

@ -4,7 +4,7 @@
"require": {
"flashwave/index": "dev-master",
"flashwave/syokuhou": "dev-master",
"sentry/sdk": "^4.0"
"sentry/sdk": "^3.5"
},
"require-dev": {
"phpstan/phpstan": "^1.10"

1172
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,13 @@
database:dsn mariadb://user:password@:unix:/eeprom?socket=/var/run/mysqld/mysqld.sock&charset=utf8mb4
; Must be implementations of \EEPROM\Auth\IAuth
auth:clients \EEPROM\Auth\MisuzuAuth \EEPROM\Auth\NabuccoAuth
misuzu:secret woomy
misuzu:endpoint https://flashii.net/_sockchat/verify
nabucco:secret secret key
domain:short i.flashii.net
domain:api eeprom.flashii.net

View file

@ -21,18 +21,21 @@ try {
$uploadsData = $uploadsCtx->getUploadsData();
// Mark expired as deleted
$expired = $uploadsData->getUploads(expired: true, deleted: false);
$expired = $uploadsData->getUploads(expired: true, deleted: false, dmca: false);
foreach($expired as $uploadInfo)
$uploadsData->deleteUpload($uploadInfo);
// Hard delete soft deleted files
$deleted = $uploadsData->getUploads(deleted: true);
$deleted = $uploadsData->getUploads(deleted: true, dmca: false);
foreach($deleted as $uploadInfo) {
$uploadsCtx->deleteUploadData($uploadInfo);
$uploadsData->nukeUpload($uploadInfo);
}
// new storage format should store by hashes again, ensure blacklisted data is no longer saved
// Ensure local data of DMCA'd files is gone
$deleted = $uploadsData->getUploads(dmca: true);
foreach($deleted as $uploadInfo)
$uploadsCtx->deleteUploadData($uploadInfo);
} finally {
sem_release($semaphore);
}

View file

@ -1,18 +0,0 @@
<?php
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigration;
final class NewBlacklistSystem_20231111_015548 implements IDbMigration {
public function migrate(IDbConnection $conn): void {
$conn->execute('
CREATE TABLE prm_blacklist (
bl_hash BINARY(32) NOT NULL,
bl_reason ENUM("copyright", "rules", "other") NOT NULL COLLATE "ascii_general_ci",
bl_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (bl_hash)
) ENGINE=InnoDB COLLATE="utf8mb4_bin"
');
$conn->execute('ALTER TABLE prm_uploads DROP COLUMN upload_dmca');
}
}

209
package-lock.json generated
View file

@ -1,209 +0,0 @@
{
"name": "eeprom.edgii.net",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@swc/core": "^1.3.107"
}
},
"node_modules/@swc/core": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
"integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.1",
"@swc/types": "^0.1.5"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.3.107",
"@swc/core-darwin-x64": "1.3.107",
"@swc/core-linux-arm-gnueabihf": "1.3.107",
"@swc/core-linux-arm64-gnu": "1.3.107",
"@swc/core-linux-arm64-musl": "1.3.107",
"@swc/core-linux-x64-gnu": "1.3.107",
"@swc/core-linux-x64-musl": "1.3.107",
"@swc/core-win32-arm64-msvc": "1.3.107",
"@swc/core-win32-ia32-msvc": "1.3.107",
"@swc/core-win32-x64-msvc": "1.3.107"
},
"peerDependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
"integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
"integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
"integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
"integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
"integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
"integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
"integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
"integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
"integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
"integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz",
"integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw=="
},
"node_modules/@swc/types": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw=="
}
}
}

View file

@ -1,5 +0,0 @@
{
"dependencies": {
"@swc/core": "^1.3.107"
}
}

View file

@ -56,7 +56,7 @@
</div>
</div>
<div class="eeprom-footer">
<a href="https://flash.moe" target="_blank" rel="noopener">flashwave</a> 2020-2024
<a href="https://flash.moe" target="_blank" rel="noopener">flashwave</a> 2020-2023
</div>
</div>
</div>

211
public/js/eeprom-v1.0.js Normal file
View file

@ -0,0 +1,211 @@
var EEPROM = function(srcId, endpoint, authorization) {
var obj = {
srcId: parseInt(srcId),
endpoint: endpoint,
authorization: authorization,
};
obj.setEndpoint = function(endpoint) {
obj.endpoint = (endpoint || '').toString();
};
obj.setAuthorization = function(authorization) {
obj.authorization = (authorization || '').toString();
};
obj.deleteUpload = function(fileInfo) {
return new EEPROM.EEPROMDeleteTask(
obj.authorization,
fileInfo
);
};
obj.createUpload = function(file) {
return new EEPROM.EEPROMUploadTask(
obj.srcId,
obj.endpoint,
obj.authorization,
file
);
};
return obj;
};
EEPROM.ERR_GENERIC = 'generic';
EEPROM.ERR_INVALID = 'invalid';
EEPROM.ERR_AUTH = 'auth';
EEPROM.ERR_ACCESS = 'access';
EEPROM.ERR_DMCA = 'dmca';
EEPROM.ERR_GONE = 'gone';
EEPROM.ERR_SERVER = 'server';
EEPROM.ERR_SIZE = 'size';
EEPROM.EEPROMFile = function(fileInfo) {
var obj = {
id: (fileInfo.id || '').toString(),
url: (fileInfo.url || '').toString(),
urlf: (fileInfo.urlf || '').toString(),
thumb: (fileInfo.thumb || '').toString(),
name: (fileInfo.name || '').toString(),
type: (fileInfo.type || '').toString(),
size: parseInt(fileInfo.size || 0),
user: parseInt(fileInfo.user || 0),
hash: (fileInfo.hash || '').toString(),
created: (fileInfo.created || null),
accessed: (fileInfo.accessed || null),
expires: (fileInfo.expires || null),
deleted: (fileInfo.deleted || null),
dmca: (fileInfo.dmca || null),
};
obj.isImage = function() { return obj.type.indexOf('image/') === 0; };
obj.isAudio = function() { return obj.type === 'application/x-font-gdos' || obj.type.indexOf('audio/') === 0; };
obj.isVideo = function() { return obj.type.indexOf('video/') === 0; };
obj.isMedia = function() { return obj.isImage() || obj.isAudio() || obj.isVideo(); };
return obj;
};
EEPROM.EEPROMDeleteTask = function(authorization, fileInfo) {
var obj = {
authorization: authorization,
fileInfo: fileInfo,
onSuccess: undefined,
onFailure: undefined,
};
var xhr = obj.xhr = new XMLHttpRequest;
obj.xhr.addEventListener('readystatechange', function() {
if(xhr.readyState !== 4)
return;
if(xhr.status !== 204) {
obj.errorCode = EEPROM.ERR_GENERIC;
switch(xhr.status) {
case 401:
obj.errorCode = EEPROM.ERR_AUTH;
break;
case 403:
obj.errorCode = EEPROM.ERR_ACCESS;
break;
case 404:
case 410:
obj.errorCode = EEPROM.ERR_GONE;
break;
case 500:
case 503:
obj.errorCode = EEPROM.ERR_SERVER;
break;
}
if(obj.onFailure)
obj.onFailure(obj.errorCode);
return;
}
if(obj.onSuccess)
obj.onSuccess();
});
obj.start = function() {
xhr.open('DELETE', obj.fileInfo.urlf);
xhr.setRequestHeader('Authorization', obj.authorization);
xhr.send();
};
return obj;
};
EEPROM.EEPROMUploadTask = function(srcId, endpoint, authorization, file) {
var obj = {
aborted: false,
endpoint: endpoint,
authorization: authorization,
onComplete: undefined,
onFailure: undefined,
onProgress: undefined,
failureResponse: undefined,
};
var xhr = obj.xhr = new XMLHttpRequest,
fd = obj.formData = new FormData;
fd.append('src', srcId);
fd.append('file', file);
var reportUploadProgress = function(ev) {
if(obj.onProgress)
obj.onProgress({
loaded: ev.loaded,
total: ev.total,
progress: Math.ceil((ev.loaded / ev.total) * 100),
});
};
xhr.upload.addEventListener('loadstart', reportUploadProgress);
xhr.upload.addEventListener('progress', reportUploadProgress);
xhr.upload.addEventListener('load', reportUploadProgress);
xhr.addEventListener('readystatechange', function() {
if(this.readyState !== 4)
return;
if(this.status !== 201) {
obj.failureResponse = {
userAborted: obj.aborted,
error: EEPROM.ERR_GENERIC,
};
switch(this.status) {
case 400:
case 405:
obj.failureResponse.error = EEPROM.ERR_INVALID;
break;
case 401:
obj.failureResponse.error = EEPROM.ERR_AUTH;
break;
case 403:
obj.failureResponse.error = EEPROM.ERR_ACCESS;
break;
case 404:
case 410:
obj.failureResponse.error = EEPROM.ERR_GONE;
break;
case 451:
obj.failureResponse.error = EEPROM.ERR_DMCA;
break;
case 413:
obj.failureResponse.error = EEPROM.ERR_SIZE;
obj.failureResponse.maxSize = parseInt(this.getResponseHeader('X-EEPROM-Max-Size'));
break;
case 500:
case 503:
obj.failureResponse.error = EEPROM.ERR_SERVER;
break;
}
if(obj.onFailure)
obj.onFailure(obj.failureResponse);
return;
}
obj.fileInfo = new EEPROM.EEPROMFile(JSON.parse(this.responseText));
if(obj.onComplete)
obj.onComplete(obj.fileInfo);
});
obj.abort = function() {
obj.aborted = true;
xhr.abort();
};
obj.start = function() {
xhr.open('POST', obj.endpoint);
xhr.setRequestHeader('Authorization', obj.authorization);
xhr.send(fd);
};
return obj;
};

View file

@ -1,108 +0,0 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const utils = require('./utils.js');
exports.process = async function(root, options) {
const macroPrefix = options.prefix || '#';
const entryPoint = options.entry || '';
root = fs.realpathSync(root);
const included = [];
const processFile = async function(fileName) {
const fullPath = path.join(root, fileName);
if(included.includes(fullPath))
return '';
included.push(fullPath);
if(!fullPath.startsWith(root))
return '/* *** INVALID PATH: ' + fullPath + ' */';
if(!fs.existsSync(fullPath))
return '/* *** FILE NOT FOUND: ' + fullPath + ' */';
const lines = readline.createInterface({
input: fs.createReadStream(fullPath),
crlfDelay: Infinity,
});
let output = '';
let lastWasEmpty = false;
if(options.showPath)
output += "/* *** PATH: " + fullPath + " */\n";
for await(const line of lines) {
const lineTrimmed = utils.trim(line);
if(lineTrimmed === '')
continue;
if(line.startsWith(macroPrefix)) {
const args = lineTrimmed.split(' ');
const macro = utils.trim(utils.trimStart(args.shift(), macroPrefix));
switch(macro) {
case 'comment':
break;
case 'include': {
const includePath = utils.trimEnd(args.join(' '), ';');
output += utils.trim(await processFile(includePath));
output += "\n";
break;
}
case 'buildvars':
if(typeof options.buildVars === 'object') {
const bvTarget = options.buildVarsTarget || 'window';
const bvProps = [];
for(const bvName in options.buildVars)
bvProps.push(`${bvName}: { value: ${JSON.stringify(options.buildVars[bvName])}, writable: false }`);
if(Object.keys(bvProps).length > 0)
output += `Object.defineProperties(${bvTarget}, { ${bvProps.join(', ')} });\n`;
}
break;
default:
output += line;
output += "\n";
break;
}
} else {
output += line;
output += "\n";
}
}
return output;
};
return await processFile(entryPoint);
};
exports.housekeep = function(assetsPath) {
const files = fs.readdirSync(assetsPath).map(fileName => {
const stats = fs.statSync(path.join(assetsPath, fileName));
return {
name: fileName,
lastMod: stats.mtimeMs,
};
}).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name);
const regex = /^(.+)[\-\.]([a-f0-9]+)\.(.+)$/i;
const counts = {};
for(const fileName of files) {
const match = fileName.match(regex);
if(match) {
const name = match[1] + '-' + match[3];
counts[name] = (counts[name] || 0) + 1;
if(counts[name] > 5)
fs.unlinkSync(path.join(assetsPath, fileName));
}
}
};

View file

@ -1,195 +0,0 @@
window.EEPROM = (() => {
const errGeneric = 'generic';
const errInvalid = 'invalid';
const errAuth = 'auth';
const errAccess = 'access';
const errDMCA = 'dmca';
const errGone = 'gone';
const errSize = 'size';
const errServer = 'server';
const createClient = function(srcId, endPoint, authorization) {
if(typeof srcId !== 'number')
srcId = parseInt(srcId);
if(typeof endPoint !== 'string')
throw 'endPoint must be a string';
if(typeof authorization !== 'string' && typeof authorization !== 'function')
throw 'authorization must be a string or a function returning a string';
const createUpload = file => {
const uploadTask = { onComplete: undefined, onFailure: undefined, onProgress: undefined };
let userAborted = false;
const xhr = new XMLHttpRequest;
const fd = new FormData;
fd.append('src', srcId);
fd.append('file', file);
const reportUploadProgress = ev => {
if(typeof uploadTask.onProgress === 'function')
uploadTask.onProgress({
loaded: ev.loaded,
total: ev.total,
progress: Math.ceil((ev.loaded / ev.total) * 100),
});
};
xhr.upload.addEventListener('loadstart', reportUploadProgress);
xhr.upload.addEventListener('progress', reportUploadProgress);
xhr.upload.addEventListener('load', reportUploadProgress);
xhr.addEventListener('readystatechange', () => {
if(xhr.readyState !== 4)
return;
if(xhr.status !== 201) {
const failureResponse = {
userAborted: userAborted,
error: errGeneric,
};
switch(xhr.status) {
case 400:
case 405:
failureResponse.error = errInvalid;
break;
case 401:
failureResponse.error = errAuth;
break;
case 403:
failureResponse.error = errAccess;
break;
case 404:
case 410:
failureResponse.error = errGone;
break;
case 451:
failureResponse.error = errDMCA;
break;
case 413:
failureResponse.error = errSize;
failureResponse.maxSize = parseInt(xhr.getResponseHeader('X-EEPROM-Max-Size'));
break;
case 500:
case 503:
failureResponse.error = errServer;
break;
}
if(typeof uploadTask.onFailure === 'function')
uploadTask.onFailure(failureResponse);
return;
}
if(typeof uploadTask.onComplete === 'function') {
const fileInfo = JSON.parse(xhr.responseText);
if(typeof fileInfo !== 'object') {
if(typeof uploadTask.onFailure === 'function')
uploadTask.onFailure({
userAborted: userAborted,
error: errServer,
});
return;
}
fileInfo.isImage = () => typeof fileInfo.type === 'string' && fileInfo.type.indexOf('image/') === 0;
fileInfo.isVideo = () => typeof fileInfo.type === 'string' && fileInfo.type.indexOf('video/') === 0;
fileInfo.isAudio = () => typeof fileInfo.type === 'string' && (fileInfo.type === 'application/x-font-gdos' || fileInfo.type.indexOf('audio/') === 0);
fileInfo.isMedia = () => typeof fileInfo.type === 'string' && (fileInfo.isImage() || fileInfo.isAudio() || fileInfo.isVideo());
uploadTask.onComplete(fileInfo);
}
});
uploadTask.abort = () => {
userAborted = true;
xhr.abort();
};
uploadTask.start = () => {
xhr.open('POST', endPoint);
const authIsFunc = typeof authorization === 'function';
if(authIsFunc || typeof authorization === 'string')
xhr.setRequestHeader('Authorization', authIsFunc ? authorization() : authorization);
else
xhr.withCredentials = true;
xhr.send(fd);
};
return uploadTask;
};
const deleteUpload = fileInfo => {
const deleteTask = { onSuccess: undefined, onFailure: undefined };
const xhr = new XMLHttpRequest;
xhr.addEventListener('readystatechange', () => {
if(xhr.readyState !== 4)
return;
if(xhr.status !== 204) {
let errorCode = errGeneric;
switch(xhr.status) {
case 401:
errorCode = errAuth;
break;
case 403:
errorCode = errAccess;
break;
case 404:
case 410:
errorCode = errGone;
break;
case 500:
case 503:
errorCode = errServer;
break;
}
if(typeof deleteTask.onFailure === 'function')
deleteTask.onFailure(errorCode);
return;
}
if(typeof deleteTask.onSuccess === 'function')
deleteTask.onSuccess();
});
deleteTask.start = () => {
xhr.open('DELETE', fileInfo.urlf);
const authIsFunc = typeof authorization === 'function';
if(authIsFunc || typeof authorization === 'string')
xhr.setRequestHeader('Authorization', authIsFunc ? authorization() : authorization);
else
xhr.withCredentials = true;
xhr.send();
};
return deleteTask;
};
return {
createUpload: createUpload,
deleteUpload: deleteUpload,
};
};
Object.defineProperties(createClient, {
ERR_GENERIC: { value: errGeneric },
ERR_INVALID: { value: errInvalid },
ERR_AUTH: { value: errAuth },
ERR_ACCESS: { value: errAccess },
ERR_DMCA: { value: errDMCA },
ERR_GONE: { value: errGone },
ERR_SERVER: { value: errServer },
ERR_SIZE: { value: errSize },
});
return createClient;
})();

View file

@ -1,45 +0,0 @@
const EEPFMT = (() => {
const symbols = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'];
const format = (bytes, decimal) => {
if(bytes === 0)
return 'Zero Bytes';
const negative = bytes < 0;
const power = decimal ? 1000 : 1024;
const exp = Math.floor(Math.log(bytes) / Math.log(power));
bytes = Math.abs(bytes);
const number = bytes / Math.pow(power, exp);
const symbol = symbols[exp];
let string = '';
if(negative)
string += '-';
const fractionDigits = bytes < power ? 0 : (number < 10 ? 2 : 1);
string += number.toLocaleString(undefined, {
maximumFractionDigits: fractionDigits,
minimumFractionDigits: fractionDigits,
});
string += ` ${symbol}`;
if(symbol === '') {
string += 'Byte';
if(number > 1)
string += 's';
} else {
if(!decimal)
string += 'i';
string += 'B';
}
return string;
};
return {
format: format,
};
})();

View file

@ -1,213 +0,0 @@
#include bytefmt.js
#include xhr.js
const EEPROM = function(appId, endPoint, auth) {
if(typeof appId !== 'string')
appId = (appId || '').toString();
if(typeof endPoint !== 'string')
throw 'endPoint must be a string';
const applyAuth = options => {
if(typeof auth === 'function')
options.headers.Authorization = auth();
else if(typeof auth === 'string')
options.headers.Authorization = auth;
else
options.authed = true;
};
const createUpload = fileInput => {
if(!(fileInput instanceof File))
throw 'fileInput must be an instance of window.File';
let userAborted = false;
let abortHandler, progressHandler;
const reportProgress = ev => {
if(progressHandler !== undefined)
progressHandler({
loaded: ev.loaded,
total: ev.total,
progress: ev.total <= 0 ? 0 : ev.loaded / ev.total,
});
};
const uploadAbortedError = () => {
return {
error: 'eeprom:aborted_error',
aborted: userAborted,
toString: () => 'File upload was aborted manually.',
};
};
const uploadGenericError = () => {
return {
error: 'eeprom:generic_error',
aborted: userAborted,
toString: () => 'File upload failed for unknown reasons.',
};
};
return {
abort: () => {
userAborted = true;
if(abortHandler !== undefined)
abortHandler();
},
onProgress: handler => {
if(typeof handler !== 'function')
throw 'handler must be a function';
progressHandler = handler;
},
start: async () => {
if(userAborted)
throw uploadAbortedError();
const options = {
type: 'json',
headers: {},
upload: reportProgress,
abort: handler => abortHandler = handler,
};
applyAuth(options);
const formData = new FormData;
formData.append('src', appId);
formData.append('file', fileInput);
try {
const result = await EEPXHR.post(`${endPoint}/uploads`, options, formData);
if(result.status !== 201) {
if(result.status === 400)
throw {
error: 'eeprom:request_error',
aborted: userAborted,
toString: () => 'There was an error with the upload request.',
};
if(result.status === 401)
throw {
error: 'eeprom:auth_error',
aborted: userAborted,
toString: () => 'Could not authenticate upload request. If this persists, try refreshing the page.',
};
if(result.status === 403)
throw {
error: 'eeprom:access_error',
aborted: userAborted,
toString: () => 'You are not allowed to upload files.',
};
if(result.status === 404)
throw {
error: 'eeprom:app_error',
aborted: userAborted,
toString: () => 'EEPROM app is not configured properly.',
};
if(result.status === 413) {
const maxSize = parseInt(result.headers().get('x-eeprom-max-size'));
const maxSizeFormatted = EEPFMT.format(maxSize);
throw {
error: 'eeprom:size_error',
maxSize: maxSize,
maxSizeFormatted: maxSizeFormatted,
aborted: userAborted,
toString: () => maxSize < 1 ? 'Uploaded file was too large.' : `Uploads may not be larger than ${maxSizeFormatted}.`,
};
}
if(result.status === 451)
throw {
error: 'eeprom:dmca_error',
aborted: userAborted,
toString: () => 'This file is blocked from being uploaded, possibly for copyright reasons.',
};
if(result.status === 500)
throw {
error: 'eeprom:server_error',
aborted: userAborted,
toString: () => 'An error occurred within the EEPROM server. If this persists, report it to a developer.',
};
if(result.status === 503)
throw {
error: 'eeprom:maintenance_error',
aborted: userAborted,
toString: () => 'EEPROM server is temporarily unavailable for maintenance.',
};
throw uploadGenericError();
}
const fileInfo = result.body();
if(typeof fileInfo.type === 'string') {
fileInfo.isImage = () => fileInfo.type.indexOf('image/') === 0;
fileInfo.isVideo = () => fileInfo.type.indexOf('video/') === 0;
fileInfo.isAudio = () => fileInfo.type === 'application/x-font-gdos' || fileInfo.type.indexOf('audio/') === 0;
} else
fileInfo.isImage = fileInfo.isVideo = fileInfo.isAudio = () => false;
fileInfo.isMedia = () => fileInfo.isImage() || fileInfo.isAudio() || fileInfo.isVideo();
return Object.freeze(fileInfo);
} catch(ex) {
if(!ex.abort) {
console.error(ex);
throw ex;
}
throw uploadAbortedError();
}
},
};
};
const deleteUpload = async fileInfo => {
if(typeof fileInfo !== 'object')
throw 'fileInfo must be an object';
if(typeof fileInfo.urlf !== 'string')
throw 'fileInfo.urlf must be a string';
const options = { headers: {} };
applyAuth(options);
const result = await EEPXHR.delete(fileInfo.urlf, options);
if(result.status !== 204) {
if(result.status === 401)
throw {
error: 'eeprom:auth_error',
toString: () => 'Could not authenticate delete request. If this persists, try refreshing the page.',
};
if(result.status === 403)
throw {
error: 'eeprom:access_error',
toString: () => 'You are not allowed to delete this file.',
};
if(result.status === 404)
throw {
error: 'eeprom:file_error',
toString: () => 'File not found.',
};
if(result.status === 500)
throw {
error: 'eeprom:server_error',
toString: () => 'An error occurred within the EEPROM server. If this persists, report it to a developer.',
};
if(result.status === 503)
throw {
error: 'eeprom:maintenance_error',
toString: () => 'EEPROM server is temporarily unavailable for maintenance.',
};
throw {
error: 'eeprom:generic_error',
toString: () => 'File delete failed for unknown reasons.',
};
}
};
return {
create: createUpload,
delete: deleteUpload,
};
};

View file

@ -1,75 +0,0 @@
const EEPXHR = (() => {
const send = function(method, url, options, body) {
const xhr = new XMLHttpRequest;
const requestHeaders = new Map;
if('headers' in options && typeof options.headers === 'object')
for(const name in options.headers)
if(options.headers.hasOwnProperty(name))
requestHeaders.set(name.toLowerCase(), options.headers[name]);
if(typeof options.upload === 'function') {
xhr.upload.onloadstart = ev => options.upload(ev);
xhr.upload.onprogress = ev => options.upload(ev);
xhr.upload.onloadend = ev => options.upload(ev);
}
if(options.authed)
xhr.withCredentials = true;
if(typeof options.timeout === 'number')
xhr.timeout = options.timeout;
if(typeof options.type === 'string')
xhr.responseType = options.type;
if(typeof options.abort === 'function')
options.abort(() => xhr.abort());
return new Promise((resolve, reject) => {
let responseHeaders = undefined;
xhr.onload = ev => resolve({
status: xhr.status,
body: () => xhr.response,
headers: () => {
if(responseHeaders !== undefined)
return responseHeaders;
responseHeaders = new Map;
const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
for(const name in raw)
if(raw.hasOwnProperty(name)) {
const parts = raw[name].split(': ');
responseHeaders.set(parts.shift(), parts.join(': '));
}
return responseHeaders;
},
});
xhr.onabort = ev => reject({
abort: true,
xhr: xhr,
ev: ev,
});
xhr.onerror = ev => reject({
abort: false,
xhr: xhr,
ev: ev,
});
xhr.open(method, url);
for(const [name, value] of requestHeaders)
xhr.setRequestHeader(name, value);
xhr.send(body);
});
};
return {
post: (url, options, body) => send('POST', url, options, body),
delete: (url, options, body) => send('DELETE', url, options, body),
};
})();

View file

@ -1,35 +0,0 @@
const crypto = require('crypto');
exports.strtr = (str, replacements) => str.toString().replace(
/{([^}]+)}/g, (match, key) => replacements[key] || match
);
const trim = function(str, chars, flags) {
if(chars === undefined)
chars = " \n\r\t\v\0";
let start = 0,
end = str.length;
if(flags & 0x01)
while(start < end && chars.indexOf(str[start]) >= 0)
++start;
if(flags & 0x02)
while(end > start && chars.indexOf(str[end - 1]) >= 0)
--end;
return (start > 0 || end < str.length)
? str.substring(start, end)
: str;
};
exports.trimStart = (str, chars) => trim(str, chars, 0x01);
exports.trimEnd = (str, chars) => trim(str, chars, 0x02);
exports.trim = (str, chars) => trim(str, chars, 0x03);
exports.shortHash = function(text) {
const hash = crypto.createHash('sha256');
hash.update(text);
return hash.digest('hex').substring(0, 8);
};

View file

@ -1,7 +1,9 @@
<?php
namespace EEPROM\Auth;
use Index\Http\Routing\{HttpMiddleware,RouteHandler};
use stdClass;
use Index\Routing\Route;
use Index\Routing\RouteHandler;
use Syokuhou\IConfig;
use EEPROM\Users\UsersContext;
@ -12,30 +14,27 @@ class AuthRoutes extends RouteHandler {
private UsersContext $usersCtx
) {}
#[HttpMiddleware('/')]
#[Route('/')]
public function getIndex($response, $request) {
$auth = $request->getHeaderLine('Authorization');
if(empty($auth)) {
$cookie = (string)$request->getCookie('msz_auth');
if(!empty($cookie))
$auth = sprintf('Misuzu %s', $cookie);
}
if(!empty($auth)) {
$authParts = explode(' ', $auth, 2);
$authMethod = strval($authParts[0] ?? '');
$authToken = strval($authParts[1] ?? '');
if($authMethod === 'Misuzu') {
$authResult = ChatAuth::attempt(
$this->config->getString('endpoint'),
$this->config->getString('secret'),
$authToken
);
$authClients = $this->config->getArray('clients');
if(!empty($authResult->success))
$this->authInfo->setInfo($this->usersCtx->getUser($authResult->user_id));
foreach($authClients as $client) {
$client = new $client;
if($client->getName() !== $authMethod)
continue;
$authUserId = $client->verifyToken($authToken);
break;
}
if(isset($authUserId) && $authUserId > 0)
$this->authInfo->setInfo($this->usersCtx->getUser($authUserId));
}
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace EEPROM\Auth;
use stdClass;
final class ChatAuth {
public static function attempt(string $endPoint, string $secret, string $cookie): stdClass {
if(!empty($cookie)) {
$method = 'Misuzu';
$signature = sprintf('verify#%s#%s#%s', $method, $cookie, $_SERVER['REMOTE_ADDR']);
$signature = hash_hmac('sha256', $signature, $secret);
$login = curl_init($endPoint);
curl_setopt_array($login, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'method' => $method,
'token' => $cookie,
'ipaddr' => $_SERVER['REMOTE_ADDR'],
], '', '&', PHP_QUERY_RFC3986),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'EEPROM',
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-SharpChat-Signature: ' . $signature,
],
]);
$userInfo = json_decode(curl_exec($login));
curl_close($login);
}
if(empty($userInfo->success)) {
$userInfo = new stdClass;
$userInfo->success = false;
$userInfo->user_id = 0;
$userInfo->username = 'Anonymous';
$userInfo->colour_raw = 0x40000000;
$userInfo->rank = 0;
$userInfo->hierarchy = 0;
$userInfo->perms = 0;
}
return $userInfo;
}
}

7
src/Auth/IAuth.php Normal file
View file

@ -0,0 +1,7 @@
<?php
namespace EEPROM\Auth;
interface IAuth {
public function getName(): string;
public function verifyToken(string $token): int;
}

58
src/Auth/MisuzuAuth.php Normal file
View file

@ -0,0 +1,58 @@
<?php
namespace EEPROM\Auth;
use RuntimeException;
use Index\Serialisation\Serialiser;
class MisuzuAuth implements IAuth {
private $endPoint = '';
private $secretKey = '';
public function __construct() {
global $cfg;
$this->endPoint = $cfg->getString('misuzu:endpoint');
$this->secretKey = $cfg->getString('misuzu:secret');
}
public function getName(): string { return 'Misuzu'; }
public function verifyToken(string $token): int {
if(empty($token))
return 0;
$method = 'Misuzu';
$signature = sprintf('verify#%s#%s#%s', $method, $token, $_SERVER['REMOTE_ADDR']);
$signature = hash_hmac('sha256', $signature, $this->secretKey);
$login = curl_init($this->endPoint);
curl_setopt_array($login, [
CURLOPT_AUTOREFERER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'method' => $method,
'token' => $token,
'ipaddr' => $_SERVER['REMOTE_ADDR'],
], '', '&', PHP_QUERY_RFC3986),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_MAXREDIRS => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Flashii EEPROM',
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-SharpChat-Signature: ' . $signature,
],
]);
$rawUserInfo = curl_exec($login);
$userInfo = json_decode($rawUserInfo);
curl_close($login);
return empty($userInfo->success) || empty($userInfo->user_id) ? 0 : $userInfo->user_id;
}
}

36
src/Auth/NabuccoAuth.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace EEPROM\Auth;
use Index\Serialisation\UriBase64;
class NabuccoAuth implements IAuth {
private $secretKey = '';
public function __construct() {
global $cfg;
$this->secretKey = $cfg->getString('nabucco:secret');
}
public function getName(): string { return 'Nabucco'; }
public function hashToken(string $token): string {
return hash_hmac('md5', $token, $this->secretKey);
}
public function verifyToken(string $token): int {
$length = strlen($token);
if($length < 32 || $length > 100)
return -1;
$userHash = substr($token, 0, 32);
$packed = UriBase64::decode(substr($token, 32));
$realHash = $this->hashToken($packed);
if(!hash_equals($realHash, $userHash))
return -1;
$unpacked = unpack('NuserId/Ntime/CipWidth/a16ipAddr', $packed);
if(empty($unpacked['userId']) || empty($unpacked['time'])
|| $unpacked['time'] < strtotime('-1 month'))
return -1;
return intval($unpacked['userId']);
}
}

View file

@ -1,32 +0,0 @@
<?php
namespace EEPROM\Blacklist;
use Index\Data\IDbConnection;
use EEPROM\Uploads\UploadInfo;
class BlacklistContext {
private BlacklistData $blacklistData;
public function __construct(IDbConnection $dbConn) {
$this->blacklistData = new BlacklistData($dbConn);
}
public function getBlacklistData(): BlacklistData {
return $this->blacklistData;
}
public function createBlacklistEntry(UploadInfo|string $item, string $reason): void {
if($item instanceof UploadInfo)
$item = hex2bin($item->getHashString());
$this->blacklistData->createBlacklistEntry($item, $reason);
}
public function getBlacklistEntry(UploadInfo|string $item): ?BlacklistInfo {
// will this ever be useful? who knows!
if($item instanceof UploadInfo)
$item = hex2bin($item->getHashString());
return $this->blacklistData->getBlacklistEntry($item);
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace EEPROM\Blacklist;
use InvalidArgumentException;
use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
class BlacklistData {
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function getBlacklistEntry(string $hash): ?BlacklistInfo {
$stmt = $this->cache->get('SELECT bl_hash, bl_reason, UNIX_TIMESTAMP(bl_created) FROM prm_blacklist WHERE bl_hash = ?');
$stmt->addParameter(1, $hash);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? new BlacklistInfo($result) : null;
}
public function createBlacklistEntry(string $hash, string $reason): void {
if(strlen($hash) !== 32)
throw new InvalidArgumentException('$hash must be 32 bytes.');
if(!in_array($reason, BlacklistInfo::REASONS))
throw new InvalidArgumentException('$reason is not a valid reason.');
$stmt = $this->cache->get('INSERT INTO prm_blacklist (bl_hash, bl_reason) VALUES (?, ?)');
$stmt->addParameter(1, $hash);
$stmt->addParameter(2, $reason);
$stmt->execute();
}
}

View file

@ -1,47 +0,0 @@
<?php
namespace EEPROM\Blacklist;
use Index\DateTime;
use Index\Data\IDbResult;
class BlacklistInfo {
private string $hash;
private string $reason;
private int $created;
public const REASONS = [
'copyright',
'rules',
'other',
];
public function __construct(IDbResult $result) {
$this->hash = $result->getString(0);
$this->reason = $result->getString(1);
$this->created = $result->getInteger(2);
}
public function getHash(): string {
return $this->hash;
}
public function getReason(): string {
return $this->reason;
}
public function isCopyrightTakedown(): bool {
return $this->reason === 'copyright';
}
public function isRulesViolation(): bool {
return $this->reason === 'rules';
}
public function getCreatedTime(): int {
return $this->created;
}
public function getCreatedAt(): DateTime {
return DateTime::fromUnixTimeSeconds($this->created);
}
}

View file

@ -12,7 +12,6 @@ class EEPROMContext {
private AuthInfo $authInfo;
private Apps\AppsContext $appsCtx;
private Blacklist\BlacklistContext $blacklistCtx;
private Uploads\UploadsContext $uploadsCtx;
private Users\UsersContext $usersCtx;
@ -23,7 +22,6 @@ class EEPROMContext {
$this->authInfo = new AuthInfo;
$this->appsCtx = new Apps\AppsContext($dbConn);
$this->blacklistCtx = new Blacklist\BlacklistContext($dbConn);
$this->uploadsCtx = new Uploads\UploadsContext($config, $dbConn);
$this->usersCtx = new Users\UsersContext($dbConn);
}
@ -44,10 +42,6 @@ class EEPROMContext {
return $this->appsCtx;
}
public function getBlacklistContext(): Blacklist\BlacklistContext {
return $this->blacklistCtx;
}
public function getUploadsContext(): Uploads\UploadsContext {
return $this->uploadsCtx;
}
@ -61,7 +55,7 @@ class EEPROMContext {
if($isApiDomain) {
$routingCtx->register(new Auth\AuthRoutes(
$this->config->scopeTo('misuzu'),
$this->config->scopeTo('auth'),
$this->authInfo,
$this->usersCtx
));
@ -73,7 +67,6 @@ class EEPROMContext {
$this->authInfo,
$this->appsCtx,
$this->uploadsCtx,
$this->blacklistCtx,
$isApiDomain
));

View file

@ -2,20 +2,21 @@
namespace EEPROM;
use stdClass;
use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Routing\Route;
use Index\Routing\RouteHandler;
class LandingRoutes extends RouteHandler {
public function __construct(
private DatabaseContext $dbCtx
) {}
#[HttpGet('/')]
#[Route('GET', '/')]
public function getIndex($response) {
$response->accelRedirect('/index.html');
$response->setContentType('text/html; charset=utf-8');
}
#[HttpGet('/stats.json')]
#[Route('GET', '/stats.json')]
public function getStats() {
$dbConn = $this->dbCtx->getConnection();
@ -25,7 +26,7 @@ class LandingRoutes extends RouteHandler {
$stats->types = 0;
$stats->members = 0;
$result = $dbConn->query('SELECT COUNT(upload_id), SUM(upload_size), COUNT(DISTINCT upload_type) FROM prm_uploads WHERE upload_deleted IS NULL');
$result = $dbConn->query('SELECT COUNT(upload_id), SUM(upload_size), COUNT(DISTINCT upload_type) FROM prm_uploads WHERE upload_deleted IS NULL AND upload_dmca IS NULL');
if($result->next()) {
$stats->files = $result->getInteger(0);
$stats->size = $result->getInteger(1);
@ -39,9 +40,9 @@ class LandingRoutes extends RouteHandler {
return $stats;
}
#[HttpGet('/eeprom.js')]
#[Route('GET', '/eeprom.js')]
public function getEepromJs($response) {
$response->accelRedirect('/scripts/eepromv1.js');
$response->accelRedirect('/js/eeprom-v1.0.js');
$response->setContentType('application/javascript; charset=utf-8');
}
}

View file

@ -1,15 +1,17 @@
<?php
namespace EEPROM;
use Index\Http\HttpFx;
use Index\Http\HttpRequest;
use Index\Http\Routing\{HttpRouter,IRouter,IRouteHandler};
use Index\Routing\IRouter;
use Index\Routing\IRouteHandler;
use Syokuhou\IConfig;
class RoutingContext {
private HttpRouter $router;
private HttpFx $router;
public function __construct(private IConfig $config) {
$this->router = new HttpRouter;
$this->router = new HttpFx;
$this->router->use('/', $this->middleware(...));
}

View file

@ -16,6 +16,7 @@ class UploadInfo {
private ?int $accessed;
private ?int $expires;
private ?int $deleted;
private ?int $dmca;
private int $bump;
private string $name;
private string $type;
@ -31,10 +32,11 @@ class UploadInfo {
$this->accessed = $result->getIntegerOrNull(6);
$this->expires = $result->getIntegerOrNull(7);
$this->deleted = $result->getIntegerOrNull(8);
$this->bump = $result->getInteger(9);
$this->name = $result->getString(10);
$this->type = $result->getString(11);
$this->size = $result->getInteger(12);
$this->dmca = $result->getIntegerOrNull(9);
$this->bump = $result->getInteger(10);
$this->name = $result->getString(11);
$this->type = $result->getString(12);
$this->size = $result->getInteger(13);
}
public function getId(): string {
@ -117,12 +119,20 @@ class UploadInfo {
return $this->deleted === null ? null : DateTime::fromUnixTimeSeconds($this->deleted);
}
public function getBumpAmount(): int {
return $this->bump;
public function isCopyrightTakedown(): bool {
return $this->dmca !== null;
}
public function getBumpAmountForUpdate(): ?int {
return $this->expires !== null && $this->bump > 0 ? $this->bump : null;
public function getCopyrightTakedownTime(): ?int {
return $this->dmca;
}
public function getCopyrightTakedownAt(): ?DateTime {
return $this->dmca === null ? null : DateTime::fromUnixTimeSeconds($this->dmca);
}
public function getBumpAmount(): int {
return $this->bump;
}
public function getName(): string {

View file

@ -65,8 +65,8 @@ class UploadsContext {
return sprintf('%s.t', $this->getFileUrlV1($uploadInfo, $forceApiDomain));
}
public function convertToClientJsonV1(UploadInfo $uploadInfo, array $overrides = []): array {
return array_merge([
public function convertToClientJsonV1(UploadInfo $uploadInfo): array {
return [
'id' => $uploadInfo->getId(),
'url' => $this->getFileUrlV1($uploadInfo),
'urlf' => $this->getFileUrlV1($uploadInfo, true),
@ -84,7 +84,7 @@ class UploadsContext {
// These can never be reached, and in situation where they technically could it's because of an outdated local record
'deleted' => null,
'dmca' => null,
], $overrides);
];
}
public function supportsThumbnailing(UploadInfo $uploadInfo): bool {
@ -108,34 +108,18 @@ class UploadsContext {
$imagick = new Imagick;
if($uploadInfo->isImage()) {
try {
$file = fopen($filePath, 'rb');
$imagick->readImageFile($file);
} finally {
if(isset($file) && is_resource($file))
fclose($file);
}
} elseif($uploadInfo->isAudio()) {
// Index\IO\Stream needs some way to grab the underlying handle so we can use readImageFile
$stream = (string)FFMPEG::grabAudioCover($filePath);
if($stream === '')
return '';
$imagick->readImageBlob($stream);
} elseif($uploadInfo->isVideo()) {
$stream = (string)FFMPEG::grabVideoFrame($filePath);
if($stream === '')
return '';
$imagick->readImageBlob($stream);
}
$imagick->readImageBlob(file_get_contents($filePath));
} elseif($uploadInfo->isAudio())
$imagick->readImageBlob(FFMPEG::grabAudioCover($filePath));
elseif($uploadInfo->isVideo())
$imagick->readImageBlob(FFMPEG::grabVideoFrame($filePath));
$imagick->setImageFormat('jpg');
$imagick->setImageCompressionQuality($this->config->getInteger('thumb:quality', 80));
$imagick->setImageCompressionQuality($this->config->getInteger('thumb:quality', 40));
$thumbRes = $this->config->getInteger('thumb:dimensions', 100);
$width = $imagick->getImageWidth();
$height = $imagick->getImageHeight();
$thumbRes = min($this->config->getInteger('thumb:dimensions', 300), $width, $height);
if($width === $height) {
$resizeWidth = $resizeHeight = $thumbRes;

View file

@ -17,19 +17,23 @@ class UploadsData {
public function getUploads(
?bool $deleted = null,
?bool $expired = null
?bool $expired = null,
?bool $dmca = null
): array {
$hasDeleted = $deleted !== null;
$hasExpired = $expired !== null;
$hasDMCA = $dmca !== null;
$args = 0;
$query = 'SELECT upload_id, user_id, app_id, LOWER(HEX(upload_hash)), INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_accessed), UNIX_TIMESTAMP(upload_expires), UNIX_TIMESTAMP(upload_deleted), upload_bump, upload_name, upload_type, upload_size FROM prm_uploads';
$query = 'SELECT upload_id, user_id, app_id, LOWER(HEX(upload_hash)), INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_accessed), UNIX_TIMESTAMP(upload_expires), UNIX_TIMESTAMP(upload_deleted), UNIX_TIMESTAMP(upload_dmca), upload_bump, upload_name, upload_type, upload_size FROM prm_uploads';
if($hasDeleted) {
++$args;
$query .= sprintf(' WHERE upload_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
}
if($hasExpired)
$query .= sprintf(' %s upload_expires %s NOW()', ++$args > 1 ? 'AND' : 'WHERE', $expired ? '<=' : '>');
if($hasDMCA)
$query .= sprintf(' %s upload_dmca %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $dmca ? 'IS NOT' : 'IS');
$stmt = $this->cache->get($query);
$stmt->execute();
@ -55,7 +59,7 @@ class UploadsData {
$hasUserInfo = $userInfo !== null;
$args = 0;
$query = 'SELECT upload_id, user_id, app_id, LOWER(HEX(upload_hash)), INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_accessed), UNIX_TIMESTAMP(upload_expires), UNIX_TIMESTAMP(upload_deleted), upload_bump, upload_name, upload_type, upload_size FROM prm_uploads';
$query = 'SELECT upload_id, user_id, app_id, LOWER(HEX(upload_hash)), INET6_NTOA(upload_ip), UNIX_TIMESTAMP(upload_created), UNIX_TIMESTAMP(upload_accessed), UNIX_TIMESTAMP(upload_expires), UNIX_TIMESTAMP(upload_deleted), UNIX_TIMESTAMP(upload_dmca), upload_bump, upload_name, upload_type, upload_size FROM prm_uploads';
if($hasUploadId) {
++$args;
$query .= ' WHERE upload_id = ?';
@ -121,39 +125,23 @@ class UploadsData {
return $this->getUpload(uploadId: $uploadId);
}
public function updateUpload(
UploadInfo|string $uploadInfo,
?string $fileName = null,
int|null|bool $accessedAt = false,
int|null|false $expiresAt = false
): void {
$fields = [];
$values = [];
public function bumpUploadAccess(UploadInfo $uploadInfo): void {
$stmt = $this->cache->get('UPDATE prm_uploads SET upload_accessed = NOW() WHERE upload_id = ?');
$stmt->addParameter(1, $uploadInfo->getId());
$stmt->execute();
}
if($fileName !== null) {
$fields[] = 'upload_name = ?';
$values[] = $fileName;
}
if($accessedAt !== false) {
$fields[] = sprintf('upload_accessed = %s', $accessedAt === true ? 'NOW()' : 'FROM_UNIXTIME(?)');
if($accessedAt !== true)
$values[] = $accessedAt;
}
if($expiresAt !== false) {
$fields[] = 'upload_expires = NOW() + INTERVAL ? SECOND';
$values[] = $expiresAt;
}
if(empty($fields))
public function bumpUploadExpires(UploadInfo|string $uploadInfo): void {
if(!$uploadInfo->hasExpiryTime())
return;
$args = 0;
$stmt = $this->cache->get(sprintf('UPDATE prm_uploads SET %s WHERE upload_id = ?', implode(', ', $fields)));
foreach($values as $value)
$stmt->addParameter(++$args, $value);
$stmt->addParameter(++$args, $uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo);
$bumpAmount = $uploadInfo->getBumpAmount();
if($bumpAmount < 1)
return;
$stmt = $this->cache->get('UPDATE prm_uploads SET upload_expires = NOW() + INTERVAL ? SECOND WHERE upload_id = ?');
$stmt->addParameter(1, $bumpAmount);
$stmt->addParameter(2, $uploadInfo->getId());
$stmt->execute();
}
@ -170,7 +158,7 @@ class UploadsData {
}
public function nukeUpload(UploadInfo|string $uploadInfo): void {
$stmt = $this->cache->get('DELETE FROM prm_uploads WHERE upload_id = ?');
$stmt = $this->cache->get('DELETE FROM prm_uploads WHERE upload_id = ? AND upload_dmca IS NULL');
$stmt->addParameter(1, $uploadInfo instanceof UploadInfo ? $uploadInfo->getId() : $uploadInfo);
$stmt->execute();
}

View file

@ -2,10 +2,11 @@
namespace EEPROM\Uploads;
use RuntimeException;
use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,IRouter,IRouteHandler};
use Index\Routing\IRouter;
use Index\Routing\IRouteHandler;
use Index\Routing\Route;
use EEPROM\Apps\AppsContext;
use EEPROM\Auth\AuthInfo;
use EEPROM\Blacklist\BlacklistContext;
use EEPROM\Uploads\UploadsContext;
class UploadsRoutes implements IRouteHandler {
@ -13,22 +14,21 @@ class UploadsRoutes implements IRouteHandler {
private AuthInfo $authInfo,
private AppsContext $appsCtx,
private UploadsContext $uploadsCtx,
private BlacklistContext $blacklistCtx,
private bool $isApiDomain
) {}
public function registerRoutes(IRouter $router): void {
if($this->isApiDomain) {
HandlerAttribute::register($router, $this);
Route::handleAttributes($router, $this);
} else {
$router->options('/', $this->getUpload(...));
$router->get('/([A-Za-z0-9\-_]+)(?:\.(t))?', $this->getUpload(...));
$router->get('/:filename', $this->getUpload(...));
}
}
#[HttpOptions('/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?')]
#[HttpGet('/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?')]
public function getUpload($response, $request, string $fileId, string $fileExt = '') {
#[Route('OPTIONS', '/uploads/:filename')]
#[Route('GET', '/uploads/:filename')]
public function getUpload($response, $request, string $fileName) {
if($this->isApiDomain) {
if($request->hasHeader('Origin'))
$response->setHeader('Access-Control-Allow-Credentials', 'true');
@ -42,6 +42,9 @@ class UploadsRoutes implements IRouteHandler {
if($request->getMethod() === 'OPTIONS')
return 204;
$pathInfo = pathinfo($fileName);
$fileId = $pathInfo['filename'];
$fileExt = $pathInfo['extension'] ?? '';
$isData = $fileExt === '';
$isThumbnail = $fileExt === 't';
$isJson = $this->isApiDomain && $fileExt === 'json';
@ -56,15 +59,9 @@ class UploadsRoutes implements IRouteHandler {
return 404;
}
$blInfo = $this->blacklistCtx->getBlacklistEntry($uploadInfo);
if($blInfo !== null) {
$response->setContent(match($blInfo->getReason()) {
'copyright' => 'File is unavailable for copyright reasons.',
'rules' => 'File was in violation of the rules.',
default => 'File was removed for reasons beyond understanding.',
});
return $blInfo->isCopyrightTakedown() ? 451 : 410;
if($uploadInfo->isCopyrightTakedown()) {
$response->setContent('File is unavailable for copyright reasons.');
return 451;
}
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
@ -81,12 +78,10 @@ class UploadsRoutes implements IRouteHandler {
return 404;
}
if(!$isThumbnail)
$uploadsData->updateUpload(
$uploadInfo,
accessedAt: true,
expiresAt: $uploadInfo->getBumpAmountForUpdate(),
);
if(!$isThumbnail) {
$uploadsData->bumpUploadAccess($uploadInfo);
$uploadsData->bumpUploadExpires($uploadInfo);
}
$fileName = $uploadInfo->getName();
$contentType = $uploadInfo->getMediaTypeString();
@ -110,8 +105,8 @@ class UploadsRoutes implements IRouteHandler {
$response->setFileName(addslashes($fileName));
}
#[HttpOptions('/uploads')]
#[HttpPost('/uploads')]
#[Route('OPTIONS', '/uploads')]
#[Route('POST', '/uploads')]
public function postUpload($response, $request) {
if($request->hasHeader('Origin'))
$response->setHeader('Access-Control-Allow-Credentials', 'true');
@ -128,7 +123,7 @@ class UploadsRoutes implements IRouteHandler {
$content = $request->getContent();
try {
$appInfo = $this->appsCtx->getApp((string)$content->getParam('src', FILTER_VALIDATE_INT));
$appInfo = $this->appsCtx->getApp($content->getParam('src', FILTER_VALIDATE_INT));
} catch(RuntimeException $ex) {
return 404;
}
@ -162,16 +157,23 @@ class UploadsRoutes implements IRouteHandler {
$uploadsData = $this->uploadsCtx->getUploadsData();
$hash = hash_file('sha256', $localFile);
$blInfo = $this->blacklistCtx->getBlacklistEntry(hex2bin($hash));
if($blInfo !== null)
return 451;
// this is stupid: dmca status is stored as a file record rather than in a separate table requiring this hack ass garbage
$uploadInfo = $uploadsData->getUpload(appInfo: $appInfo, userInfo: $userInfo, hashString: $hash)
?? $uploadsData->getUpload(hashString: $hash);
if($uploadInfo !== null) {
if($uploadInfo->isCopyrightTakedown())
return 451;
if($uploadInfo->getUserId() !== $userInfo->getId()
|| $uploadInfo->getAppId() !== $appInfo->getId())
$uploadInfo = null;
}
$fileName = $file->getSuggestedFileName();
$uploadInfo = $uploadsData->getUpload(appInfo: $appInfo, userInfo: $userInfo, hashString: $hash);
if($uploadInfo === null) {
$uploadInfo = $uploadsData->createUpload(
$appInfo, $userInfo, $_SERVER['REMOTE_ADDR'],
$fileName, mime_content_type($localFile),
$file->getSuggestedFileName(), mime_content_type($localFile),
$fileSize, $hash, $appInfo->getBumpAmount(), true
);
$filePath = $this->uploadsCtx->getFileDataPath($uploadInfo);
@ -181,29 +183,17 @@ class UploadsRoutes implements IRouteHandler {
if($uploadInfo->isDeleted())
$uploadsData->restoreUpload($uploadInfo);
$uploadsData->updateUpload(
$uploadInfo,
fileName: $fileName,
expiresAt: $uploadInfo->getBumpAmountForUpdate(),
);
$uploadsData->bumpUploadExpires($uploadInfo);
}
$response->setStatusCode(201);
$response->setHeader('Content-Type', 'application/json; charset=utf-8');
return $this->uploadsCtx->convertToClientJsonV1($uploadInfo, [
'name' => $fileName,
]);
return $this->uploadsCtx->convertToClientJsonV1($uploadInfo);
}
#[HttpDelete('/uploads/([A-Za-z0-9\-_]+)')]
#[Route('DELETE', '/uploads/:fileid')]
public function deleteUpload($response, $request, string $fileId) {
if($request->hasHeader('Origin'))
$response->setHeader('Access-Control-Allow-Credentials', 'true');
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, DELETE');
if(!$this->authInfo->isLoggedIn())
return 401;
@ -215,6 +205,11 @@ class UploadsRoutes implements IRouteHandler {
return 404;
}
if($uploadInfo->isCopyrightTakedown()) {
$response->setContent('File is unavailable for copyright reasons.');
return 451;
}
if($uploadInfo->isDeleted() || $uploadInfo->hasExpired()) {
$response->setContent('File not found.');
return 404;

View file

@ -1,11 +0,0 @@
#!/usr/bin/env php
<?php
require_once __DIR__ . '/../eeprom.php';
try {
touch(PRM_ROOT . '/.migrating');
chmod(PRM_ROOT . '/.migrating', 0777);
} finally {
unlink(PRM_ROOT . '/.migrating');
}