scratch – Rev 141

Subversion Repositories:
Rev:
<?php

###########################################################################
##  Copyright (C) Wizardry and Steamworks 2017 - License: GNU GPLv3      ##
###########################################################################

require_once('php/pseudocrypt.php');
require_once('php/functions.php');
require_once('php/ip.php');
require_once('vendor/autoload.php');

### Load configuration.
$config = spyc_load_file('config.yaml');

#### POST -> upload / GET -> download
switch ($_SERVER['REQUEST_METHOD']) {
    case 'POST':
        #### Script restrictions.
        session_start();
        if (empty($_POST['token']) || !hash_equals($_SESSION['token'], $_POST['token'])) {
            http_response_code(403);
            die('Forbidden.');
        }
        #### Retrieve uploaded file.
        if (!empty($_FILES['file']) and
            is_uploaded_file($_FILES['file']['tmp_name'])) {
            if($_FILES['file']['size'] > $config['ALLOWED_ASSET_SIZE'] * 1048576) {
                http_response_code(403);
                die('File size exceeds '.$config['ALLOWED_ASSET_SIZE'].'MiB.');
            }
            # Regular multipart/form-data upload.
            $name = $_FILES['file']['name'];
            $data = atomized_get_contents($_FILES['file']['tmp_name']);
        } else {
            if((int)get_file_size("php://input") > $config['ALLOWED_ASSET_SIZE'] * 1048576) {
                http_response_code(403);
                die('File size exceeds '.$config['ALLOWED_ASSET_SIZE'].'MiB.');
            }
            # Raw POST data.
            $name = urldecode(@$_SERVER['HTTP_X_FILE_NAME']);
            $data = atomized_get_contents("php://input");
        }

        #### Grab the file extension.
        $fileExtension = pathinfo($name, PATHINFO_EXTENSION);

        #### If the extension is not allowed then change it to a text extension.
        if (!isset($fileExtension) ||
            !in_array(strtoupper($fileExtension),
                array_map('strtoupper', $config['ALLOWED_FILE_EXTENSIONS']))) {
            http_response_code(403);
            die('File extension not allowed.');
        }
    
        #### Hash filename.
        $file = strtolower(
            PseudoCrypt::hash(
                preg_replace(
                    '/\D/',
                    '',
                    hash(
                        'sha512',
                        $data
                    )
                ),
                $config['ASSET_HASH_SIZE']
            )
        );

        #### Build the user path.
        $userPath = join(
            DIRECTORY_SEPARATOR,
            array(
                $config['STORE_FOLDER'],
                $file
            )
        );

        #### Check for path traversals.
        $pathPart = pathinfo($userPath.'.'.$fileExtension);
        if (strcasecmp(
            realpath($pathPart['dirname']), realpath($config['STORE_FOLDER'])) != 0) {
            http_response_code(500);
            die('Internal server error.');
        }

        #### Store the file.
        $timestamp = atomized_put_contents($userPath.'.'.$fileExtension, $data);
        
        ### Log IP address.
        $db = new PDO('sqlite:db/scratch.sqlite3');
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        try {
            $db->beginTransaction();
            
            ## Create tags table if it does not exist.
            $db->query('CREATE TABLE IF NOT EXISTS "uploaders" ("hash" text NOT NULL COLLATE NOCASE, "ip" text COLLATE NOCASE, UNIQUE("hash") ON CONFLICT REPLACE)');
            
            $q = $db->prepare('REPLACE INTO "uploaders" ("hash", "ip") VALUES(:hash, :ip)');
            $q->bindParam(':hash', $file);
            $q->bindParam(':ip', get_ip_address());
            $q->execute();
            
            $db->commit();
        } catch (Exception $e) {
            error_log($e);
            ## Rollback.
            $db->rollback();
        }
        
        ### Process any sent tags.
        if(isset($_POST['tags'])) {
            $tags = json_decode(
                stripslashes(
                    $_POST['tags']
                )
            );
            
            ## If we have any tags then insert them into the database.
            if(!empty($tags)) {
                ## Connect or create the scratch database.
                $db = new PDO('sqlite:db/scratch.sqlite3');
                ## Set the error mode to exceptions.
                $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                try {
                    
                    $db->beginTransaction();
                    
                    ## Create tags table if it does not exist.
                    $db->query('CREATE TABLE IF NOT EXISTS "tags" ("hash" text NOT NULL COLLATE NOCASE, "tag" text COLLATE NOCASE, UNIQUE("hash", "tag") ON CONFLICT REPLACE)');
                    
                    ## Now add all the tags.
                    foreach($tags as $tag) {
                        $q = $db->prepare('REPLACE INTO "tags" ("hash", "tag") VALUES(:hash, :tag)');
                        $q->bindParam(':hash', $file);
                        $q->bindParam(':tag', $tag);
                        $q->execute();
                    }
                    $db->commit();
                } catch (Exception $e) {
                    error_log($e);
                    ## Rollback.
                    $db->rollback();
                }
            }
        }

        ### Hook for various file extensions.
        $opengraph = FALSE;
        switch(strtoupper($fileExtension)) {
            case 'MP4':
            case 'GIF':
                $opengraph = TRUE;
                break;
        }
        
        ### Return the URL to the file.
        header('Content-Type: text/plain; charset=utf-8');
        echo json_encode(
            array(
                "hash" => $file,
                "timestamp" => $timestamp,
                "opengraph" => $opengraph
            )
        );
    break;
    case 'GET':
        ### Tell browser not to cache files.
        header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
        header("Cache-Control: post-check=0, pre-check=0", false);
        header("Pragma: no-cache");
    
        ### If no file has been specified for download then return.
        if (!isset($_GET['hash']) or empty($_GET['hash'])) {
            http_response_code(404);
            die('File not found.');
        }

        ### Find the requested file.
        $file = array_shift(
            preg_grep(
                '/'.$_GET['hash'].'/',
                scandir($config['STORE_FOLDER'])
            )
        );

        if (!isset($file) or empty($file)) {
            http_response_code(404);
            die('File not found.');
        }
        
        ### Check the path for path traversals.
        $fileExtension = pathinfo($file, PATHINFO_EXTENSION);

        #### If the extension is not allowed then return.
        if (!isset($fileExtension) ||
            !in_array(strtoupper($fileExtension),
                array_map('strtoupper', $config['ALLOWED_FILE_EXTENSIONS']))) {
            http_response_code(403);
            die('File extension not allowed.');
        }
        
        #### Build the user path.
        $userPath = join(
            DIRECTORY_SEPARATOR,
            array(
                $config['STORE_FOLDER'],
                $file
            )
        );

        #### Check for path traversals
        $pathPart = pathinfo($userPath);
        if (strcasecmp(
            realpath($pathPart['dirname']), realpath($config['STORE_FOLDER'])) != 0) {
            http_response_code(500);
            die('Internal server error.');
        }

        ### Hook for various file extensions.
        switch(strtoupper($fileExtension)) {
            case "HTML":
            case "HTM":
                header('Content-type: text/html');
                break;
            break;
            case "URL":
                if(preg_match(
                    "/URL=(https?:\/\/[\-_\.\+!\*'\(\),a-zA-Z0-9]+:?[0-9]{0,5}\/.*?)\n/",
                        file_get_contents($userPath), $matches)) {
                    header('Location: '.$matches[1]);
                    return;
                }
            break;
            default:
                ### Open MIME info database and send the content type.
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                if (!$finfo) {
                    http_response_code(500);
                    die('Internal server error.');
                }
                header('Content-type: '.finfo_file($finfo, $userPath));
                finfo_close($finfo);
            break;
        }
        
        ### Send the file along with the inline content disposition.
        header('Content-length: '.(int)get_file_size($userPath));
        header('Content-Disposition: inline; filename="' . basename($userPath) . '"');
        header('Content-Transfer-Encoding: binary');
        header('X-Sendfile: '.$userPath);
    break;
}