Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
flash | 86e455bd5b | ||
flash | 81ae1976e5 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,11 +3,9 @@
|
|||
.DS_Store
|
||||
/public/data/*
|
||||
/public/thumb/*
|
||||
/public/scripts
|
||||
/public/robots.txt
|
||||
/config.cfg
|
||||
/config.ini
|
||||
/.debug
|
||||
/vendor
|
||||
/.migrating
|
||||
/node_modules
|
||||
|
|
2
LICENCE
2
LICENCE
|
@ -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
|
||||
|
|
|
@ -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
102
build.js
|
@ -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);
|
||||
})();
|
|
@ -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
1172
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
||||
|
|
9
cron.php
9
cron.php
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
209
package-lock.json
generated
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@swc/core": "^1.3.107"
|
||||
}
|
||||
}
|
|
@ -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
211
public/js/eeprom-v1.0.js
Normal 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;
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
})();
|
|
@ -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,
|
||||
};
|
||||
})();
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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),
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
7
src/Auth/IAuth.php
Normal 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
58
src/Auth/MisuzuAuth.php
Normal 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
36
src/Auth/NabuccoAuth.php
Normal 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']);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
));
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(...));
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
Loading…
Reference in a new issue