Skip to content
Snippets Groups Projects
class.file.php 28.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jared Hancock's avatar
    Jared Hancock committed
    <?php
    /*********************************************************************
        class.file.php
    
        Peter Rotich <peter@osticket.com>
    
        Copyright (c)  2006-2013 osTicket
    
    Jared Hancock's avatar
    Jared Hancock committed
        http://www.osticket.com
    
        Released under the GNU General Public License WITHOUT ANY WARRANTY.
        See LICENSE.TXT for details.
    
        vim: expandtab sw=4 ts=4 sts=4:
    **********************************************************************/
    
    require_once(INCLUDE_DIR.'class.signal.php');
    
    require_once(INCLUDE_DIR.'class.error.php');
    
    class AttachmentFile extends VerySimpleModel {
    
        static $meta = array(
            'table' => FILE_TABLE,
            'pk' => array('id'),
            'joins' => array(
                'attachments' => array(
                    'reverse' => 'Attachment.file'
                ),
            ),
        );
    
        static $keyCache = array();
    
        function __onload() {
            // Cache for lookup in the ::lookupByHash method below
            static::$keyCache[$this->key] = $this;
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getHashtable() {
            return $this->ht;
        }
    
        function getInfo() {
            return $this->getHashtable();
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getNumEntries() {
    
            return $this->attachments->count();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function isCanned() {
    
            return $this->getNumEntries();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function isInUse() {
    
            return $this->getNumEntries();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function getId() {
            return $this->id;
        }
    
        function getType() {
    
        function getBackend() {
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getMime() {
            return $this->getType();
        }
    
        function getSize() {
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function getName() {
    
        function getKey() {
    
        function getSignature($cascade=false) {
    
            $sig = $this->signature;
    
            if (!$sig && $cascade) return $this->getKey();
    
            return $sig;
    
        function lastModified() {
    
        function open() {
    
            return FileStorageBackend::getInstance($this);
    
        function sendData($redirect=true, $disposition='inline') {
    
            $bk = $this->open();
    
            if ($redirect && $bk->sendRedirectUrl($disposition))
    
            @ini_set('zlib.output_compression', 'Off');
    
            try {
                $bk->passthru();
            }
            catch (IOException $ex) {
                Http::response(404, 'File not found');
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function getData() {
    
            # XXX: This is horrible, and is subject to php's memory
            #      restrictions, etc. Don't use this function!
            ob_start();
    
            try {
                $this->sendData(false);
            }
            catch (IOException $ex) {
                Http::response(404, 'File not found');
            }
    
            $data = &ob_get_contents();
            ob_end_clean();
            return $data;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function delete() {
    
    
            if ($bk = $this->open())
                $bk->unlink();
    
        function makeCacheable($ttl=86400) {
    
            Http::cacheable($this->getSignature(true), $this->lastModified(), $ttl);
    
        function display($scale=false) {
    
            $this->makeCacheable();
    
            if ($scale && extension_loaded('gd')) {
                $image = imagecreatefromstring($this->getData());
                $width = imagesx($image);
                if ($scale <= $width) {
                    $height = imagesy($image);
                    if ($width > $height) {
                        $heightp = $height * (int)$scale / $width;
                        $widthp = $scale;
                    } else {
                        $widthp = $width * (int)$scale / $height;
                        $heightp = $scale;
                    }
                    $thumb = imagecreatetruecolor($widthp, $heightp);
                    $white = imagecolorallocate($thumb, 255,255,255);
                    imagefill($thumb, 0, 0, $white);
                    imagecopyresized($thumb, $image, 0, 0, 0, 0, $widthp,
                        $heightp, $width, $height);
                    header('Content-Type: image/png');
                    imagepng($thumb);
                    return;
                }
            }
    
            header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream'));
    
    Jared Hancock's avatar
    Jared Hancock committed
            header('Content-Length: '.$this->getSize());
    
            $this->sendData();
    
        function getDownloadUrl($minage=false, $disposition=false, $handler=false) {
            // XXX: Drop this when AttachmentFile goes to ORM
            return static::generateDownloadUrl($this->getId(),
                strtolower($this->getKey()), $this->getSignature(), $minage,
                $disposition, $handler);
        }
    
        static function generateDownloadUrl($id, $key, $hash, $minage=false,
            $disposition=false, $handler=false
        ) {
            // Expire at the nearest midnight, allowing at least 12 hours access
            $minage = $minage ?: 43200;
            $gmnow = Misc::gmtime() + $minage;
            $expires = $gmnow + 86400 - ($gmnow % 86400);
    
            // Generate a signature based on secret content
            $signature = static::_genUrlSignature($id, $key, $hash, $expires);
    
            $handler = $handler ?: ROOT_PATH . 'file.php';
    
            // Return sanitized query string
            $args = array(
                'key' => $key,
                'expires' => $expires,
                'signature' => $signature,
            );
    
            if ($disposition)
                $args['disposition'] = $disposition;
    
            return $handler . '?' . http_build_query($args);
        }
    
        function verifySignature($signature, $expires) {
            $gmnow = Misc::gmtime();
            if ($expires < $gmnow)
                return false;
    
            $check = static::_genUrlSignature($this->getId(), $this->getKey(),
                $this->getSignature(), $expires);
            return $signature == $check;
        }
    
        static function _genUrlSignature($id, $key, $signature, $expires) {
            $pieces = array(
                'Host='.$_SERVER['HTTP_HOST'],
                'Path='.ROOT_PATH,
                'Id='.$id,
                'Key='.strtolower($key),
                'Hash='.$signature,
                'Expires='.$expires,
            );
            return hash_hmac('sha1', implode("\n", $pieces), SECRET_SALT);
        }
    
        function download($disposition=false, $expires=false) {
    
            $disposition = $disposition ?: 'inline';
    
            $bk = $this->open();
    
            if ($bk->sendRedirectUrl($disposition))
    
            $ttl = ($expires) ? $expires - Misc::gmtime() : false;
            $this->makeCacheable($ttl);
    
            $type = $this->getType() ?: 'application/octet-stream';
            if (isset($_REQUEST['overridetype']))
                $type = $_REQUEST['overridetype'];
            Http::download($this->getName(), $type, null, 'inline');
    
    Jared Hancock's avatar
    Jared Hancock committed
            header('Content-Length: '.$this->getSize());
    
            $this->sendData(false);
    
        function _getKeyAndHash($data=false, $file=false) {
            if ($file) {
                $sha1 = base64_encode(sha1_file($data, true));
                $md5 = base64_encode(md5_file($data, true));
            }
            else {
                $sha1 = base64_encode(sha1($data, true));
                $md5 = base64_encode(md5($data, true));
            }
    
            // Use 5 chars from the microtime() prefix and 27 chars from the
            // sha1 hash. This should make a sufficiently strong unique key for
            // file content. In the event there is a sha1 collision for data, it
            // should be unlikely that there will be a collision for the
            // microtime hash coincidently.  Remove =, change + and / to chars
            // better suited for URLs and filesystem paths
            $prefix = base64_encode(sha1(microtime(), true));
            $key = str_replace(
                array('=','+','/'),
    
                array('','-','_'),
    
                substr($prefix, 0, 5) . $sha1);
    
            // The hash is a 32-char value where the first half is from the last
            // 16 chars from the SHA1 hash and the last 16 chars are the last 16
            // chars from the MD5 hash. This should provide for better
            // resiliance against hash collisions and attacks against any one
            // hash algorithm. Since we're using base64 encoding, with 6-bits
            // per char, we should have a total hash strength of 192 bits.
            $hash = str_replace(
                array('=','+','/'),
    
                array('','-','_'),
    
                substr($sha1, 0, 16) . substr($md5, 0, 16));
    
    
            return array($key, $hash);
        }
    
    
        /* Function assumes the files types have been validated */
    
        static function upload($file, $ft='T', $deduplicate=true) {
    
            if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name']))
    
    Jared Hancock's avatar
    Jared Hancock committed
                return false;
    
    
            list($key, $sig) = self::_getKeyAndHash($file['tmp_name'], true);
    
    Jared Hancock's avatar
    Jared Hancock committed
            $info=array('type'=>$file['type'],
    
                        'filetype'=>$ft,
    
    Jared Hancock's avatar
    Jared Hancock committed
                        'size'=>$file['size'],
                        'name'=>$file['name'],
    
                        'signature'=>$sig,
                        'tmp_name'=>$file['tmp_name'],
    
            return static::create($info, $ft, $deduplicate);
    
        static function uploadLogo($file, &$error, $aspect_ratio=2) {
    
            /* Borrowed in part from
             * http://salman-w.blogspot.com/2009/04/crop-to-fit-image-using-aspphp.html
             */
            if (!extension_loaded('gd'))
                return self::upload($file, 'L');
    
            $source_path = $file['tmp_name'];
    
            list($source_width, $source_height, $source_type) = getimagesize($source_path);
    
            switch ($source_type) {
                case IMAGETYPE_GIF:
                case IMAGETYPE_JPEG:
                case IMAGETYPE_PNG:
                    break;
                default:
                    // TODO: Return an error
    
                    $error = __('Invalid image file type');
    
                    return false;
            }
    
            $source_aspect_ratio = $source_width / $source_height;
    
            if ($source_aspect_ratio >= $aspect_ratio)
    
                return self::upload($file, 'L', false);
    
            $error = __('Image is too square. Upload a wider image');
    
        static function create(&$file, $ft='T', $deduplicate=true) {
    
            if (isset($file['encoding'])) {
                switch ($file['encoding']) {
                case 'base64':
                    $file['data'] = base64_decode($file['data']);
                }
            }
    
            if (isset($file['data'])) {
                // Allow a callback function to delay or avoid reading or
                // fetching ihe file contents
                if (is_callable($file['data']))
                    $file['data'] = $file['data']();
    
                list($key, $file['signature'])
                    = self::_getKeyAndHash($file['data']);
                if (!$file['key'])
                    $file['key'] = $key;
    
            if (isset($file['size']) && $file['size'] > 0) {
    
                // Check and see if the file is already on record
    
                $existing = static::objects()->filter(array(
                    'signature' => $file['signature'],
                    'size' => $file['size']
    
    
                // If the record exists in the database already, a file with
                // the same hash and size is already on file -- just return
                // the file
    
                if ($deduplicate && $existing) {
    
                    $file['key'] = $existing->key;
                    return $existing;
    
                }
            }
            elseif (!isset($file['data'])) {
                // Unable to determine the file's size
                return false;
    
            if (!$file['type'] && extension_loaded('fileinfo')) {
    
                $finfo = new finfo(FILEINFO_MIME_TYPE);
                if ($file['data'])
                    $type = $finfo->buffer($file['data']);
                elseif ($file['tmp_name'])
                    $type = $finfo->file($file['tmp_name']);
    
                if ($type)
                    $file['type'] = $type;
            }
    
            if (!$file['type'])
                $file['type'] = 'application/octet-stream';
    
            $f = parent::create(array(
                'type' => strtolower($file['type']),
                'name' => $file['name'],
                'key' => $file['key'],
                'ft' => $ft ?: 'T',
                'signature' => $file['signature'],
            ));
    
            if (isset($file['size']))
                $f->size = $file['size'];
    
            // Note that this is preferred over $f->open() because the file does
            // not have a valid backend configured yet. ::getBackendForFile()
            // will consider the system configuration for storing the file
    
            $bks = array(self::getBackendForFile($f));
            if (!$bks[0]->getBkChar() !== 'D')
                $bks[] = new AttachmentChunkedData($f);
    
            // Consider the selected backen first and then save to database
            // otherwise.
            $succeeded = false;
            foreach ($bks as $bk) {
    
                try {
                    if (isset($file['tmp_name'])) {
                        if ($bk->upload($file['tmp_name'])) {
                            $succeeded = true; break;
                        }
                    }
                    elseif ($bk->write($file['data']) && $bk->flush()) {
    
                        $succeeded = true; break;
                    }
                }
    
                catch (Exception $e) {
                    // Try next backend
    
                }
                // Fallthrough to default backend if different?
    
            if (!$succeeded) {
                // Unable to save data (weird)
    
    
            if (!isset($file['size'])) {
                if ($size = $bk->getSize())
    
                // Prefer mb_strlen, because mbstring.func_overload will
                // automatically prefer it if configured.
    
                elseif (extension_loaded('mbstring'))
    
                    $f->size = mb_strlen($file['data'], '8bit');
    
                // bootstrap.php include a compat version of mb_strlen
    
                    $f->size = strlen($file['data']);
    
        static function __create($file, &$errors) {
            return static::create($file);
        }
    
    
        /**
         * Migrate this file from the current backend to the backend specified.
         *
         * Parameters:
         * $bk - (string) type char of the target storage backend. Use
         *      AttachmentStorageBackend::allRegistered() to get a list of type
         *      chars and associated class names
         *
         * Returns:
         * True if the migration was successful and false otherwise.
         */
        function migrate($bk) {
    
            // Copy the file to the new backend and hash the contents
    
            $target = FileStorageBackend::lookup($bk, $this);
    
            $source = $this->open();
    
    
            // Initialize hashing algorithm to verify uploaded contents
            $algos = $target->getNativeHashAlgos();
            $common_algo = 'sha1';
            if ($algos && is_array($algos)) {
                $supported = hash_algos();
                foreach ($algos as $a) {
                    if (in_array(strtolower($a), $supported)) {
                        $common_algo = strtolower($a);
                        break;
                    }
                }
            }
            $before = hash_init($common_algo);
    
            // TODO: Make this resumable so that if the file cannot be migrated
            //      in the max_execution_time, the migration can be continued
            //      the next time the cron runs
    
            try {
                while ($block = $source->read($target->getBlockSize())) {
                    hash_update($before, $block);
                    $target->write($block);
                }
                $target->flush();
            }
            catch (Exception $e) {
                // Migration failed
                return false;
    
    
            // Ask the backend to generate its own hash if at all possible
            if (!($target_hash = $target->getHashDigest($common_algo))) {
                $after = hash_init($common_algo);
                // Verify that the hash of the target file matches the hash of
                // the source file
                $target = FileStorageBackend::lookup($bk, $this);
                while ($block = $target->read())
                    hash_update($after, $block);
                $target_hash = hash_final($after);
            }
    
            if (hash_final($before) != $target_hash) {
    
                $target->unlink();
    
            $this->bk = $target->getBkChar();
            if (!$this->save())
    
                return false;
    
            return $source->unlink();
        }
    
    
        /**
         * Considers the system's configuration for file storage selection based
         * on the file information and purpose (FAQ attachment, image, etc).
         *
         * Parameters:
         * $file - (hasharray) file information which would be passed to
         * ::save() for instance.
         *
         * Returns:
         * Instance<FileStorageBackend> backend selected based on the file
         * received.
         */
    
        static function getBackendForFile($file) {
            global $cfg;
    
    
            if (!$cfg)
                return new AttachmentChunkedData($file);
    
    
            $char = $cfg->getDefaultStorageBackendChar();
            return FileStorageBackend::lookup($char, $file);
    
        static function lookupByHash($hash) {
    
            if (isset(static::$keyCache[$hash]))
                return static::$keyCache[$hash];
    
    
            // Cache a negative lookup if no such file exists
    
            return parent::lookup(array('key' => $hash));
    
            return is_string($id)
                ? static::lookupByHash($id)
                : parent::lookup($id);
    
          Method formats http based $_FILE uploads - plus basic validation.
         */
    
        function format($files) {
    
            global $ost;
    
            if(!$files || !is_array($files))
                return null;
    
            //Reformat $_FILE  for the sane.
            $attachments = array();
            foreach($files as $k => $a) {
                if(is_array($a))
                    foreach($a as $i => $v)
                        $attachments[$i][$k] = $v;
            }
    
            //Basic validation.
            foreach($attachments as $i => &$file) {
                //skip no file upload "error" - why PHP calls it an error is beyond me.
                if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) {
    
                    unset($attachments[$i]);
    
                    continue;
                }
    
                if($file['error']) //PHP defined error!
                    $file['error'] = 'File upload error #'.$file['error'];
                elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name']))
                    $file['error'] = 'Invalid or bad upload POST';
            }
            unset($file);
    
            return array_filter($attachments);
        }
    
    
        /**
         * Removes files and associated meta-data for files which no ticket,
         * canned-response, or faq point to any more.
         */
    
        static function deleteOrphans() {
    
            // XXX: Allow plugins to define filetypes which do not represent
            //      files attached to tickets or other things in the attachment
            //      table and are not logos
    
            $files = static::objects()
                ->filter(array(
                    'attachments__object_id__isnull' => true,
                    'ft' => 'T',
                    'created__gt' => new DateTime('now -1 day'),
                ));
    
            foreach ($files as $f) {
                if (!$f->delete())
    
                    break;
    
    
        /* static */
        function allLogos() {
    
            return static::objects()
                ->filter(array('ft' => 'L'))
                ->order_by('created');
    
    class FileStorageBackend {
    
        var $meta;
        static $desc = false;
        static $registry;
    
        static $blocksize = 131072;
    
        static $private = false;
    
    
        /**
         * All storage backends should call this function during the request
         * bootstrap phase.
         */
        static function register($typechar, $class) {
            self::$registry[$typechar] = $class;
        }
    
    
        static function allRegistered($private=false) {
            $R = self::$registry;
            if (!$private) {
                foreach ($R as $i=>$bk) {
                    if ($bk::$private)
                        unset($R[$i]);
                }
            }
            return $R;
    
        }
    
        /**
         * Retrieves the type char registered for this storage backend's class.
         * Null is returned if the backend is not properly registered.
         */
    
        function getBkChar() {
    
            foreach (self::$registry as $tc=>$class)
                if ($this instanceof $class)
                    return $tc;
        }
    
    
        static function isRegistered($type) {
            return isset(self::$registry[$type]);
        }
    
    
        static function lookup($type, $file=null) {
            if (!isset(self::$registry[$type]))
                throw new Exception("No such backend registered");
    
            $class = self::$registry[$type];
            return new $class($file);
        }
    
        static function getInstance($file) {
            if (!isset(self::$registry[$file->getBackend()]))
                throw new Exception("No such backend registered");
    
            $class = self::$registry[$file->getBackend()];
            return new $class($file);
        }
    
    
        /**
         * Returns the optimal block size for the backend. When migrating, this
         * size blocks would be best for sending to the ::write() method
         */
        function getBlockSize() {
            return static::$blocksize;
        }
    
    
        /**
         * Create an instance of the storage backend linking the related file.
         * Information about the file metadata is accessible via the received
         * filed object.
         */
        function __construct($meta) {
            $this->meta = $meta;
        }
    
        /**
         * Commit file to the storage backend. This method is used if the
         * backend cannot support writing a file directly. Otherwise, the
         * ::upload($file) method is preferred.
         *
         * Parameters:
         * $data - (string|binary) file contents to be written to the backend
         */
        function write($data) {
            return false;
        }
    
    
        /**
         * Called after all the blocks are sent to the ::write() method. This
         * method should return boolean FALSE if flushing the data was
         * somehow inhibited.
         */
        function flush() {
            return true;
        }
    
    
        /**
         * Upload a file to the backend. This method is preferred over ::write()
         * for files which are uploaded or are otherwise available out of
         * memory. The backend is encouraged to avoid reading the entire
         * contents into memory.
         */
        function upload($filepath) {
    
            return $this->write(file_get_contents($filepath));
    
        }
    
        /**
         * Returns data from the backend, optionally returning only the number
         * of bytes indicated at the specified offset. If the data is available
         * in chunks, one chunk may be returned at a time. The backend should
         * return boolean false when no more chunks are available.
         */
        function read($amount=0, $offset=0) {
    
            return false;
    
        }
    
        /**
         * Convenience method to send all the file to standard output
         */
        function passthru() {
            while ($block = $this->read())
                echo $block;
        }
    
        /**
         * If the data is not stored or not available locally, a redirect
         * response can be sent to the user agent indicating the actual HTTP
         * location of the data.
         *
         * If the data is available locally, this method should return boolean
         * false to indicate that the read() method should be used to retrieve
         * the data and broker it to the user agent.
         */
    
        function sendRedirectUrl($disposition='inline') {
    
            return false;
        }
    
        /**
         * Requests the backend to remove the file contents.
         */
        function unlink() {
            return false;
        }
    
    
        /**
         * Fetches a list of hash algorithms that are supported transparently
         * through the ::write() and ::upload() methods. After writing or
         * uploading file content, the ::getHashDigest($algo) method can be
         * called to get a hash of the remote content without fetching the
         * entire data stream to verify the content locally.
         */
        function getNativeHashAlgos() {
            return array();
        }
    
        /**
         * Returns a hash of the content calculated remotely by the storage
         * backend. If this method fails, the hash chould be calculated by
         * downloading the content and hashing locally
         */
        function getHashDigest($algo) {
            return false;
        }
    
    
        /**
         * getSize
         *
         * Retrieves the size of the contents written or available to be read.
         * The backend should optimize this process if possible by keeping track
         * of the bytes written in a way apart from `strlen`. This value will be
         * used instead of inspecting the contents using `strlen`.
         */
        function getSize() {
            return false;
        }
    
     * Attachments stored in the database are cut into 500kB chunks and stored
    
     * in the FILE_CHUNK_TABLE to overcome the max_allowed_packet limitation of
     * LOB fields in the MySQL database
     */
    
    define('CHUNK_SIZE', 500*1024); # Beware if you change this...
    
    class AttachmentChunkedData extends FileStorageBackend {
    
        static $desc = "In the database";
    
        static $blocksize = CHUNK_SIZE;
    
    
        function __construct($file) {
            $this->file = $file;
    
            $this->_chunk = 0;
            $this->_buffer = false;
    
        function getSize() {
    
            list($length) = db_fetch_row(db_query(
                 'SELECT SUM(LENGTH(filedata)) FROM '.FILE_CHUNK_TABLE
    
                .' WHERE file_id='.db_input($this->file->getId())));
    
            return $length;
    
        function read($amount=CHUNK_SIZE, $offset=0) {
    
            # Read requested length of data from attachment chunks
    
            while (strlen($this->_buffer) < $amount + $offset) {
                list($buf) = @db_fetch_row(db_query(
                    'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id='
                    .db_input($this->file->getId()).' AND chunk_id='.$this->_chunk++));
                if (!$buf)
                    break;
                $this->_buffer .= $buf;
            }
            $chunk = substr($this->_buffer, $offset, $amount);
            $this->_buffer = substr($this->_buffer, $offset + $amount);
            return $chunk;
    
        function write($what, $chunk_size=CHUNK_SIZE) {
    
            $offset=0;
    
            $what = bin2hex($what);
    
            for (;;) {
    
                $block = substr($what, $offset, $chunk_size*2);
    
                if (!$block) break;
                if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE
    
                        .' SET filedata=0x'.$block.', file_id='
    
                        .db_input($this->file->getId()).', chunk_id='.db_input($this->_chunk++)))
    
                    return false;
    
                $offset += strlen($block);
    
            return $this->_chunk;
    
        function unlink() {
            db_query('DELETE FROM '.FILE_CHUNK_TABLE
                .' WHERE file_id='.db_input($this->file->getId()));
            return db_affected_rows() > 0;
        }
    
    FileStorageBackend::register('D', 'AttachmentChunkedData');
    
    /**
     * This class provides an interface for files attached on the filesystem in
     * versions previous to v1.7. The upgrader will keep the attachments on the
     * disk where they were and write the path into the `attrs` field of the
     * %file table. This module will continue to serve those files until they
     * are migrated with the `file` cli app
     */
    class OneSixAttachments extends FileStorageBackend {
        static $desc = "upload_dir folder (from osTicket v1.6)";
        static $private = true;
    
        function read($bytes=32768, $offset=false) {
            $filename = $this->meta->attrs;
            if (!$this->fp)
                $this->fp = @fopen($filename, 'rb');
            if (!$this->fp)
                throw new IOException($filename.': Unable to open for reading');
            if ($offset)
                fseek($this->fp, $offset);
            if (($status = @fread($this->fp, $bytes)) === false)
                throw new IOException($filename.': Unable to read from file');
            return $status;
        }
    
        function passthru() {
            $filename = $this->meta->attrs;
            if (($status = @readfile($filename)) === false)
                throw new IOException($filename.': Unable to read from file');
            return $status;
        }
    
        function write($data) {
            throw new IOException('This backend does not support new files');
        }
    
        function upload($filepath) {
            throw new IOException('This backend does not support new files');
        }
    
        function unlink() {
            $filename = $this->meta->attrs;
            if (!@unlink($filename))
                throw new IOException($filename.': Unable to delete file');
            // Drop usage of the `attrs` field
            $this->meta->attrs = null;
            $this->meta->save();
            return true;
        }
    }
    FileStorageBackend::register('6', 'OneSixAttachments');