The Framework Code

class/framework/pages/getfile.php

File List

<?php
/**
 * A class that contains code to handle file data fetching requests related requests.
 *
 * This assumes that access control is needed for the files - if it isn't then the files
 * should be stored in a sub-directory of the assets directory (or directories) in the root of the site and
 * the web server will deal with things like range requests etc.
 *
 * As written it assumes that there is a directory in the root of the site whose
 * name is set in the constant DATADIR. It also assumes that there are subdirectories
 * in DATADIR that provide the structure /user_id/year/month/filename
 *
 * This code provides a very simple access control scheme whereby there is an upload database table
 * that relates a filename with a user so that you can check
 * that only the owner (or the admin) can access the file. The table
 * should also contain the original filename that the user used when uploading the file, as this is returned
 * as part of Content-Disposition. Allowing sharing with specified other users, groups of users or users with particular roles
 * would not be hard to add.
 *
 * @author Lindsay Marshall <lindsay.marshall@ncl.ac.uk>
 * @copyright 2012-2021 Newcastle University
 * @package Framework
 * @subpackage SystemPages
 */
    namespace Framework\Pages;

    use \Config\Framework as FW;
    use \Support\Context;
/**
 * The Getfile class
 *
 * This returns a file requested from the upload area
 */
    class Getfile extends \Framework\SiteAction
    {
/*
 * The name of the directory where files are kept
 */
        private const DATADIR   = 'private';
/** @var string The name of the file we are working on */
        private string $file = '';
/** @var string    The last modified time for the file */
        private string $mtime = '';

        use \Support\GetFile;

/**
 * Return data files as requested
 * Always return empty string as all the file sending is done internally.
 *
 * @throws \Framework\Exception\BadValue
 * @throws \Framework\Exception\Forbidden
 */
        public function handle(Context $context) : array|string
        {
            $web = $context->web(); // it's used all over the place so grab it once

            \chdir($context->local()->basedir());
            $fpt = $context->rest();

            if (\count($fpt) >= 2 && $fpt[0] == 'file')
            { // this is access by upload ID
                $file = \R::load(FW::UPLOAD, (int) $fpt[1]);
                if ($file->getID() == 0)
                {
                    return $this->missing();
                }
                $this->file = \substr($file->fname, 1); // drop the separator at the start....
            }
            else
            {
                \chdir(self::DATADIR);
/**
 * Depending on how you construct the URL, it's possible to do some sanity checks on the
 * values passed in. The structure assumed here is /user_id/year/month/filename so
 * the regexp test following makes sense.
 * This all depends on your application and how you want to treat files and filenames and access of course!
 *
 * Always be careful that filenames do not have .. in them of course.
 */
                $this->file = \implode(DIRECTORY_SEPARATOR, $fpt);
                if (!\preg_match('#^[0-9]+/[0-9]+/[0-9]+/[^/]+$#', \implode('/', $fpt)))
                { // filename constructed is not the right format
                    return $this->other('Illegal filename');
                }
/*
 * Now do an access control check
 */
                $file = \R::findOne(FW::UPLOAD, 'fname=?', [\DIRECTORY_SEPARATOR . self::DATADIR . \DIRECTORY_SEPARATOR . $this->file]);
                if (!\is_object($file))
                { // not recorded in the database so 404 it
                    $web->notfound();
                    /* NOT REACHED */
                }
            }
            if (!$file->canAccess($context->user(), 'r'))
            { // current user cannot access the file
                return $this->noaccess();
            }
            /** @psalm-suppress InvalidPropertyAssignmentValue */
            if (($this->mtime = (string) \filemtime($this->file)) === FALSE)
            {
                $web->internal('Lost File: '.$this->file);
                /* NOT REACHED */
            }
            $file->downloaded($context);
            $this->ifmodcheck($context); // check to see if we actually need to send anything

            $web->addheader([
//                'Last-Modified'   => $this->mtime,
                'Etag'      => '"'.$this->makeetag($context).'"',
            ]);
            $web->sendfile($this->file, $file->filename);
            return '';
        }
    }
?>