269 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			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();
 | 
						|
  }
 | 
						|
}
 |