Skip to content
Snippets Groups Projects
class.thread.php 46.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • 
            if (!$vars['body'] instanceof ThreadBody) {
                if ($cfg->isHtmlThreadEnabled())
                    $vars['body'] = new HtmlThreadBody($vars['body']);
                else
                    $vars['body'] = new TextThreadBody($vars['body']);
    
            // Drop stripped images
    
            if ($vars['attachments']) {
    
                foreach ($vars['body']->getStrippedImages() as $cid) {
    
                    foreach ($vars['attachments'] as $i=>$a) {
                        if (@$a['cid'] && $a['cid'] == $cid) {
                            // Inline referenced attachment was stripped
    
                            unset($vars['attachments'][$i]);
    
            // Handle extracted embedded images (<img src="data:base64,..." />).
            // The extraction has already been performed in the ThreadBody
            // class. Here they should simply be added to the attachments list
            if ($atts = $vars['body']->getEmbeddedHtmlImages()) {
                if (!is_array($vars['attachments']))
                    $vars['attachments'] = array();
                foreach ($atts as $info) {
                    $vars['attachments'][] = $info;
                }
            }
    
    
            if (!($body = $vars['body']->getClean()))
    
                $body = '-'; //Special tag used to signify empty message as stored.
    
            $poster = $vars['poster'];
            if ($poster && is_object($poster))
    
    Peter Rotich's avatar
    Peter Rotich committed
                $poster = (string) $poster;
    
    Peter Rotich's avatar
    Peter Rotich committed
            $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '
                .' ,thread_type='.db_input($vars['type'])
                .' ,ticket_id='.db_input($vars['ticketId'])
    
                .' ,title='.db_input(Format::sanitize($vars['title'], true))
    
                .' ,format='.db_input($vars['body']->getType())
    
    Peter Rotich's avatar
    Peter Rotich committed
                .' ,staff_id='.db_input($vars['staffId'])
    
                .' ,user_id='.db_input($vars['userId'])
    
                .' ,poster='.db_input($poster)
    
    Peter Rotich's avatar
    Peter Rotich committed
                .' ,source='.db_input($vars['source']);
    
    
            if (!isset($vars['attachments']) || !$vars['attachments'])
    
                // Otherwise, body will be configured in a block below (after
                // inline attachments are saved and updated in the database)
    
    Peter Rotich's avatar
    Peter Rotich committed
            if(isset($vars['pid']))
                $sql.=' ,pid='.db_input($vars['pid']);
    
            // Check if 'reply_to' is in the $vars as the previous ThreadEntry
            // instance. If the body of the previous message is found in the new
            // body, strip it out.
            elseif (isset($vars['reply_to'])
                    && $vars['reply_to'] instanceof ThreadEntry)
                $sql.=' ,pid='.db_input($vars['reply_to']->getId());
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if($vars['ip_address'])
                $sql.=' ,ip_address='.db_input($vars['ip_address']);
    
            //echo $sql;
            if(!db_query($sql) || !($entry=self::lookup(db_insert_id(), $vars['ticketId'])))
                return false;
    
            /************* ATTACHMENTS *****************/
    
            //Upload/save attachments IF ANY
            if($vars['files']) //expects well formatted and VALIDATED files array.
                $entry->uploadFiles($vars['files']);
    
            //Canned attachments...
            if($vars['cannedattachments'] && is_array($vars['cannedattachments']))
                $entry->saveAttachments($vars['cannedattachments']);
    
    
            //Emailed or API attachments
            if (isset($vars['attachments']) && $vars['attachments']) {
    
                foreach ($vars['attachments'] as &$a)
    
                    if (isset($a['cid']) && $a['cid']
                            && strpos($body, 'cid:'.$a['cid']) !== false)
    
                        $a['inline'] = true;
                unset($a);
    
    
                $entry->importAttachments($vars['attachments']);
    
                foreach ($vars['attachments'] as $a) {
    
                    // Change <img src="cid:"> inside the message to point to
                    // a unique hash-code for the attachment. Since the
                    // content-id will be discarded, only the unique hash-code
                    // will be available to retrieve the image later
                    if ($a['cid'] && $a['key']) {
    
                        $body = preg_replace('/src=("|\'|\b)(?:cid:)?'
                            . preg_quote($a['cid'], '/').'\1/i',
    
                            'src="cid:'.$a['key'].'"', $body);
    
                $sql = 'UPDATE '.TICKET_THREAD_TABLE.' SET body='.db_input($body)
    
                    .' WHERE `id`='.db_input($entry->getId());
                if (!db_query($sql) || !db_affected_rows())
                    return false;
            }
    
    
            // Email message id (required for all thread posts)
            if (!isset($vars['mid']))
                $vars['mid'] = sprintf('<%s@%s>', Misc::randCode(24),
                    substr(md5($cfg->getUrl()), -10));
            $entry->saveEmailInfo($vars);
    
    
            // Inline images (attached to the draft)
    
            $entry->saveAttachments(Draft::getAttachmentIds($body));
    
            Signal::send('model.created', $entry);
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            return $entry;
        }
    
        function add($vars) {
            return ($entry=self::create($vars))?$entry->getId():0;
        }
    
    }
    
    /* Message - Ticket thread entry of type message */
    class Message extends ThreadEntry {
    
        function Message($id, $ticketId=0) {
            parent::ThreadEntry($id, 'M', $ticketId);
        }
    
        function getSubject() {
            return $this->getTitle();
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function create($vars, &$errors) {
            return self::lookup(self::add($vars, $errors));
        }
    
        function add($vars, &$errors) {
    
            if(!$vars || !is_array($vars) || !$vars['ticketId'])
    
                $errors['err'] = __('Missing or invalid data');
    
    Peter Rotich's avatar
    Peter Rotich committed
            elseif(!$vars['message'])
    
                $errors['message'] = __('Message content is required');
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if($errors) return false;
    
            $vars['type'] = 'M';
            $vars['body'] = $vars['message'];
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            if (!$vars['poster']
                    && $vars['userId']
                    && ($user = User::lookup($vars['userId'])))
                $vars['poster'] = (string) $user->getName();
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            return ThreadEntry::add($vars);
        }
    
        function lookup($id, $tid=0, $type='M') {
    
            return ($id
    
                    && is_numeric($id)
    
    Peter Rotich's avatar
    Peter Rotich committed
                    && ($m = new Message($id, $tid))
    
                    && $m->getId()==$id
                    )?$m:null;
    
        function lastByTicketId($ticketId) {
    
            return self::byTicketId($ticketId);
        }
    
        function firstByTicketId($ticketId) {
            return self::byTicketId($ticketId, false);
        }
    
        function byTicketId($ticketId, $last=true) {
    
    
            $sql=' SELECT thread.id FROM '.TICKET_THREAD_TABLE.' thread '
                .' WHERE thread_type=\'M\' AND thread.ticket_id = '.db_input($ticketId)
    
                .sprintf(' ORDER BY thread.id %s LIMIT 1', $last ? 'DESC' : 'ASC');
    
    
            if (($res = db_query($sql)) && ($id = db_result($res)))
    
                return Message::lookup($id);
    
    }
    
    /* Response - Ticket thread entry of type response */
    class Response extends ThreadEntry {
    
        function Response($id, $ticketId=0) {
            parent::ThreadEntry($id, 'R', $ticketId);
        }
    
        function getSubject() {
            return $this->getTitle();
        }
    
        function getRespondent() {
            return $this->getStaff();
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function create($vars, &$errors) {
            return self::lookup(self::add($vars, $errors));
        }
    
        function add($vars, &$errors) {
    
            if(!$vars || !is_array($vars) || !$vars['ticketId'])
    
                $errors['err'] = __('Missing or invalid data');
    
    Peter Rotich's avatar
    Peter Rotich committed
            elseif(!$vars['response'])
    
                $errors['response'] = __('Response content is required');
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if($errors) return false;
    
            $vars['type'] = 'R';
            $vars['body'] = $vars['response'];
            if(!$vars['pid'] && $vars['msgId'])
                $vars['pid'] = $vars['msgId'];
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            if (!$vars['poster']
                    && $vars['staffId']
                    && ($staff = Staff::lookup($vars['staffId'])))
                $vars['poster'] = (string) $staff->getName();
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            return ThreadEntry::add($vars);
        }
    
    
        function lookup($id, $tid=0, $type='R') {
    
            return ($id
    
                    && is_numeric($id)
    
    Peter Rotich's avatar
    Peter Rotich committed
                    && ($r = new Response($id, $tid))
    
                    && $r->getId()==$id
                    )?$r:null;
    
        }
    }
    
    /* Note - Ticket thread entry of type note (Internal Note) */
    class Note extends ThreadEntry {
    
        function Note($id, $ticketId=0) {
            parent::ThreadEntry($id, 'N', $ticketId);
        }
    
        function getMessage() {
            return $this->getBody();
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        /* static */
        function create($vars, &$errors) {
            return self::lookup(self::add($vars, $errors));
        }
    
        function add($vars, &$errors) {
    
            //Check required params.
            if(!$vars || !is_array($vars) || !$vars['ticketId'])
    
                $errors['err'] = __('Missing or invalid data');
    
    Peter Rotich's avatar
    Peter Rotich committed
            elseif(!$vars['note'])
    
                $errors['note'] = __('Note content is required');
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if($errors) return false;
    
            //TODO: use array_intersect_key  when we move to php 5 to extract just what we need.
            $vars['type'] = 'N';
            $vars['body'] = $vars['note'];
    
            return ThreadEntry::add($vars);
        }
    
        function lookup($id, $tid=0, $type='N') {
    
            return ($id
    
                    && is_numeric($id)
    
    Peter Rotich's avatar
    Peter Rotich committed
                    && ($n = new Note($id, $tid))
    
                    && $n->getId()==$id
                    )?$n:null;
    
    
    class ThreadBody /* extends SplString */ {
    
    
        static $types = array('text', 'html');
    
        var $stripped_images = array();
    
        var $embedded_images = array();
    
        var $options = array(
            'strip-embedded' => true
        );
    
        function __construct($body, $type='text', $options=array()) {
    
            $type = strtolower($type);
            if (!in_array($type, static::$types))
    
                throw new Exception("$type: Unsupported ThreadBody type");
    
            $this->body = (string) $body;
    
            if (strlen($this->body) > 250000) {
                $max_packet = db_get_variable('max_allowed_packet', 'global');
                // Truncate just short of the max_allowed_packet
    
                $this->body = substr($this->body, 0, $max_packet - 2048) . ' ... '
    
                   . _S('(truncated)');
    
            $this->options = array_merge($this->options, $options);
    
        function isEmpty() {
            return !$this->body || $this->body == '-';
    
        }
    
        function convertTo($type) {
            if ($type === $this->type)
                return $this;
    
            $conv = $this->type . ':' . strtolower($type);
            switch ($conv) {
            case 'text:html':
                return new ThreadBody(sprintf('<pre>%s</pre>',
                    Format::htmlchars($this->body)), $type);
            case 'html:text':
    
                return new ThreadBody(Format::html2text((string) $this), $type);
    
        function stripQuotedReply($tag) {
    
            //Strip quoted reply...on emailed  messages
            if (!$tag || strpos($this->body, $tag) === false)
                return;
    
    
            // Capture a list of inline images
            $images_before = $images_after = array();
    
            preg_match_all('/src=("|\'|\b)cid:(\S+)\1/', $this->body, $images_before,
    
                PREG_PATTERN_ORDER);
    
    
            // Strip the quoted part of the body
            if ((list($msg) = explode($tag, $this->body, 2)) && trim($msg)) {
    
                $this->body = $msg;
    
    
                // Capture a list of dropped inline images
                if ($images_before) {
    
                    preg_match_all('/src=("|\'|\b)cid:(\S+)\1/', $this->body,
    
                        $images_after, PREG_PATTERN_ORDER);
    
                    $this->stripped_images = array_diff($images_before[2],
                        $images_after[2]);
    
                }
            }
        }
    
        function getStrippedImages() {
            return $this->stripped_images;
    
        function getEmbeddedHtmlImages() {
            return $this->embedded_images;
        }
    
    
        function getType() {
            return $this->type;
        }
    
    
        function getClean() {
            return trim($this->body);
        }
    
    
        function __toString() {
    
            return (string) $this->body;
    
        }
    
        function toHtml() {
            return $this->display('html');
        }
    
    
        function display($format=false) {
    
            throw new Exception('display: Abstract display() method not implemented');
    
        function getSearchable() {
    
            return Format::searchable($this->body);
    
        static function fromFormattedText($text, $format=false) {
            switch ($format) {
            case 'text':
                return new TextThreadBody($text);
            case 'html':
                return new HtmlThreadBody($text, array('strip-embedded'=>false));
            default:
                return new ThreadBody($text);
            }
    
        }
    }
    
    class TextThreadBody extends ThreadBody {
    
        function __construct($body, $options=array()) {
            parent::__construct($body, 'text', $options);
        }
    
        function getClean() {
            return Format::stripEmptyLines($this->body);
        }
    
        function display($output=false) {
            if ($this->isEmpty())
                return '(empty)';
    
            switch ($output) {
            case 'html':
    
                return '<div style="white-space:pre-wrap">'
    
                    .Format::clickableurls(Format::htmlchars($this->body)).'</div>';
    
            case 'email':
    
                return '<div style="white-space:pre-wrap">'.$this->body.'</div>';
            case 'pdf':
                return nl2br($this->body);
            default:
                return '<pre>'.$this->body.'</pre>';
            }
    
    
        function asVar() {
            // Email template, assume HTML
    
            return $this->display('email');
    
    }
    class HtmlThreadBody extends ThreadBody {
    
        function __construct($body, $options=array()) {
    
            if (!isset($options['strip-embedded']) || $options['strip-embedded'])
    
                $body = $this->extractEmbeddedHtmlImages($body);
    
            parent::__construct($body, 'html', $options);
    
    
        function extractEmbeddedHtmlImages($body) {
            $self = $this;
            return preg_replace_callback('/src="(data:[^"]+)"/',
            function ($m) use ($self) {
                $info = Format::parseRfc2397($m[1], false, false);
                $info['cid'] = 'img'.Misc::randCode(12);
                list(,$type) = explode('/', $info['type'], 2);
                $info['name'] = 'image'.Misc::randCode(4).'.'.$type;
                $self->embedded_images[] = $info;
                return 'src="cid:'.$info['cid'].'"';
            }, $body);
        }
    
        function getClean() {
    
            return trim($this->body, " <>br/\t\n\r") ? Format::sanitize($this->body) : '';
    
        function getSearchable() {
            // <br> -> \n
    
            $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body);
            $body = Format::htmldecode(Format::striptags($body));
            return Format::searchable($body);
    
        function display($output=false) {
            if ($this->isEmpty())
                return '(empty)';
    
            switch ($output) {
    
            case 'email':
                return $this->body;
    
            case 'pdf':
                return Format::clickableurls($this->body, false);
            default:
                return Format::display($this->body);
            }