arrel/main.php

269 lines
6.1 KiB
PHP

<?php
session_start();
// Global variables
$_dr = $_SERVER['DOCUMENT_ROOT'];
// TODO: Make configurable in .ini
// Actual filesystem
$GLOBALS['path_root'] = '/srv/http/boorein';
$GLOBALS['path_media'] = $GLOBALS['path_root'] . '/media';
$GLOBALS['path_mediadb'] = $GLOBALS['path_root'] . '/media.db';
// Uri addressed
$GLOBALS['base_root'] =
$_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
$GLOBALS['base_media'] = 'media';
header('Content-Type: application/json');
// ----- ROUTER -----
(match ([$_SERVER['DOCUMENT_URI'], $_SERVER['REQUEST_METHOD']]) {
['/item', 'POST'] => post_item(),
['/item', 'GET'] => get_item(),
//['/search', 'GET'] => get_search(),
default => Json::error('Not implemented')
})->die();
function get_item()
{
$id = $_GET['q'] ?? false;
if (!$id) Json::error('ID not specified')->die();
$media_db = new MediaDB();
$item = Item::load($media_db, $id);
if ($item === null) {
Json::error('No item was found with the given ID')->die();
}
return Json::new($item->getUri());
}
function post_item()
{
// The checks are performed internally
$item = Item::upload($_FILES['file']);
return Json::new([
'type' => 'success',
'id' => $item->getHash(),
'link' => $item->getUri(),
]);
};
class MediaDB
{
private $handler;
private bool $finished = false;
private array $lines = [];
public function __construct()
{
// Just initialize the file handler
$this->handler = fopen($GLOBALS['path_mediadb'], 'r');
if ($this->handler === false)
Json::error('Error opening media DB');
}
private function getLine(): ?array
{
$ret = fgetcsv($this->handler, 0, ' ');
if ($ret === false)
return null;
return $ret;
}
public function map(callable $func)
{
// First read from what's already in memory
foreach ($this->lines as $i_line) {
if ($func($i_line) === false) return;
}
// REVIEW: Maybe this isn't needed cuz the while would be as eficient
// anyway?
// If we already got to the end, there's no sense in carrying on
if ($this->finished) return;
// If we run out, read from the file and append to array so the next lookup
// is fast
while ($line = $this->getLine()) {
$this->lines[] = $line;
if ($func($line) === false) return;
}
// Set the flag so we don't reach this point two times
$this->finished = true;
}
}
class Item
{
// TODO: Change to id here in all occurrences
private string $hash;
private string $extension;
private array $tags = [];
public static function load(MediaDB $db, string $id): ?Item
{
$ret = null;
$db->map(function ($line) use (&$ret, $id) {
if ($line[0] == $id) {
$ret = new Item();
$ret->hash = array_shift($line);
$ret->tags = $line;
$ret->extensionFromTags();
return true;
}
return false;
});
return $ret;
}
// ::upload() requires the PHP file upload ARRAY from $_FILES
public static function upload(array $php_file): Item
{
$from_path = $php_file['tmp_name'] ?? null;
// --- CHECKS ---
if (!is_string($from_path))
Json::error('Passed invalid upload structure')->die();
if (!is_uploaded_file($from_path))
Json::error('Trying to upload illegal or non-existent file')->die();
// --- INITIALIZE ---
$ret = new Item();
$ret->hash = hash_file('sha256', $from_path);
$ret->extensionFromUpload($php_file);
// --- ACTUALLY GRAB FILE ---
$new_path = $ret->getPath();
if (file_exists($new_path))
Json::error('File already exists')->die();
if (!move_uploaded_file($from_path, $new_path))
Json::error('Failed to move uploaded file')->die();
return $ret;
}
private function extensionFromTags()
{
foreach ($this->tags as $i_tag) {
$parts = explode(':', $i_tag);
if ($parts[0] == 'format') {
$this->extension = $parts[1];
return;
}
}
}
// Assumes is_uploaded_file has been done
private function extensionFromUpload(array $php_file)
{
$path = $php_file['tmp_name'] ?? false;
$name = $php_file['name'] ?? null;
// Try to get extension from mimetype
$ext = (new finfo(FILEINFO_EXTENSION))->file($path);
// REVIEW: Maybe change it so it doesn't have to be separated like this but
// do somethign like ['mime/type', null] be equivalent to this?
if (array_search($ext, ['webp', 'png', 'jpeg']) !== false) {
$this->extension = $ext;
return;
}
// If it doesn't work, try to figure it out from the original extension
if ($ext == '???') {
// We mustn't accept everything straight away, so we're only accepting
// mime-type and original extension combinations that make sense
$whitelist = [
['application/zip', 'kra'],
['application/zip', 'krz'],
];
// NOTE: The original extension has to be grabbed from the original name
$mime = mime_content_type($path);
$ext = pathinfo($name, PATHINFO_EXTENSION);
if (array_search([$mime, $ext], $whitelist) !== false) {
$this->extension = $ext;
return;
}
}
Json::error('File mime-type and or extension not allowed.')->die();
}
public function getHash(): string
{
return $this->hash;
}
public function getExtension(): string
{
return $this->extension;
}
public function getPath(bool $absolute = false): string
{
return sprintf(
'%s/%s.%s',
$GLOBALS[$absolute ? 'path_media' : 'path_media'],
$this->hash,
$this->extension
);
}
public function getUri(): string
{
// TODO: Implement relative
return sprintf(
'%s/%s/%s.%s',
$GLOBALS['base_root'],
$GLOBALS['base_media'],
$this->hash,
$this->extension,
);
}
}
class Json
{
function __construct(
private mixed $struct
) {
$this->struct = $struct;
}
public static function new(mixed $struct): Json
{
return new Json($struct);
}
public static function error(mixed $msg): Json
{
return new Json([
'type' => 'error',
'msg' => $msg,
]);
}
public function die()
{
print(json_encode($this->struct));
die();
}
}