<?php
class CONFIG
{
    const MAX_FILESIZE =   1; //max. filesize in MiB
    const MAX_FILEAGE =  365; //max. age of files in days
    const MIN_FILEAGE =    3; //min. age of files in days
    const DECAY_EXP =    1.5;

    const UPLOAD_TIMEOUT = 5*60; //max. time an upload can take before it times out
    const MIN_ID_LENGTH = 1; //min. length of the random file ID
    const MAX_ID_LENGTH = 2; //max. length of the random file ID, set to MIN_ID_LENGTH to disable
    const STORE_PATH = '/var/www/html/up/load/'; //directory to store uploaded files in
    const LOG_PATH = '/tmp/0x0log'; //path to log uploads + resulting links to
    const DOWNLOAD_PATH = '%s'; //the path part of the download url. %s = placeholder for filename
    const MAX_EXT_LEN = 8; //max. length for file extensions
    const EXTERNAL_HOOK = null; //external program to call for each upload
    const AUTO_FILE_EXT = true; //automatically try to detect file extension for files that have none

    const PERMITTED_EXT = array(
                                'c'    => true,
                                'h'    => true,
                                'm4'   => true,
                                'md'   => true,
                                'org'  => true,
                                'txt'  => true,
                                'diff' => true,
                                'gpg'  => true,
                                'pdf'  => true,
                                'avif' => true,
                                'jpeg' => true,
                                'jpg'  => true,
                                'png'  => true,
                                'jxl'  => true,
                                'gif'  => true,
                                'mkv'  => true,
                                'mp4'  => true,
                                'webm' => true
);
    const LIMIT_EXT = true; // to enable the usage of the above

    const FORCE_HTTPS = true; //force generated links to be https://

    const ADMIN_EMAIL = 'admin@chud.cyou';  //address for inquiries

    public static function SITE_URL() : string
    {
        $proto = ($_SERVER['HTTPS'] ?? 'off') == 'on' || CONFIG::FORCE_HTTPS ? 'https' : 'http';
        return "$proto://up.{$_SERVER['HTTP_HOST']}";
    }

    public static function SCRIPT_URL() : string
    {
        return CONFIG::SITE_URL().$_SERVER['REQUEST_URI'];
    }
};


// generate a random string of characters with given length
function rnd_str(int $len) : string
{
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
    $max_idx = strlen($chars) - 1;
    $out = '';
    while ($len--)
    {
        $out .= $chars[mt_rand(0,$max_idx)];
    }
    return $out;
}

// check php.ini settings and print warnings if anything's not configured properly
function check_config() : void
{
    return;
    $warn_config_value = function($ini_name, $var_name, $var_val)
    {
        $ini_val = intval(ini_get($ini_name));
        if ($ini_val < $var_val)
            print("<pre>Warning: php.ini: $ini_name ($ini_val) set lower than $var_name ($var_val)\n</pre>");
    };

    $warn_config_value('upload_max_filesize', 'MAX_FILESIZE', CONFIG::MAX_FILESIZE);
    $warn_config_value('post_max_size', 'MAX_FILESIZE', CONFIG::MAX_FILESIZE);
    $warn_config_value('max_input_time', 'UPLOAD_TIMEOUT', CONFIG::UPLOAD_TIMEOUT);
    $warn_config_value('max_execution_time', 'UPLOAD_TIMEOUT', CONFIG::UPLOAD_TIMEOUT);
}

//extract extension from a path (does not include the dot)
function ext_by_path(string $path) : string
{
    $ext = pathinfo($path, PATHINFO_EXTENSION);
    //special handling of .tar.* archives
    $ext2 = pathinfo(substr($path,0,-(strlen($ext)+1)), PATHINFO_EXTENSION);
    if ($ext2 === 'tar')
    {
        $ext = $ext2.'.'.$ext;
    }
    return $ext;
}

function ext_by_finfo(string $path) : string
{
    $finfo = finfo_open(FILEINFO_EXTENSION);
    $finfo_ext = finfo_file($finfo, $path);
    finfo_close($finfo);
    if ($finfo_ext != '???')
    {
        return explode('/', $finfo_ext, 2)[0];
    }
    else
    {
        $finfo = finfo_open();
        $finfo_info = finfo_file($finfo, $path);
        finfo_close($finfo);
        if (strstr($finfo_info, 'text') !== false)
        {
            return 'txt';
        }
    }
    return '';
}

// store an uploaded file, given its name and temporary path (e.g. values straight out of $_FILES)
// files are stored wit a randomised name, but with their original extension
//
// $name: original filename
// $tmpfile: temporary path of uploaded file
// $formatted: set to true to display formatted message instead of bare link
function store_file(string $name, string $tmpfile, bool $formatted = false) : void
{
    //create folder, if it doesn't exist
    if (!file_exists(CONFIG::STORE_PATH))
    {
        mkdir(CONFIG::STORE_PATH, 0750, true); //TODO: error handling
    }

    //check file size
    $size = filesize($tmpfile);
    if ($size > CONFIG::MAX_FILESIZE * 1024 * 1024)
    {
        header('HTTP/1.0 413 Payload Too Large');
        print("Error 413: Max File Size ({CONFIG::MAX_FILESIZE} MiB) Exceeded\n");
        return;
    }
    if ($size == 0)
    {
        header('HTTP/1.0 400 Bad Request');
        print('Error 400: Uploaded file is empty\n');
        return;
    }

    $ext = ext_by_path($name);
    if (empty($ext) && CONFIG::AUTO_FILE_EXT)
    {
        $ext = ext_by_finfo($tmpfile);
    }
    $ext = substr($ext, 0, CONFIG::MAX_EXT_LEN);

    if (CONFIG::LIMIT_EXT) {
        $permitted_ext = CONFIG::PERMITTED_EXT;
        if ($permitted_ext[$ext] != true) {
            header('HTTP/1.0 400 Bad Request');
            return;
        }
    }

    $tries_per_len=3; //try random names a few times before upping the length

    $id_length=CONFIG::MIN_ID_LENGTH;
    if(isset($_POST['id_length']) && ctype_digit($_POST['id_length'])) {
        $id_length = max(CONFIG::MIN_ID_LENGTH, min(CONFIG::MAX_ID_LENGTH, $_POST['id_length']));
    }

    for ($len = $id_length; ; ++$len)
    {
        for ($n=0; $n<=$tries_per_len; ++$n)
        {
            $id = rnd_str($len);
            $basename = $id . (empty($ext) ? '' : '.' . $ext);
            $target_file = CONFIG::STORE_PATH . $basename;

            if (!file_exists($target_file))
                break 2;
        }
    }

    $res = move_uploaded_file($tmpfile, $target_file);
    if (!$res)
    {
        //TODO: proper error handling?
        header('HTTP/1.0 520 Unknown Error');
        return;
    }

    if (CONFIG::EXTERNAL_HOOK !== null)
    {
        putenv('REMOTE_ADDR='.$_SERVER['REMOTE_ADDR']);
        putenv('ORIGINAL_NAME='.$name);
        putenv('STORED_FILE='.$target_file);
        $ret = -1;
        $out = null;
        $last_line = exec(CONFIG::EXTERNAL_HOOK, $out, $ret);
        if ($last_line !== false && $ret !== 0)
        {
            unlink($target_file);
            header('HTTP/1.0 400 Bad Request');
            print("Error: $last_line\n");
            return;
        }
    }

    //print the download link of the file
    $url = sprintf(CONFIG::SITE_URL().'/'.CONFIG::DOWNLOAD_PATH, $basename);

    if ($formatted)
    {
        print("<center><h1>Uploaded!</h1><h2>Access your file here: <a href=\"$url\">$url</a></h2></center>");
    }
    else
    {
        print("$url\n");
    }

    // log uploader's IP, original filename, etc.
    if (CONFIG::LOG_PATH)
    {
        file_put_contents(
            CONFIG::LOG_PATH,
            implode("\t", array(
                date('c'),
                $_SERVER['REMOTE_ADDR'],
                filesize($tmpfile),
                escapeshellarg($name),
                $basename
            )) . "\n",
            FILE_APPEND
        );
    }
}

// purge all files older than their retention period allows.
function purge_files() : void
{
    $num_del = 0;    //number of deleted files
    $total_size = 0; //total size of deleted files

    //for each stored file
    foreach (scandir(CONFIG::STORE_PATH) as $file)
    {
        //skip virtual . and .. files
        if ($file === '.' ||
            $file === '..')
        {
            continue;
        }

        $file = CONFIG::STORE_PATH . $file;

        $file_size = filesize($file) / (1024*1024); //size in MiB
        $file_age = (time()-filemtime($file)) / (60*60*24); //age in days

        //keep all files below the min age
        if ($file_age < CONFIG::MIN_FILEAGE)
        {
            continue;
        }

        //calculate the maximum age in days for this file
        $file_max_age = CONFIG::MIN_FILEAGE +
                        (CONFIG::MAX_FILEAGE - CONFIG::MIN_FILEAGE) *
                        pow(1 - ($file_size / CONFIG::MAX_FILESIZE), CONFIG::DECAY_EXP);

        //delete if older
        if ($file_age > $file_max_age)
        {
            unlink($file);

            print("deleted $file, $file_size MiB, $file_age days old\n");
            $num_del += 1;
            $total_size += $file_size;
        }
    }
    print("Deleted $num_del files totalling $total_size MiB\n");
}

function send_text_file(string $filename, string $content) : void
{
    header('Content-type: application/octet-stream');
    header("Content-Disposition: attachment; filename=\"$filename\"");
    header('Content-Length: '.strlen($content));
    print($content);
}

// send a ShareX custom uploader config as .json
function send_sharex_config() : void
{
    $name = $_SERVER['SERVER_NAME'];
    $site_url = str_replace("?sharex", "", CONFIG::SCRIPT_URL());
    send_text_file($name.'.sxcu', <<<EOT
{
  "Name": "$name",
  "DestinationType": "ImageUploader, FileUploader",
  "RequestType": "POST",
  "RequestURL": "$site_url",
  "FileFormName": "file",
  "ResponseType": "Text"
}
EOT);
}

// send a Hupl uploader config as .hupl (which is just JSON)
function send_hupl_config() : void
{
    $name = $_SERVER['SERVER_NAME'];
    $site_url = str_replace("?hupl", "", CONFIG::SCRIPT_URL());
    send_text_file($name.'.hupl', <<<EOT
{
  "name": "$name",
  "type": "http",
  "targetUrl": "$site_url",
  "fileParam": "file"
}
EOT);
}

// print a plaintext info page, explaining what this script does and how to
// use it, how to upload, etc.
function print_index() : void
{
    $site_url = CONFIG::SCRIPT_URL();
    $sharex_url = $site_url.'?sharex';
    $hupl_url = $site_url.'?hupl';
    $decay = CONFIG::DECAY_EXP;
    $min_age = CONFIG::MIN_FILEAGE;
    $max_size = CONFIG::MAX_FILESIZE;
    $max_age = CONFIG::MAX_FILEAGE;
    $mail = CONFIG::ADMIN_EMAIL;
    $max_id_length = CONFIG::MAX_ID_LENGTH;

    $length_info = "\nTo use a longer file ID (up to $max_id_length characters), add -F id_length=&lt;number&gt;\n";
    if (CONFIG::MIN_ID_LENGTH == CONFIG::MAX_ID_LENGTH)
    {
        $length_info  = "";
    }

$ext = array_keys(CONFIG::PERMITTED_EXT);
$permitted_ext = "";

for($i=0; $i < count($ext); ++$i) {
if ($i+1 < count($ext)) { $permitted_ext .= $ext[$i] . ", "; }
else { $permitted_ext .= $ext[$i]; }
}

$alias_file = file_get_contents('aliases');

echo <<<EOT
<!DOCTYPE html>
<html lang="en">
<head>
<title>UpChud</title>
<meta name="description" content="Minimalistic service for sharing temporary files." />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/css/style.css">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon.png">
</head>
<body>
<div class="box">
<div class="back"><a href="https://chud.cyou/">&lt; Back</a></div>
<h1 class="subbox">- - - - - - - - - - - - - UpChud - - - - - - - - - - - -</h1>
<p>You can upload files to this site via a simple HTTP POST, e.g. using curl:</p>
<div class="box2"><pre>curl -F "file=@./file" https://chud.cyou/up</pre></div>
<p>Or simply choose a file and click "Upload" below:</p>
<div class="box2">
<br/>
<small>Notice: by using this method, you accept that you have lost "The Game"</small>
<form id="frm" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file" />
<input type="hidden" name="formatted" value="true" />
<input type="submit" value="Upload"/>
</form>
<br/>
</div>
<hr/>
<h2>Limits!</h2>
<lo>
<li>The maximum allowed file size is $max_size MiB.</li>
<li>Files are kept for a minimum of $min_age, and a maximum of $max_age Days.</li>
<li>Permitted filetypes are:
<div class="box2">
<pre>$permitted_ext</pre>
</div>
</li>
</lo>
<p>
How long a file is kept depends on its size. Larger files are deleted earlier 
than small ones.<br/> This relation is non-linear and skewed in favour of small 
files.<br />
</p>
<div class="box2">
<pre>MIN_AGE + (MAX_AGE - MIN_AGE) * (1-(FILE_SIZE/MAX_SIZE))^$decay</pre>
</div>
<hr/>
<h2><a href="aliases">Aliases</a></h2>
<small>These really aren't aliases, but bash functions, but... it's bash. - And I put these in my aliases file.</small>
<div class="box2">
<pre>$alias_file</pre>
</div>
<hr/>
<h2>Source</h2>
<p>The UpChud page's source can be seen on <a href="https://git.xolatile.top/emil/chud.cyou">the git</a>.</p>
<p>The unmodified PHP script used to provide this service is open source and available on 
<a href="https://github.com/Rouji/single_php_filehost">GitHub</a>.</p>
<hr/>
<h2>"Muh TOS"</h2>
<p>- No Porn Or Illegal Activity As Per U.S. Law.</p>
<p>- For programming, screenshots, small file transfer, & mildly amusing images.</p>
<hr/>
<h2>Contact</h2>
<p>If you want to report abuse of this service, or have any other inquiries, 
please write an email to <a href="mailto:$mail">$mail</a></p>
</div>
</body>
</html>
EOT;
}


// decide what to do, based on POST parameters etc.
if (isset($_FILES['file']['name']) &&
    isset($_FILES['file']['tmp_name']) &&
    is_uploaded_file($_FILES['file']['tmp_name']))
{
    //file was uploaded, store it
    $formatted = isset($_REQUEST['formatted']);
    store_file($_FILES['file']['name'],
              $_FILES['file']['tmp_name'],
              $formatted);
}
else if (isset($_GET['sharex']))
{
    send_sharex_config();
}
else if (isset($_GET['hupl']))
{
    send_hupl_config();
}
else if ($argv[1] ?? null === 'purge')
{
    purge_files();
}
else
{
    check_config();
    print_index();
}
