Skip to content
Snippets Groups Projects
class.thread.php 96.8 KiB
Newer Older
  • Learn to ignore specific revisions
  •     function getEditor() {
            static $types = array(
                'U' => 'User',
                'S' => 'Staff',
            );
            if (!isset($types[$this->editor_type]))
                return null;
    
            return $types[$this->editor_type]::lookup($this->editor);
        }
    
        function getName() {
            if ($this->staff_id)
                return $this->staff->getName();
            if ($this->user_id)
                return $this->user->getName();
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getEmailHeader() {
    
            if ($this->email_info)
                return $this->email_info->headers;
    
        function isAutoReply() {
    
            if (!isset($this->is_autoreply))
    
                $this->is_autoreply = $this->getEmailHeaderArray()
                    ?  TicketFilter::isAutoReply($this->getEmailHeaderArray()) : false;
    
            return $this->is_autoreply;
    
            if (!isset($this->is_bounce))
    
                $this->is_bounce = $this->getEmailHeaderArray()
                    ? TicketFilter::isBounce($this->getEmailHeaderArray()) : false;
    
            return $this->is_bounce;
    
        function isBounceOrAutoReply() {
            return ($this->isAutoReply() || $this->isBounce());
    
        function hasFlag($flag) {
            return ($this->get('flags', 0) & $flag) != 0;
        }
        function clearFlag($flag) {
            return $this->set('flags', $this->get('flags') & ~$flag);
        }
        function setFlag($flag) {
            return $this->set('flags', $this->get('flags') | $flag);
        }
    
    
        function isSystem() {
            return $this->hasFlag(self::FLAG_SYSTEM);
        }
    
    
        protected function normalizeFileInfo($files, $add_error=true) {
            static $error_descriptions = array(
                UPLOAD_ERR_INI_SIZE     => /* @trans */ 'File is too large',
                UPLOAD_ERR_FORM_SIZE    => /* @trans */ 'File is too large',
                UPLOAD_ERR_PARTIAL      => 'The uploaded file was only partially uploaded.',
                UPLOAD_ERR_NO_TMP_DIR   => 'Missing a temporary folder.',
                UPLOAD_ERR_CANT_WRITE   => 'Failed to write file to disk.',
                UPLOAD_ERR_EXTENSION    => 'A PHP extension stopped the file upload.',
            );
    
            if (!is_array($files))
                $files = array($files);
    
            $ids = array();
            foreach ($files as $name => $file) {
                $F = array('inline' => is_array($file) && @$file['inline']);
    
                if (is_numeric($file))
                    $fileId = $file;
                elseif ($file instanceof AttachmentFile)
                    $fileId = $file->getId();
                elseif (is_array($file) && isset($file['id']))
                    $fileId = $file['id'];
                elseif ($AF = AttachmentFile::create($file))
                    $fileId = $AF->getId();
                elseif ($add_error) {
                    $error = $file['error']
                        ?: sprintf(_S('Unable to import attachment - %s'),
                            $name ?: $file['name']);
                    if (is_numeric($error) && isset($error_descriptions[$error])) {
                        $error = sprintf(_S('Error #%1$d: %2$s'), $error,
                            _S($error_descriptions[$error]));
                    }
                    // No need to log the missing-file error number
    
                    if ($error != UPLOAD_ERR_NO_FILE
                        && ($thread = $this->getThread())
                    ) {
                        // Log to the thread directly, since alerts should be
                        // suppressed and this is defintely a system message
                        $thread->addNote(array(
                            'title' => _S('File Import Error'),
                            'note' => new TextThreadEntryBody($error),
                            'poster' => 'SYSTEM',
                            'staffId' => 0,
                        ));
                    }
    
                if (is_string($name))
                    $F['name'] = $name;
                if (isset($AF))
                    $F['file'] = $AF;
    
                // Add things like the `key` field, but don't change current
                // keys of the file array
                if (is_array($file))
                    $F += $file;
    
                // Key is required for CID rewriting in the body
                if (!isset($F['key']) && ($AF = AttachmentFile::lookup($F['id'])))
                    $F['key'] = $AF->key;
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
       /*
        Save attachment to the DB.
        @file is a mixed var - can be ID or file hashtable.
        */
    
        function createAttachment($file, $name=false) {
    
            $att = new Attachment(array(
    
                'type' => 'H',
                'object_id' => $this->getId(),
    
                'file_id' => $file['id'],
                'inline' => $file['inline'] ? 1 : 0,
    
            // Record varying file names in the attachment record
            if (is_array($file) && isset($file['name'])) {
                $filename = $file['name'];
            }
            elseif (is_string($name)) {
                $filename = $name;
            }
            if ($filename) {
                // This should be a noop since the ORM caches on PK
    
                $F = @$file['file'] ?: AttachmentFile::lookup($file['id']);
    
                // XXX: This is not Unicode safe
                if ($F && 0 !== strcasecmp($F->name, $filename))
    
            if (!$att->save())
                return false;
            return $att;
    
        function createAttachments(array $files) {
    
            $attachments = array();
    
            foreach ($files as $info) {
               if ($A = $this->createAttachment($info, @$info['name'] ?: false))
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getAttachments() {
            return $this->attachments;
        }
    
    
        function getAttachmentUrls() {
    
            foreach ($this->attachments as $att) {
                $json[$att->file->getKey()] = array(
                    'download_url' => $att->file->getDownloadUrl(),
    
                    'filename' => $att->getFilename(),
    
        function getAttachmentsLinks($file='attachment.php', $target='_blank', $separator=' ') {
    
            // TODO: Move this to the respective UI templates
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            $str='';
    
            foreach ($this->attachments as $att ) {
                if ($att->inline) continue;
    
    Peter Rotich's avatar
    Peter Rotich committed
                $size = '';
    
                if ($att->file->size)
                    $size=sprintf('<em>(%s)</em>', Format::file_size($att->file->size));
    
                $str .= sprintf(
                    '<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
                    $att->file->getDownloadUrl(), $target,
                    Format::htmlchars($att->file->name), $size, $separator);
    
    Peter Rotich's avatar
    Peter Rotich committed
            }
    
            return $str;
        }
    
        /* save email info
         * TODO: Refactor it to include outgoing emails on responses.
         */
    
        function saveEmailInfo($vars) {
    
    
            // Don't save empty message ID
            if (!$vars || !$vars['mid'])
    
    Peter Rotich's avatar
    Peter Rotich committed
                return 0;
    
    
            $this->ht['email_mid'] = $vars['mid'];
    
            $header = false;
            if (isset($vars['header']))
                $header = $vars['header'];
            self::logEmailHeaders($this->getId(), $vars['mid'], $header);
    
        /* static */
        function logEmailHeaders($id, $mid, $header=false) {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (!$id || !$mid)
                return false;
    
    
            $this->email_info = new ThreadEntryEmailInfo(array(
    
                $this->email_info->headers = trim($header);
    
            return $this->email_info->save();
    
        function getActivity() {
            return new ThreadActivity('', '');
    
        function __toString() {
    
            return (string) $this->getBody();
    
        // TemplateVariable interface
    
            return (string) $this->getBody()->display('email');
    
        function getVar($tag) {
            switch(strtolower($tag)) {
                case 'create_date':
    
                    return new FormattedDate($this->getCreateDate());
    
                    return new FormattedDate($this->getUpdateDate());
                case 'files':
                    throw new OOBContent(OOBContent::FILES, $this->attachments->all());
    
        static function getVarScope() {
            return array(
    
              '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 ($mailinfo['mid'] &&
                    ($entry = ThreadEntry::objects()
                     ->filter(array('email_info__mid' => $mailinfo['mid']))
                     ->order_by(false)
                     ->first()
                     )
             ) {
    
                $seen = true;
    
                if ($mailinfo['system_emails']
                        && ($t = $entry->getThread()->getObject())
                        && $t instanceof Ticket)
                    $t->systemReferral($mailinfo['system_emails']);
    
    
            $possibles = array();
    
            foreach (array('mid', '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],
    
                // 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[1]));
    
            }
    
            // Add the message id if it is embedded in the body
            $match = array();
    
            if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
                    (string) $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 || !$mid_info['loopback'])
                    continue;
                if (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'];
    
    
                        $user = User::lookupByEmail($mailinfo['email']);
                        if ($user && $mailinfo['userId'] != $user->getId())
                          $mailinfo['userId'] = $user->getId();
    
                    elseif (@$mid_info['staffId']) {
                        $mailinfo['staffId'] = $mid_info['staffId'];
    
    
                        $staffId = Staff::getIdByEmail($mailinfo['email']);
                        if ($staffId && $mailinfo['staffId'] != $staffId)
                          $mailinfo['staffId'] = $staffId;
    
    Peter Rotich's avatar
    Peter Rotich committed
    
                    // Capture the user type
                    if (@$mid_info['userClass'])
                        $mailinfo['userClass'] = $mid_info['userClass'];
    
    
    
                    // ThreadEntry was positively identified
                    return $t;
                }
    
    Peter Rotich's avatar
    Peter Rotich committed
            }
            // Passive threading - listen mode
    
            if (count($possibles)
                    && ($entry = ThreadEntry::objects()
                        ->filter(array('email_info__mid__in' => array_map(
                            function ($a) { return "<$a>"; },
                        $possibles)))
                        ->first()
                    )
             ) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                $mailinfo['passive'] = true;
                return $entry;
    
            }
    
            // 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();
                }
            }
    
        /**
         * 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)
    
            // 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)
            );
    
    Peter Rotich's avatar
    Peter Rotich committed
        //new entry ... we're trusting the caller to check validity of the data.
    
        static function create($vars=false) {
    
            assert(is_array($vars));
    
    
    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) {
    
                if ($cfg->isRichTextEnabled())
    
    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']);
    
            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 = new static(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'],
    
                'flags' => $vars['flags'] ?: 0,
    
            //add recipients to thread entry
    
            if ($vars['thread_entry_recipients']) {
                $count = 0;
                foreach ($vars['thread_entry_recipients'] as $key => $value)
                    $count = $count + count($value);
    
                if ($count > 1)
                    $entry->flags |= ThreadEntry::FLAG_REPLY_ALL;
                else
                    $entry->flags |= ThreadEntry::FLAG_REPLY_USER;
    
    
                $entry->recipients = json_encode($vars['thread_entry_recipients']);
    
    
            if (Collaborator::getIdByUserId($vars['userId'], $vars['threadId']))
              $entry->flags |= ThreadEntry::FLAG_COLLABORATOR;
    
    
            if ($entry->format == 'html')
                // The current codebase properly balances html
                $entry->flags |= self::FLAG_BALANCED;
    
    
            // Flag system messages
            if (!($vars['staffId'] || $vars['userId']))
                $entry->flags |= self::FLAG_SYSTEM;
    
    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
    
            /************* ATTACHMENTS *****************/
    
            // Drop stripped email inline 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 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;
                }
            }
    
            $attached_files = array();
            foreach (array(
                // Web uploads and canned attachments
                $vars['files'], $vars['cannedattachments'],
                // Emailed or API attachments
                $vars['attachments'],
                // Inline images (attached to the draft)
                Draft::getAttachmentIds($body),
            ) as $files
            ) {
                if (is_array($files)) {
                    // Detect *inline* email attachments
                    foreach ($files as $i=>$a) {
                        if (isset($a['cid']) && $a['cid']
                                && strpos($body, 'cid:'.$a['cid']) !== false)
                            $files[$i]['inline'] = true;
                    }
                    foreach ($entry->normalizeFileInfo($files) as $F) {
    
                        // Deduplicate on the `key` attribute. The key is
                        // necessary for the CID rewrite below
    
                        $attached_files[$F['key']] = $F;
    
            // 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 (key) will be available to
            // retrieve the image later
            foreach ($attached_files as $key => $a) {
                if (isset($a['cid']) && $a['cid']) {
                    $body = preg_replace('/src=("|\'|\b)(?:cid:)?'
                        . preg_quote($a['cid'], '/').'\1/i',
                        'src="cid:'.$key.'"', $body);
                }
    
            // Set body here after it was rewritten to capture the stored file
            // keys (above)
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            // Store body as an attachment if bigger than allowed packet size
            if (mb_strlen($body) >= 65000) { // 65,535 chars in text field.
                 $entry->body = NULL;
                 $file = array(
                         'type' => 'text/html',
                         'name' => md5($body).'.txt',
                         'data' => $body,
                         );
    
                 if (($AF = AttachmentFile::create($file))) {
                     $attached_files[$file['key']] = array(
                             'id' => $AF->getId(),
                             'inline' => true,
                             'file' => $AF);
                 } else {
                     $entry->body = $body;
                 }
            } else {
                $entry->body = $body;
    
            }
    
            if (!$entry->save(true))
    
                return false;
    
            // Associate the attached files with this new entry
            $entry->createAttachments($attached_files);
    
    
            // Save mail message id, if available
    
            $entry->saveEmailInfo($vars);
    
    
            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;
    
    aydreeihn's avatar
    aydreeihn committed
    
        static function getTypes() {
            return self::$types;
        }
    
    RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions());
    
    
    Peter Rotich's avatar
    Peter Rotich committed
    
    class ThreadReferral extends VerySimpleModel {
        static $meta = array(
            'table' => THREAD_REFERRAL_TABLE,
            'pk' => array('id'),
            'joins' => array(
                'thread' => array(
                    'constraint' => array('thread_id' => 'Thread.id'),
                ),
                'agent' => array(
                    'constraint' => array(
                        'object_type' => "'S'",
                        'object_id' => 'Staff.staff_id',
                    ),
                ),
                'team' => array(
                    'constraint' => array(
                        'object_type' => "'E'",
                        'object_id' => 'Team.team_id',
                    ),
                ),
                'dept' => array(
                    'constraint' => array(
                        'object_type' => "'D'",
                        'object_id' => 'Dept.id',
                    ),
                ),
              )
            );
    
        var $icons = array(
                'E' => 'group',
                'D' => 'sitemap',
                'S' => 'user'
                );
    
        var $_object = null;
    
        function getId() {
            return $this->id;
        }
    
        function getName() {
            return (string) $this->getObject();
        }
    
        function getObject() {
    
            if (!isset($this->_object)) {
                $this->_object = ObjectModel::lookup(
                        $this->object_id, $this->object_type);
            }
    
            return $this->_object;
        }
    
        function getIcon() {
            return $this->icons[$this->object_type];
        }
    
        function display() {
            return sprintf('<i class="icon-%s"></i> %s',
                    $this->getIcon(), $this->getName());
        }
    
        static function create($vars) {
    
            $new = new self($vars);
            $new->created = SqlFunction::NOW();
            return $new->save();
        }
    }
    
    
    class ThreadEvent extends VerySimpleModel {
        static $meta = array(
    
            'table' => THREAD_EVENT_TABLE,
    
            'pk' => array('id'),
            'joins' => array(
    
                // Originator of activity
                'agent' => array(
                    'constraint' => array(
                        'uid' => 'Staff.staff_id',
                    ),
                    'null' => true,
    
                // Agent assignee
    
                'staff' => array(
                    'constraint' => array(
    
                        'staff_id' => 'Staff.staff_id',
                    ),
                    'null' => true,
                ),
                'team' => array(
                    'constraint' => array(
                        'team_id' => 'Team.team_id',
    
                    ),
                    'null' => true,
                ),
    
                'thread' => array(
                    'constraint' => array('thread_id' => 'Thread.id'),
                ),
    
                'user' => array(
                    'constraint' => array(
                        'uid' => 'User.id',
                    ),
                    'null' => true,
                ),
                'dept' => array(
                    'constraint' => array(
                        'dept_id' => 'Dept.id',
                    ),
                    'null' => true,
                ),
    
                'topic' => array(
                    'constraint' => array(
                        'topic_id' => 'Topic.topic_id',
                    ),
                    'null' => true,
                ),
    
        // Valid events for database storage
        const ASSIGNED  = 'assigned';
    
        const RELEASED  = 'released';
    
        const CLOSED    = 'closed';
        const CREATED   = 'created';
        const COLLAB    = 'collab';
        const EDITED    = 'edited';
        const ERROR     = 'error';
        const OVERDUE   = 'overdue';
        const REOPENED  = 'reopened';
        const STATUS    = 'status';
    
        const TRANSFERRED = 'transferred';
    
    Peter Rotich's avatar
    Peter Rotich committed
        const REFERRED = 'referred';
    
        const VIEWED    = 'viewed';
    
        const MODE_STAFF = 1;
        const MODE_CLIENT = 2;
    
        var $_data;
    
    
        function getAvatar($size=null) {
    
            if ($this->uid && $this->uid_type == 'S')
    
                return $this->agent ? $this->agent->getAvatar($size) : '';
    
            if ($this->uid && $this->uid_type == 'U')
    
                return $this->user ? $this->user->getAvatar($size) : '';
    
        function getUserName() {
            if ($this->uid && $this->uid_type == 'S')
    
                return $this->agent ? $this->agent->getName() : $this->username;
    
            if ($this->uid && $this->uid_type == 'U')
    
                return $this->user ? $this->user->getName() : $this->username;
    
            return $this->username;
        }
    
        function getIcon() {
            $icons = array(
    
                'assigned'  => 'hand-right',
    
                'released'  => 'unlock',
    
                'collab'    => 'group',
                'created'   => 'magic',
                'overdue'   => 'time',
    
                'transferred' => 'share-alt',
    
    Peter Rotich's avatar
    Peter Rotich committed
                'referred' => 'exchange',
    
                'edited'    => 'pencil',
    
                'closed'    => 'thumbs-up-alt',
                'reopened'  => 'rotate-right',
    
                'resent'    => 'reply-all icon-flip-horizontal',
    
            );
            return @$icons[$this->state] ?: 'chevron-sign-right';
        }
    
        function getDescription($mode=self::MODE_STAFF) {
    
            // Abstract description
            return $this->template(sprintf(
                __('%s by {somebody} {timestamp}'),
                $this->state
    
            ), $mode);
    
        function template($description, $mode=self::MODE_STAFF) {
    
            global $thisstaff, $cfg;
    
            $hideName = $cfg->hideStaffName();
    
            return preg_replace_callback('/\{(<(?P<type>([^>]+))>)?(?P<key>[^}.]+)(\.(?P<data>[^}]+))?\}/',
    
                function ($m) use ($self, $thisstaff, $cfg, $hideName, $mode) {
    
                    switch ($m['key']) {
    
                    case 'assignees':
                        $assignees = array();
    
                        if ($S = $self->staff) {
    
                            $avatar = '';
                            if ($cfg->isAvatarsEnabled())
                                $avatar = $S->getAvatar();
    
                        if ($T = $self->team) {
    
                            $assignees[] = $T->getLocalName();
                        }
                        return implode('/', $assignees);
    
                        if ($hideName && $self->agent && $mode == self::MODE_CLIENT)
                            $name = __('Staff');
                        else
                            $name = $self->getUserName();
    
                        if ($cfg->isAvatarsEnabled()
                                && ($avatar = $self->getAvatar()))
    
                    case 'timestamp':
    
                        $timeFormat = null;
                        if ($thisstaff
                                && !strcasecmp($thisstaff->datetime_format,
                                    'relative')) {
                            $timeFormat = function ($timestamp) {
                                return Format::relativeTime(Misc::db2gmtime($timestamp));
                            };
                        }
    
                        return sprintf('<time %s datetime="%s"
                                data-toggle="tooltip" title="%s">%s</time>',
                            $timeFormat ? 'class="relative"' : '',
    
                            date(DateTime::W3C, Misc::db2gmtime($self->timestamp)),
                            Format::daydatetime($self->timestamp),
    
                            $timeFormat ? $timeFormat($self->timestamp) :
                            Format::datetime($self->timestamp)
    
                        $name = $self->agent->getName();
    
                        if ($cfg->isAvatarsEnabled()
                                && ($avatar = $self->getAvatar()))
    
                        return $name;
    
                    case 'dept':
    
                        if ($dept = $self->getDept())
    
                            return $dept->getLocalName();
    
                        return __('None');
    
                    case 'data':
                        $val = $self->getData($m['data']);
    
                        if (is_array($val))
                            list($val, $fallback) = $val;
    
                        if ($m['type'] && class_exists($m['type']))
                            $val = $m['type']::lookup($val);
    
                        if (!$val && $fallback)
                            $val = $fallback;
                        return Format::htmlchars((string) $val);
    
                    }
                    return $m[0];
                },
                $description
            );
    
        function getDept() {
            return $this->dept;
    
        function getData($key=false) {
            if (!isset($this->_data))
                $this->_data = JsonDataParser::decode($this->data);
            return ($key) ? @$this->_data[$key] : $this->_data;
    
        function render($mode) {
    
            $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR;
    
            $event = $this->getTypedEvent();
    
            include $inc . 'templates/thread-event.tmpl.php';
    
        static function create($ht=false, $user=false) {
    
            $inst = new static($ht);
    
            $inst->timestamp = SqlFunction::NOW();
    
            global $thisstaff, $thisclient;
    
            $user = is_object($user) ? $user : $thisstaff ?: $thisclient;
            if ($user instanceof Staff) {
    
                $inst->uid_type = 'S';
    
                $inst->uid = $user->getId();