Skip to content
Snippets Groups Projects
class.file.php 12.5 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:
    **********************************************************************/
    
    class AttachmentFile {
    
        var $id;
        var $ht;
    
        function AttachmentFile($id) {
            $this->id =0;
            return ($this->load($id));
        }
    
        function load($id=0) {
    
            if(!$id && !($id=$this->getId()))
                return false;
    
    
            $sql='SELECT id, type, size, name, hash, f.created, '
                .' count(DISTINCT c.canned_id) as canned, count(DISTINCT t.ticket_id) as tickets '
    
    Jared Hancock's avatar
    Jared Hancock committed
                .' FROM '.FILE_TABLE.' f '
                .' LEFT JOIN '.CANNED_ATTACHMENT_TABLE.' c ON(c.file_id=f.id) '
                .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' t ON(t.file_id=f.id) '
                .' WHERE f.id='.db_input($id)
                .' GROUP BY f.id';
            if(!($res=db_query($sql)) || !db_num_rows($res))
                return false;
    
            $this->ht=db_fetch_array($res);
            $this->id =$this->ht['id'];
    
            return true;
        }
    
        function reload() {
            return $this->load();
        }
    
        function getHashtable() {
            return $this->ht;
        }
    
        function getInfo() {
            return $this->getHashtable();
        }
    
        function getNumTickets() {
            return $this->ht['tickets'];
        }
    
        function isCanned() {
            return ($this->ht['canned']);
        }
    
        function isInUse() {
            return ($this->getNumTickets() || $this->isCanned());
        }
    
        function getId() {
            return $this->id;
        }
    
        function getType() {
            return $this->ht['type'];
        }
    
        function getMime() {
            return $this->getType();
        }
    
        function getSize() {
            return $this->ht['size'];
        }
    
        function getName() {
            return $this->ht['name'];
        }
    
        function getHash() {
            return $this->ht['hash'];
        }
    
    
        function lastModified() {
            return $this->ht['created'];
        }
    
        /**
         * Retrieve a hash that can be sent to scp/file.php?h= in order to
         * download this file
         */
        function getDownloadHash() {
            return strtolower($this->getHash() . md5($this->getId().session_id().$this->getHash()));
        }
    
    
        function open() {
            return new AttachmentChunkedData($this->id);
        }
    
    
        function sendData() {
    
            @ini_set('zlib.output_compression', 'Off');
    
            $file = $this->open();
            while ($chunk = $file->read())
                echo $chunk;
    
    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();
            $this->sendData();
            $data = &ob_get_contents();
            ob_end_clean();
            return $data;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function delete() {
    
            $sql='DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1';
    
            if(!db_query($sql) || !db_affected_rows())
                return false;
    
            //Delete file data.
            AttachmentChunkedData::deleteOrphans();
    
            return true;
    
        function makeCacheable($ttl=3600) {
    
            // Thanks, http://stackoverflow.com/a/1583753/1025836
    
            $last_modified = Misc::db2gmtime($this->lastModified());
            header("Last-Modified: ".date('D, d M y H:i:s', $last_modified)." GMT", false);
    
            header('ETag: "'.$this->getHash().'"');
    
            header("Cache-Control: private, max-age=$ttl");
            header('Expires: ' . gmdate(DATE_RFC822, time() + $ttl)." GMT");
    
            header('Pragma: private');
            if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified ||
                @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $this->getHash()) {
                    header("HTTP/1.1 304 Not Modified");
                    exit();
            }
    
        }
    
        function display() {
            $this->makeCacheable();
    
            header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream'));
    
    Jared Hancock's avatar
    Jared Hancock committed
            header('Content-Length: '.$this->getSize());
    
            $this->sendData();
    
    Jared Hancock's avatar
    Jared Hancock committed
            exit();
        }
    
        function download() {
    
            $this->makeCacheable();
    
            header('Content-Type: '.($this->getType()?$this->getType():'application/octet-stream'));
    
    Jared Hancock's avatar
    Jared Hancock committed
            $filename=basename($this->getName());
            $user_agent = strtolower ($_SERVER['HTTP_USER_AGENT']);
    
            if (false !== strpos($user_agent,'msie') && false !== strpos($user_agent,'win'))
                header('Content-Disposition: filename='.rawurlencode($filename).';');
            elseif (false !== strpos($user_agent, 'safari') && false === strpos($user_agent, 'chrome'))
                // Safari and Safari only can handle the filename as is
                header('Content-Disposition: filename='.str_replace(',', '', $filename).';');
            else
                // Use RFC5987
                header("Content-Disposition: filename*=UTF-8''".rawurlencode($filename).';' );
    
    Jared Hancock's avatar
    Jared Hancock committed
            header('Content-Transfer-Encoding: binary');
            header('Content-Length: '.$this->getSize());
    
            $this->sendData();
    
        /* Function assumes the files types have been validated */
    
        function upload($file, $ft='T') {
    
    
            if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name']))
    
    Jared Hancock's avatar
    Jared Hancock committed
                return false;
    
            $info=array('type'=>$file['type'],
    
                        'filetype'=>$ft,
    
    Jared Hancock's avatar
    Jared Hancock committed
                        'size'=>$file['size'],
                        'name'=>$file['name'],
                        'hash'=>MD5(MD5_FILE($file['tmp_name']).time()),
                        'data'=>file_get_contents($file['tmp_name'])
                        );
    
            return AttachmentFile::save($info);
        }
    
    
        function uploadLogo($file, &$error, $aspect_ratio=3) {
            /* 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');
    
            $error = 'Image is too square. Upload a wider image';
            return false;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function save($file) {
    
            if(!$file['hash'])
                $file['hash']=MD5(MD5($file['data']).time());
            if(!$file['size'])
                $file['size']=strlen($file['data']);
    
    Jared Hancock's avatar
    Jared Hancock committed
            $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() '
                .',type='.db_input($file['type'])
                .',size='.db_input($file['size'])
    
                .',name='.db_input($file['name'])
    
                .',hash='.db_input($file['hash']);
    
            # XXX: ft does not exists during the upgrade when attachments are
            #      migrated!
            if(isset($file['filetype']))
                $sql.=',ft='.db_input($file['filetype']);
    
    
            if (!(db_query($sql) && ($id=db_insert_id())))
                return false;
    
    
            $data = new AttachmentChunkedData($id);
            if (!$data->write($file['data']))
                return false;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        /* Static functions */
        function getIdByHash($hash) {
    
            $sql='SELECT id FROM '.FILE_TABLE.' WHERE hash='.db_input($hash);
            if(($res=db_query($sql)) && db_num_rows($res))
                list($id)=db_fetch_row($res);
    
            return $id;
        }
    
        function lookup($id) {
    
            $id = is_numeric($id)?$id:AttachmentFile::getIdByHash($id);
    
    Jared Hancock's avatar
    Jared Hancock committed
            return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null;
        }
    
          Method formats http based $_FILE uploads - plus basic validation.
          @restrict - make sure file type & size are allowed.
         */
        function format($files, $restrict=false) {
            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';
                elseif($restrict) { // make sure file type & size are allowed.
                    if(!$ost->isFileTypeAllowed($file))
                        $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']);
                    elseif($ost->getConfig()->getMaxFileSize()
                            && $file['size']>$ost->getConfig()->getMaxFileSize())
                        $file['error'] = sprintf('File %s (%s) is too big. Maximum of %s allowed',
                                Format::htmlchars($file['name']),
                                Format::file_size($file['size']),
                                Format::file_size($ost->getConfig()->getMaxFileSize()));
                }
            }
            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() {
    
            $sql = 'DELETE FROM '.FILE_TABLE.' WHERE id NOT IN ('
    
                    # DISTINCT implies sort and may not be necessary
                    .'SELECT DISTINCT(file_id) FROM ('
                        .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE
                        .' UNION ALL '
                        .'SELECT file_id FROM '.CANNED_ATTACHMENT_TABLE
                        .' UNION ALL '
                        .'SELECT file_id FROM '.FAQ_ATTACHMENT_TABLE
                    .') still_loved'
    
                .') AND `ft` = "T"';
    
            AttachmentChunkedData::deleteOrphans();
    
    
        /* static */
        function allLogos() {
            $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE ft="L"
                ORDER BY created';
            $logos = array();
            $res = db_query($sql);
            while (list($id) = db_fetch_row($res))
                $logos[] = AttachmentFile::lookup($id);
            return $logos;
        }
    
    /**
     * Attachments stored in the database are cut into 256kB 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 {
        function AttachmentChunkedData($file) {
            $this->_file = $file;
            $this->_pos = 0;
    
        function length() {
            list($length) = db_fetch_row(db_query(
                 'SELECT SUM(LENGTH(filedata)) FROM '.FILE_CHUNK_TABLE
                .' WHERE file_id='.db_input($this->_file)));
            return $length;
    
            # Read requested length of data from attachment chunks
    
            list($buffer) = @db_fetch_row(db_query(
                'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id='
                .db_input($this->_file).' AND chunk_id='.$this->_pos++));
    
            return $buffer;
        }
    
    
        function write($what, $chunk_size=CHUNK_SIZE) {
    
            $offset=0;
            for (;;) {
    
                $block = substr($what, $offset, $chunk_size);
    
                if (!$block) break;
                if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE
    
                        .' SET filedata=0x'.bin2hex($block).', file_id='
                        .db_input($this->_file).', chunk_id='.db_input($this->_pos++)))
    
                    return false;
    
                $offset += strlen($block);
    
            $sql = 'DELETE c.* FROM '.FILE_CHUNK_TABLE.' c '
                 . ' LEFT JOIN '.FILE_TABLE.' f ON(f.id=c.file_id) '
                 . ' WHERE f.id IS NULL';
    
            return db_query($sql)?db_affected_rows():0;