Skip to content
Snippets Groups Projects
class.thread.php 61.4 KiB
Newer Older
  • Learn to ignore specific revisions
  •           'files' => __('Attached files'),
              'body' => __('Message body'),
              'create_date' => array(
                  'class' => 'FormattedDate', 'desc' => __('Date created'),
              ),
              'ip_address' => __('IP address of remote user, for web submissions'),
              'poster' => __('Submitter of the thread item'),
    
              'staff' => array(
    
                'class' => 'Staff', 'desc' => __('Agent posting the note or response'),
    
              'title' => __('Subject, if any'),
    
                'class' => 'User', 'desc' => __('User posting the message'),
    
        /**
         * Parameters:
         * mailinfo (hash<String>) email header information. Must include keys
         *  - "mid" => Message-Id header of incoming mail
         *  - "in-reply-to" => Message-Id the email is a direct response to
         *  - "references" => List of Message-Id's the email is in response
         *  - "subject" => Find external ticket number in the subject line
    
         *
         *  seen (by-ref:bool) a flag that will be set if the message-id was
         *      positively found, indicating that the message-id has been
         *      previously seen. This is useful if no thread-id is associated
         *      with the email (if it was rejected for instance).
    
        function lookupByEmailHeaders(&$mailinfo, &$seen=false) {
    
            // Search for messages using the References header, then the
            // in-reply-to header
    
            if ($entry = ThreadEntry::objects()
                ->filter(array('email_info__mid' => $mailinfo['mid']))
                ->first()
            ) {
    
                $seen = true;
    
            $possibles = array();
    
            foreach (array('in-reply-to', 'references') as $header) {
    
                $matches = array();
                if (!isset($mailinfo[$header]) || !$mailinfo[$header])
                    continue;
                // Header may have multiple entries (usually separated by
    
                // spaces ( )
    
                elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header],
                            $matches))
                    continue;
    
    
                // The References header will have the most recent message-id
                // (parent) on the far right.
                // @see rfc 1036, section 2.2.5
                // @see http://www.jwz.org/doc/threading.html
    
                $possibles = array_merge($possibles, array_reverse($matches[0]));
            }
    
            // Add the message id if it is embedded in the body
            $match = array();
            if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`',
                    $mailinfo['message'], $match)
                && !in_array($match[1], $possibles)
            ) {
                $possibles[] = $match[1];
            }
    
            $thread = null;
            foreach ($possibles as $mid) {
    
                // Attempt to detect the ticket and user ids from the
                // message-id header. If the message originated from
                // osTicket, the Mailer class can break it apart. If it came
                // from this help desk, the 'loopback' property will be set
                // to true.
                $mid_info = Mailer::decodeMessageId($mid);
                if ($mid_info['loopback'] && isset($mid_info['uid'])
                    && @$mid_info['entryId']
                    && ($t = ThreadEntry::lookup($mid_info['entryId']))
                    && ($t->thread_id == $mid_info['threadId'])
                ) {
                    if (@$mid_info['userId']) {
                        $mailinfo['userId'] = $mid_info['userId'];
                    }
                    elseif (@$mid_info['staffId']) {
                        $mailinfo['staffId'] = $mid_info['staffId'];
                    }
                    // ThreadEntry was positively identified
                    return $t;
                }
    
                // Try to determine if it's a reply to a tagged email.
                // (Deprecated)
    
                $ref = null;
                if (strpos($mid, '+')) {
                    list($left, $right) = explode('@',$mid);
                    list($left, $ref) = explode('+', $left);
                    $mid = "$left@$right";
                }
                $entries = ThreadEntry::objects()
                    ->filter(array('email_info__mid' => $mid));
                foreach ($entries as $t) {
                    // Capture the first match thread item
                    if (!$thread)
                        $thread = $t;
                    // We found a match  - see if we can ID the user.
                    // XXX: Check access of ref is enough?
                    if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
                        if ($ref[0] =='s') //staff
                            $mailinfo['staffId'] = $uid;
                        else // user or collaborator.
                            $mailinfo['userId'] = $uid;
    
                        // Best possible case — found the thread and the
                        // user
                        return $t;
    
            // Second best case — found a thread but couldn't identify the
            // user from the header. Return the first thread entry matched
            if ($thread)
                return $thread;
    
    
            // Search for ticket by the [#123456] in the subject line
    
            // This is the last resort -  emails must match to avoid message
            // injection by third-party.
    
            $subject = $mailinfo['subject'];
            $match = array();
    
            if ($subject
                    && $mailinfo['email']
    
                    // Required `#` followed by one or more of
                    //      punctuation (-) then letters, numbers, and symbols
                    // (Try not to match closing punctuation (`]`) in [#12345])
                    && preg_match("/#((\p{P}*[^\p{C}\p{Z}\p{P}]+)+)/u", $subject, $match)
    
                    //Lookup by ticket number
    
                    && ($ticket = Ticket::lookupByNumber($match[1]))
    
                    //Lookup the user using the email address
                    && ($user = User::lookup(array('emails__address' => $mailinfo['email'])))) {
                //We have a valid ticket and user
                if ($ticket->getUserId() == $user->getId() //owner
                        ||  ($c = Collaborator::lookup( // check if collaborator
    
                                array('user_id' => $user->getId(),
                                      'thread_id' => $ticket->getThreadId())))) {
    
    
                    $mailinfo['userId'] = $user->getId();
                    return $ticket->getLastMessage();
                }
            }
    
            // Search for the message-id token in the body
    
            // *DEPRECATED* the current algo on outgoing mail will use
            // Mailer::getMessageId as the message id tagged here
    
            if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
    
                    $mailinfo['message'], $match)) {
                // Support new Message-Id format
                if (($info = Mailer::decodeMessageId($match[1]))
                    && $info['loopback']
                    && $info['entryId']
                ) {
                    return ThreadEntry::lookup($info['entryId']);
                }
                // Support old (deprecated) reference format
    
                if ($thread = ThreadEntry::lookupByRefMessageId($match[1],
                        $mailinfo['email']))
                    return $thread;
    
        /**
         * Find a thread entry from a message-id created from the
    
         * ::asMessageId() method.
         *
         * *DEPRECATED* use Mailer::decodeMessageId() instead
    
        function lookupByRefMessageId($mid, $from) {
    
            $mid = trim($mid, '<>');
            list($ver, $ids, $mails) = explode('$', $mid, 3);
    
            // Current version is <null>
            if ($ver !== '')
                return false;
    
            $ids = @unpack('Vthread', base64_decode($ids));
            if (!$ids || !$ids['thread'])
                return false;
    
    
            $entry = ThreadEntry::lookup($ids['thread']);
            if (!$entry)
                return false;
    
            // Compute the value to be compared from $mails (which used to be in
    
            // ThreadEntry::asMessageId() (#nolint)
    
            $domain = md5($ost->getConfig()->getURL());
            $ticket = $entry->getThread()->getObject();
            if (!$ticket instanceof Ticket)
                return false;
    
            $check = sprintf('%s@%s',
    
                substr(md5($from . $ticket->getNumber() . $ticket->getId()), -10),
    
                substr($domain, -10)
            );
    
            if ($check != $mails)
    
    Peter Rotich's avatar
    Peter Rotich committed
        //new entry ... we're trusting the caller to check validity of the data.
    
        static function create($vars, &$errors=array()) {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            //Must have...
    
    Peter Rotich's avatar
    Peter Rotich committed
            if (!$vars['threadId'] || !$vars['type'])
    
    Peter Rotich's avatar
    Peter Rotich committed
                return false;
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            if (!$vars['body'] instanceof ThreadEntryBody) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                    $vars['body'] = new HtmlThreadEntryBody($vars['body']);
    
    Peter Rotich's avatar
    Peter Rotich committed
                    $vars['body'] = new TextThreadEntryBody($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,..." />).
    
    Peter Rotich's avatar
    Peter Rotich committed
            // The extraction has already been performed in the ThreadEntryBody
    
            // 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;
    
            $entry = parent::create(array(
                'created' => SqlFunction::NOW(),
                'type' => $vars['type'],
                'thread_id' => $vars['threadId'],
                'title' => Format::sanitize($vars['title'], true),
                'format' => $vars['body']->getType(),
                'staff_id' => $vars['staffId'],
                'user_id' => $vars['userId'],
                'poster' => $poster,
                'source' => $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']))
    
            // 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)
    
                $entry->pid = $vars['reply_to']->getId();
    
    Peter Rotich's avatar
    Peter Rotich committed
            if ($vars['ip_address'])
    
                $entry->ip_address = $vars['ip_address'];
    
    Peter Rotich's avatar
    Peter Rotich committed
                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);
    
                $entry->body = $body;
                if (!$entry->save())
    
            // Save mail message id, if available
    
            $entry->saveEmailInfo($vars);
    
    
            // Inline images (attached to the draft)
    
            $entry->saveAttachments(Draft::getAttachmentIds($body));
    
            Signal::send('threadentry.created', $entry);
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            return $entry;
        }
    
    
        static function add($vars, &$errors=array()) {
    
    
        // Extensible thread entry actions ------------------------
        /**
         * getActions
         *
         * Retrieve a list of possible actions. This list is shown to the agent
         * via drop-down list at the top-right of the thread entry when rendered
         * in the UI.
         */
        function getActions() {
            if (!isset($this->_actions)) {
                $this->_actions = array();
    
                foreach (self::$action_registry as $group=>$list) {
                    $T = array();
                    $this->_actions[__($group)] = &$T;
                    foreach ($list as $id=>$action) {
                        $A = new $action($this);
                        if ($A->isVisible()) {
                            $T[$id] = $A;
                        }
                    }
                    unset($T);
                }
            }
            return $this->_actions;
        }
    
        function hasActions() {
            foreach ($this->getActions() as $group => $list) {
                if (count($list))
                    return true;
            }
            return false;
        }
    
        function triggerAction($name) {
            foreach ($this->getActions() as $group=>$list) {
                foreach ($list as $id=>$action) {
                    if (0 === strcasecmp($id, $name)) {
                        if (!$action->isEnabled())
                            return false;
    
                        $action->trigger();
                        return true;
                    }
                }
            }
            return false;
        }
    
        static $action_registry = array();
    
        static function registerAction($group, $action) {
            if (!isset(self::$action_registry[$group]))
                self::$action_registry[$group] = array();
    
            self::$action_registry[$group][$action::getId()] = $action;
        }
    
    
        static function getPermissions() {
            return self::$perms;
        }
    
    RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions());
    
    
    Peter Rotich's avatar
    Peter Rotich committed
    class ThreadEntryBody /* 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))
    
    Peter Rotich's avatar
    Peter Rotich committed
                throw new Exception("$type: Unsupported ThreadEntryBody 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':
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new ThreadEntryBody(sprintf('<pre>%s</pre>',
    
                    Format::htmlchars($this->body)), $type);
            case 'html:text':
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new ThreadEntryBody(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 prepend($what) {
            $this->body = $what . $this->body;
        }
    
        function append($what) {
            $this->body .= $what;
        }
    
    
        function asVar() {
            // Email template, assume HTML
            return $this->display('email');
        }
    
    
        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':
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new TextThreadEntryBody($text);
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new HtmlThreadEntryBody($text, array('strip-embedded'=>false));
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new ThreadEntryBody($text);
    
    Peter Rotich's avatar
    Peter Rotich committed
    class TextThreadEntryBody extends ThreadEntryBody {
    
        function __construct($body, $options=array()) {
            parent::__construct($body, 'text', $options);
        }
    
        function getClean() {
            return Format::stripEmptyLines($this->body);
        }
    
    
        function prepend($what) {
            $this->body = $what . "\n\n" . $this->body;
        }
    
    
        function display($output=false) {
            if ($this->isEmpty())
                return '(empty)';
    
    
            $escaped = Format::htmlchars($this->body);
    
            switch ($output) {
            case 'html':
    
                return '<div style="white-space:pre-wrap">'
                    .Format::clickableurls($escaped).'</div>';
    
            case 'email':
    
                return '<div style="white-space:pre-wrap">'
    
                    .$escaped.'</div>';
    
                return nl2br($escaped);
    
                return '<pre>'.$escaped.'</pre>';
    
    Peter Rotich's avatar
    Peter Rotich committed
    class HtmlThreadEntryBody extends ThreadEntryBody {
    
        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); # <?php
    
            $body = Format::htmldecode(Format::striptags($body));
            return Format::searchable($body);
    
        function prepend($what) {
            $this->body = sprintf('<div>%s<br/><br/></div>%s', $what, $this->body);
        }
    
    
        function display($output=false) {
            if ($this->isEmpty())
                return '(empty)';
    
            switch ($output) {
    
            case 'email':
                return $this->body;
    
    Peter Rotich's avatar
    Peter Rotich committed
                return Format::clickableurls($this->body);
    
            default:
                return Format::display($this->body);
            }
    
    Peter Rotich's avatar
    Peter Rotich committed
    
    
    /* Message - Ticket thread entry of type message */
    class MessageThreadEntry extends ThreadEntry {
    
        const ENTRY_TYPE = 'M';
    
        function getSubject() {
            return $this->getTitle();
        }
    
    
        static function create($vars, &$errors=array()) {
    
            return static::add($vars, $errors);
    
        static function add($vars, &$errors=array()) {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (!$vars || !is_array($vars) || !$vars['threadId'])
                $errors['err'] = __('Missing or invalid data');
            elseif (!$vars['message'])
                $errors['message'] = __('Message content is required');
    
            if ($errors) return false;
    
            $vars['type'] = self::ENTRY_TYPE;
            $vars['body'] = $vars['message'];
    
            if (!$vars['poster']
                    && $vars['userId']
                    && ($user = User::lookup($vars['userId'])))
                $vars['poster'] = (string) $user->getName();
    
            return parent::add($vars);
        }
    
    
        static function getVarScope() {
            $base = parent::getVarScope();
            unset($base['staff']);
            return $base;
        }
    
    Peter Rotich's avatar
    Peter Rotich committed
    }
    
    /* thread entry of type response */
    class ResponseThreadEntry extends ThreadEntry {
    
        const ENTRY_TYPE = 'R';
    
        function getSubject() {
            return $this->getTitle();
        }
    
        function getRespondent() {
            return $this->getStaff();
        }
    
    
        static function create($vars, &$errors=array()) {
    
            return static::add($vars, $errors);
    
        static function add($vars, &$errors=array()) {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (!$vars || !is_array($vars) || !$vars['threadId'])
                $errors['err'] = __('Missing or invalid data');
            elseif (!$vars['response'])
                $errors['response'] = __('Response content is required');
    
            if ($errors) return false;
    
            $vars['type'] = self::ENTRY_TYPE;
            $vars['body'] = $vars['response'];
            if (!$vars['pid'] && $vars['msgId'])
                $vars['pid'] = $vars['msgId'];
    
            if (!$vars['poster']
                    && $vars['staffId']
                    && ($staff = Staff::lookup($vars['staffId'])))
                $vars['poster'] = (string) $staff->getName();
    
            return parent::add($vars);
        }
    
    
        static function getVarScope() {
            $base = parent::getVarScope();
            unset($base['user']);
            return $base;
        }
    
    Peter Rotich's avatar
    Peter Rotich committed
    }
    
    /* Thread entry of type note (Internal Note) */
    class NoteThreadEntry extends ThreadEntry {
        const ENTRY_TYPE = 'N';
    
        function getMessage() {
            return $this->getBody();
        }
    
        static function create($vars, &$errors) {
    
            return self::add($vars, $errors);
    
        static function add($vars, &$errors=array()) {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            //Check required params.
            if (!$vars || !is_array($vars) || !$vars['threadId'])
                $errors['err'] = __('Missing or invalid data');
            elseif (!$vars['note'])
                $errors['note'] = __('Note content is required');
    
            if ($errors) return false;
    
            //TODO: use array_intersect_key  when we move to php 5 to extract just what we need.
            $vars['type'] = self::ENTRY_TYPE;
            $vars['body'] = $vars['note'];
    
            return parent::add($vars);
        }
    
    
        static function getVarScope() {
            $base = parent::getVarScope();
            unset($base['user']);
            return $base;
        }
    
    // Object specific thread utils.
    
    class ObjectThread extends Thread
    implements TemplateVariable {
    
    Peter Rotich's avatar
    Peter Rotich committed
        private $_entries = array();
    
    
        static $types = array(
            ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
        );
    
    
        function getCounts() {
            if (!isset($this->counts) && $this->getId()) {
                $this->counts = array();
    
                $stuff = $this->entries
                    ->values_flat('type')
                    ->annotate(array(
                        'count' => SqlAggregate::COUNT('id')
                    ));
    
                foreach ($stuff as $row) {
                    list($type, $count) = $row;
                    $this->counts[$type] = $count;
    
            return $this->counts;
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getNumMessages() {
    
            $this->getCounts();
            return $this->counts[MessageThreadEntry::ENTRY_TYPE];
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getNumResponses() {
    
            $this->getCounts();
            return $this->counts[ResponseThreadEntry::ENTRY_TYPE];
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getNumNotes() {
    
            $this->getCounts();
            return $this->counts[NoteThreadEntry::ENTRY_TYPE];
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getMessages() {
    
            return $this->entries->filter(array(
                'type' => MessageThreadEntry::ENTRY_TYPE
            ));
    
        }
    
        function getLastMessage() {
    
            return $this->entries->filter(array(
                'type' => MessageThreadEntry::ENTRY_TYPE
            ))
            ->order_by('-id')
            ->first();
    
        }
    
        function getEntry($var) {
    
            if (is_numeric($var))
                $id = $var;
            else {
                $criteria = array_merge($var, array('limit' => 1));
                $entries = $this->getEntries($criteria);
                if ($entries && $entries[0])
                    $id = $entries[0]['id'];
            }
    
            return $id ? parent::getEntry($id) : null;
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getResponses() {
    
            return $this->entries->filter(array(
                'type' => ResponseThreadEntry::ENTRY_TYPE
            ));
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getNotes() {
    
            return $this->entries->filter(array(
                'type' => NoteThreadEntry::ENTRY_TYPE
            ));
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function addNote($vars, &$errors) {
    
            //Add ticket Id.
            $vars['threadId'] = $this->getId();
            return NoteThreadEntry::create($vars, $errors);
        }
    
        function addMessage($vars, &$errors) {
    
            $vars['threadId'] = $this->getId();
            $vars['staffId'] = 0;
    
            return MessageThreadEntry::create($vars, $errors);
        }
    
        function addResponse($vars, &$errors) {
    
            $vars['threadId'] = $this->getId();
            $vars['userId'] = 0;
    
            return ResponseThreadEntry::create($vars, $errors);
        }
    
        function getVar($name) {
            switch ($name) {
    
            case 'original':
    
                $entry = $this->entries->filter(array(
    
                    'type' => MessageThreadEntry::ENTRY_TYPE,
                    'flags__hasbit' => ThreadEntry::FLAG_ORIGINAL_MESSAGE,
    
                    ))
                    ->order_by('id')
                    ->first();
                if ($entry)
                    return $entry->getBody();
    
    Peter Rotich's avatar
    Peter Rotich committed
                break;
            case 'last_message':
            case 'lastmessage':
    
                $entry = $this->getLastMessage();
                if ($entry)
                    return $entry->getBody();
    
        static function getVarScope() {
          return array(
            'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')),
            'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')),
          );
        }
    
    
        static function lookup($criteria, $type=false) {
    
            if (!$type)
                return parent::lookup($criteria);
    
    
            $class = false;
    
            if (isset(self::$types[$type]))
    
                $class = self::$types[$type];
            if (!class_exists($class))
                $class = get_called_class();
    
            return $class::lookup($criteria);
    
        }
    }
    
    // Ticket thread class
    class TicketThread extends ObjectThread {
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        static function create($ticket) {
            $id = is_object($ticket) ? $ticket->getId() : $ticket;
    
            $thread = parent::create(array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                        'object_id' => $id,
    
                        'object_type' => ObjectModel::OBJECT_TYPE_TICKET
                        ));
    
            if ($thread->save())
                return $thread;
    
    
    /**
     * Class: ThreadEntryAction
     *
     * Defines a simple action to be performed on a thread entry item, such as
     * viewing the raw email headers used to generate the message, resend the
     * confirmation emails, etc.
     */
    abstract class ThreadEntryAction {
        static $name;               // Friendly, translatable name
        static $id;                 // Unique identifier used for plumbing
        static $icon = 'cog';
    
    
        var $entry;
    
    
        function getName() {
            $class = get_class($this);
            return __($class::$name);
        }
    
        static function getId() {
            return static::$id;
        }
    
        function getIcon() {
            $class = get_class($this);
            return 'icon-' . $class::$icon;
        }
    
        function __construct(ThreadEntry $thread) {
    
            $this->entry = $thread;
    
        }
    
        abstract function trigger();
    
        function getTicket() {
    
            return $this->entry->getObject();
    
        }
    
        function isEnabled() {
            return $this->isVisible();
        }
        function isVisible() {
            return true;
        }
    
        /**
         * getJsStub
         *
         * Retrieves a small JavaScript snippet to insert into the rendered page
         * which should, via an AJAX callback, trigger this action to be
         * performed. The URL for this sort of activity is already provided for
         * you via the ::getAjaxUrl() method in this class.
         */
        abstract function getJsStub();
    
    
        /**
         * getAjaxUrl
         *
         * Generate a URL to be used as an AJAX callback. The URL can be used to
         * trigger this thread entry action via the callback.
         *
         * Parameters:
         * $dialog - (bool) used in conjunction with `$.dialog()` javascript
         *      function which assumes the `ajax.php/` should be replace a leading
         *      `#` in the url
         */
    
        function getAjaxUrl($dialog=false) {
            return sprintf('%stickets/%d/thread/%d/%s',
                $dialog ? '#' : 'ajax.php/',
    
                $this->entry->getThread()->getObjectId(),
                $this->entry->getId(),
    
    
    interface Threadable {
    
        function getThreadId();
        function getThread();
    
        function postThreadEntry($type, $vars, $options=array());