Skip to content
Snippets Groups Projects
class.thread.php 96.8 KiB
Newer Older
  • Learn to ignore specific revisions
  •         elseif ($user instanceof User) {
    
                $inst->uid_type = 'U';
    
                $inst->uid = $user->getId();
    
            return $inst;
    
        static function forTicket($ticket, $state, $user=false) {
    
          global $thisstaff;
    
          if($thisstaff && !$ticket->getStaffId())
            $staff = $thisstaff->getId();
          else
            $staff = $ticket->getStaffId();
    
    aydreeihn's avatar
    aydreeihn committed
    
    
            $inst = self::create(array(
    
                'staff_id' => $staff,
    
                'team_id' => $ticket->getTeamId(),
                'dept_id' => $ticket->getDeptId(),
                'topic_id' => $ticket->getTopicId(),
    
            ), $user);
    
            return $inst;
    
        static function forTask($task, $state, $user=false) {
            $inst = self::create(array(
                'staff_id' => $task->getStaffId(),
                'team_id' => $task->getTeamId(),
                'dept_id' => $task->getDeptId(),
            ), $user);
            return $inst;
        }
    
    
        function getTypedEvent() {
            static $subclasses;
    
            if (!isset($subclasses)) {
                $parent = get_class($this);
                $subclasses = array();
                foreach (get_declared_classes() as $class) {
                    if (is_subclass_of($class, $parent))
                        $subclasses[$class::$state] = $class;
                }
            }
            if (!($class = $subclasses[$this->state]))
                return $this;
            return new $class($this->ht);
    
    class ThreadEvents extends InstrumentedList {
        function annul($event) {
            $this->queryset
                ->filter(array('state' => $event))
                ->update(array('annulled' => 1));
    
        /**
         * Add an event to the thread activity log.
         *
         * Parameters:
         * $object - Object to log activity for
         * $state - State name of the activity (one of 'created', 'edited',
         *      'deleted', 'closed', 'reopened', 'error', 'collab', 'resent',
    
         *      'assigned', 'released', 'transferred')
    
         * $data - (array?) Details about the state change
         * $user - (string|User|Staff) user triggering the state change
         * $annul - (state) a corresponding state change that is annulled by
         *      this event
         */
        function log($object, $state, $data=null, $user=null, $annul=null) {
    
            global $thisstaff, $thisclient;
    
    
    aydreeihn's avatar
    aydreeihn committed
            if ($object && ($object instanceof Ticket))
    
                // TODO: Use $object->createEvent() (nolint)
    
                $event = ThreadEvent::forTicket($object, $state, $user);
    
    aydreeihn's avatar
    aydreeihn committed
            elseif ($object && ($object instanceof Task))
    
                $event = ThreadEvent::forTask($object, $state, $user);
    
    aydreeihn's avatar
    aydreeihn committed
            if (is_null($event))
                return;
    
    
            # Annul previous entries if requested (for instance, reopening a
            # ticket will annul an 'closed' entry). This will be useful to
            # easily prevent repeated statistics.
            if ($annul) {
                $this->annul($annul);
            }
    
            $username = $user;
            $user = is_object($user) ? $user : $thisclient ?: $thisstaff;
            if (!is_string($username)) {
                if ($user instanceof Staff) {
                    $username = $user->getUserName();
    
                // XXX: Use $user here
                elseif ($thisclient) {
    
    JediKev's avatar
    JediKev committed
                    if ($thisclient->hasAccount())
                        $username = $thisclient->getFullName();
    
                    if (!$username)
                        $username = $thisclient->getEmail();
                }
                else {
                    # XXX: Security Violation ?
                    $username = 'SYSTEM';
                }
            }
            $event->username = $username;
            $event->state = $state;
    
            if ($data) {
                if (is_array($data))
                    $data = JsonDataEncoder::encode($data);
                if (!is_string($data))
                    throw new InvalidArgumentException('Data must be string or array');
                $event->data = $data;
            }
    
            $this->add($event);
    
            // Save event immediately
            return $event->save();
        }
    }
    
    class AssignmentEvent extends ThreadEvent {
        static $icon = 'hand-right';
        static $state = 'assigned';
    
        function getDescription($mode=self::MODE_STAFF) {
            $data = $this->getData();
            switch (true) {
            case !is_array($data):
            default:
                $desc = __('Assignee changed by <b>{somebody}</b> to <strong>{assignees}</strong> {timestamp}');
                break;
            case isset($data['staff']):
                $desc = __('<b>{somebody}</b> assigned this to <strong>{<Staff>data.staff}</strong> {timestamp}');
                break;
            case isset($data['team']):
                $desc = __('<b>{somebody}</b> assigned this to <strong>{<Team>data.team}</strong> {timestamp}');
                break;
            case isset($data['claim']):
                $desc = __('<b>{somebody}</b> claimed this {timestamp}');
                break;
            }
            return $this->template($desc);
    
    class ReleaseEvent extends ThreadEvent {
        static $icon = 'unlock';
        static $state = 'released';
    
        function getDescription($mode=self::MODE_STAFF) {
            $data = $this->getData();
            switch (true) {
            case isset($data['staff'], $data['team']):
                $desc = __('Ticket released from <strong>{<Team>data.team}</strong> and <strong>{<Staff>data.staff}</strong> by <b>{somebody}</b> {timestamp}');
                break;
            case isset($data['staff']):
                $desc = __('Ticket released from <strong>{<Staff>data.staff}</strong> by <b>{somebody}</b> {timestamp}');
                break;
            case isset($data['team']):
                $desc = __('Ticket released from <strong>{<Team>data.team}</strong> by <b>{somebody}</b> {timestamp}');
                break;
            default:
                $desc = __('<b>{somebody}</b> released ticket assignment {timestamp}');
                break;
            }
            return $this->template($desc);
        }
    }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
    class ReferralEvent extends ThreadEvent {
        static $icon = 'exchange';
        static $state = 'referred';
    
        function getDescription($mode=self::MODE_STAFF) {
            $data = $this->getData();
            switch (true) {
            case isset($data['staff']):
                $desc = __('<b>{somebody}</b> referred this to <strong>{<Staff>data.staff}</strong> {timestamp}');
                break;
            case isset($data['team']):
                $desc = __('<b>{somebody}</b> referred this to <strong>{<Team>data.team}</strong> {timestamp}');
                break;
            case isset($data['dept']):
                $desc = __('<b>{somebody}</b> referred this to <strong>{<Dept>data.dept}</strong> {timestamp}');
                break;
            }
            return $this->template($desc);
        }
    }
    
    
    class CloseEvent extends ThreadEvent {
        static $icon = 'thumbs-up-alt';
        static $state = 'closed';
    
        function getDescription($mode=self::MODE_STAFF) {
    
            if ($this->getData('status'))
    
                return $this->template(__('Closed by <b>{somebody}</b> with status of {<TicketStatus>data.status} {timestamp}'), $mode);
    
                return $this->template(__('Closed by <b>{somebody}</b> {timestamp}'), $mode);
    
    class CollaboratorEvent extends ThreadEvent {
        static $icon = 'group';
        static $state = 'collab';
    
        function getDescription($mode=self::MODE_STAFF) {
            $data = $this->getData();
            switch (true) {
            case isset($data['org']):
                $desc = __('Collaborators for {<Organization>data.org} organization added');
                break;
            case isset($data['del']):
                $base = __('<b>{somebody}</b> removed <strong>%s</strong> from the collaborators {timestamp}');
                $collabs = array();
                $users = User::objects()->filter(array('id__in' => array_keys($data['del'])));
                foreach ($data['del'] as $id=>$c) {
                    $U = false;
                    foreach ($users as $user) {
                        if ($user->id == $id) {
                            $U = $user;
                            break;
                        }
                    }
                    $collabs[] = Format::htmlchars($U ? $U->getName() : @$c['name'] ?: $c);
                }
                $desc = sprintf($base, implode(', ', $collabs));
                break;
    
            case isset($data['add']):
    
                $base = __('<b>{somebody}</b> added <strong>%s</strong> as collaborators {timestamp}');
                $collabs = array();
                if ($data['add']) {
                    $users = User::objects()->filter(array('id__in' => array_keys($data['add'])));
                    foreach ($data['add'] as $id=>$c) {
                        $U = false;
                        foreach ($users as $user) {
                            if ($user->id == $id) {
                                $U = $user;
                                break;
                            }
                        }
    
                        $c = sprintf("%s %s",
    
                            Format::htmlchars($U ? $U->getName() : @$c['name'] ?: $c),
    
                            $c['src'] ? sprintf(__('via %s'
                                /* e.g. "Added collab "Me <me@company.me>" via Email (to)" */
                                ), $c['src']) : ''
    
                        );
                        $collabs[] = $c;
                    }
                }
                $desc = $collabs
                    ? sprintf($base, implode(', ', $collabs))
                    : 'somebody';
                break;
            }
    
            return $this->template($desc, $mode);
    
        }
    }
    
    class CreationEvent extends ThreadEvent {
        static $icon = 'magic';
        static $state = 'created';
    
        function getDescription($mode=self::MODE_STAFF) {
    
            return $this->template(__('Created by <b>{somebody}</b> {timestamp}'), $mode);
    
        }
    }
    
    class EditEvent extends ThreadEvent {
        static $icon = 'pencil';
        static $state = 'edited';
    
        function getDescription($mode=self::MODE_STAFF) {
            $data = $this->getData();
            switch (true) {
            case isset($data['owner']):
                $desc = __('<b>{somebody}</b> changed ownership to {<User>data.owner} {timestamp}');
                break;
            case isset($data['status']):
                $desc = __('<b>{somebody}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}');
                break;
            case isset($data['fields']):
                $fields = $changes = array();
                foreach (DynamicFormField::objects()->filter(array(
                    'id__in' => array_keys($data['fields'])
                )) as $F) {
                    $fields[$F->id] = $F;
                }
                foreach ($data['fields'] as $id=>$f) {
    
                    if (!($field = $fields[$id]))
                       continue;
                    if ($mode == self::MODE_CLIENT &&  !$field->isVisibleToUsers())
    
                        continue;
                    list($old, $new) = $f;
                    $impl = $field->getImpl($field);
                    $before = $impl->to_php($old);
                    $after = $impl->to_php($new);
                    $changes[] = sprintf('<strong>%s</strong> %s',
                        $field->getLocal('label'), $impl->whatChanged($before, $after));
                }
    
                // Fallthrough to other editable fields
            case isset($data['topic_id']):
            case isset($data['sla_id']):
            case isset($data['source']):
            case isset($data['user_id']):
            case isset($data['duedate']):
                $base = __('Updated by <b>{somebody}</b> {timestamp} — %s');
                foreach (array(
                    'topic_id' => array(__('Help Topic'), array('Topic', 'getTopicName')),
                    'sla_id' => array(__('SLA'), array('SLA', 'getSLAName')),
    
                    'duedate' => array(__('Due Date'), array('Format', 'date')),
    
                    'user_id' => array(__('Ticket Owner'), array('User', 'getNameById')),
                    'source' => array(__('Source'), null)
                ) as $f => $info) {
                    if (isset($data[$f])) {
                        list($name, $desc) = $info;
                        list($old, $new) = $data[$f];
                        if ($desc && is_callable($desc)) {
                            $new = call_user_func($desc, $new);
                            if ($old)
                                $old = call_user_func($desc, $old);
                        }
                        if ($old and $new) {
                            $changes[] = sprintf(
                                __('<strong>%1$s</strong> changed from <strong>%2$s</strong> to <strong>%3$s</strong>'),
                                Format::htmlchars($name), Format::htmlchars($old), Format::htmlchars($new)
                            );
                        }
                        elseif ($new) {
                            $changes[] = sprintf(
                                __('<strong>%1$s</strong> set to <strong>%2$s</strong>'),
                                Format::htmlchars($name), Format::htmlchars($new)
                            );
                        }
                        else {
                            $changes[] = sprintf(
                                __('unset <strong>%1$s</strong>'),
                                Format::htmlchars($name)
                            );
                        }
                    }
                }
    
                $desc = $changes
                    ? sprintf($base, implode(', ', $changes)) : '';
                break;
            }
    
    
            return $this->template($desc, $mode);
    
        }
    }
    
    class OverdueEvent extends ThreadEvent {
        static $icon = 'time';
        static $state = 'overdue';
    
        function getDescription($mode=self::MODE_STAFF) {
            return $this->template(__('Flagged as overdue by the system {timestamp}'));
        }
    }
    
    class ReopenEvent extends ThreadEvent {
        static $icon = 'rotate-right';
        static $state = 'reopened';
    
        function getDescription($mode=self::MODE_STAFF) {
    
            return $this->template(__('Reopened by <b>{somebody}</b> {timestamp}'), $mode);
    
        }
    }
    
    class ResendEvent extends ThreadEvent {
        static $icon = 'reply-all icon-flip-horizontal';
        static $state = 'resent';
    
        function getDescription($mode=self::MODE_STAFF) {
            return $this->template(__('<b>{somebody}</b> resent <strong><a href="#thread-entry-{data.entry}">a previous response</a></strong> {timestamp}'));
        }
    }
    
    class TransferEvent extends ThreadEvent {
        static $icon = 'share-alt';
        static $state = 'transferred';
    
        function getDescription($mode=self::MODE_STAFF) {
            return $this->template(__('<b>{somebody}</b> transferred this to <strong>{dept}</strong> {timestamp}'));
        }
    }
    
    class ViewEvent extends ThreadEvent {
        static $state = 'viewed';
    }
    
    
    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() {
    
    Peter Rotich's avatar
    Peter Rotich committed
            switch ($this->type) {
            case 'html':
                return trim($this->body, " <>br/\t\n\r") ? $this->body: '';
            case 'text':
                return trim($this->body) ? $this->body: '';
            default:
                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, $options=array()) {
    
            switch ($format) {
            case 'text':
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new TextThreadEntryBody($text);
    
                return new HtmlThreadEntryBody($text, array('strip-embedded'=>false) + $options);
    
    Peter Rotich's avatar
    Peter Rotich committed
                return new ThreadEntryBody($text);
    
    Peter Rotich's avatar
    Peter Rotich committed
    
        static function clean($text, $format=null) {
            global $cfg;
    
    Peter Rotich's avatar
    Peter Rotich committed
            $format = $format ?: ($cfg->isRichTextEnabled() ? 'html' : 'text');
    
    Peter Rotich's avatar
    Peter Rotich committed
            $body = static::fromFormattedText($text, $format);
            return $body->getClean();
        }
    
    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::htmlchars(Format::html_balance(Format::stripEmptyLines(parent::getClean())));
    
        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() {
    
    Peter Rotich's avatar
    Peter Rotich committed
            return Format::sanitize(parent::getClean());
    
        function getSearchable() {
    
            // Replace tag chars with spaces (to ensure words are separated)
            $body = Format::html($this->body, array('hook_tag' => function($el, $attributes=0) {
                static $non_ws = array('wbr' => 1);
                return (isset($non_ws[$el])) ? '' : ' ';
            }));
            // Collapse multiple white-spaces
            $body = html_entity_decode($body, ENT_QUOTES);
            $body = preg_replace('`\s+`u', ' ', $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);
    
                return Format::display($this->body, true, !$this->options['balanced']);
    
    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 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 getActivity() {
            return new ThreadActivity(
                    _S('New Response'),
                    _S('New response posted'));
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getSubject() {
            return $this->getTitle();
        }
    
        function getRespondent() {
            return $this->getStaff();
        }
    
    
        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();
        }
    
    
        function getActivity() {
            return new ThreadActivity(
                    _S('New Internal Note'),
                    _S('New internal note posted'));
        }
    
    
        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 {
    
        static $types = array(
            ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
    
            ObjectModel::OBJECT_TYPE_TICKET => 'TicketThread',
    
        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;
    
    Peter Rotich's avatar
    Peter Rotich committed
                }
    
            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];
    
        function getLastMessage($criteria=false) {
    
            $entries = clone $this->getEntries();
            $entries->filter(array(
    
                'type' => MessageThreadEntry::ENTRY_TYPE
    
            if ($criteria)
                $entries->filter($criteria);
    
    
            $entries->order_by('-id');
    
            return $entries->first();
        }
    
        function getLastEmailMessage($criteria=array()) {
    
            $criteria += array(
                    'source' => 'Email',
                    'email_info__headers__isnull' => false);
    
            return $this->getLastMessage($criteria);
        }
    
        function getLastEmailMessageByUser($user) {
    
            $uid = is_numeric($user) ? $user : 0;
            if (!$uid && ($user instanceof EmailContact))
                $uid = $user->getUserId();
    
            return $uid
                    ? $this->getLastEmailMessage(array('user_id' => $uid))
                    : null;
    
        function getEntry($criteria) {
    
            if (is_numeric($criteria))
                return parent::getEntry($criteria);
    
            $entries = clone $this->getEntries();
            $entries->filter($criteria);
            return $entries->first();
        }
    
        function getMessages() {
            $entries = clone $this->getEntries();
            return $entries->filter(array(
                'type' => MessageThreadEntry::ENTRY_TYPE
            ));
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getResponses() {
    
            $entries = clone $this->getEntries();
            return $entries->filter(array(
    
                'type' => ResponseThreadEntry::ENTRY_TYPE
            ));
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function getNotes() {
    
            $entries = clone $this->getEntries();
            return $entries->filter(array(
    
                'type' => NoteThreadEntry::ENTRY_TYPE
            ));
    
        function addNote($vars, &$errors=array()) {
    
    Peter Rotich's avatar
    Peter Rotich committed
            //Add ticket Id.
            $vars['threadId'] = $this->getId();
    
            return NoteThreadEntry::add($vars, $errors);
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function addMessage($vars, &$errors) {
            $vars['threadId'] = $this->getId();
            $vars['staffId'] = 0;
    
    
            if (!($message = MessageThreadEntry::add($vars, $errors)))
    
                return $message;
    
            $this->lastmessage = SqlFunction::NOW();
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function addResponse($vars, &$errors) {
            $vars['threadId'] = $this->getId();
            $vars['userId'] = 0;
    
    JediKev's avatar
    JediKev committed
            if ($message = $this->getLastMessage())
                $vars['pid'] = $message->getId();
    
    
            $vars['flags'] = 0;
    
            if (!($resp = ResponseThreadEntry::add($vars, $errors)))
    
                return $resp;
    
            $this->lastresponse = SqlFunction::NOW();
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        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 {
    
        static function create($ticket=false) {
            assert($ticket !== false);
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            $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 getObJectId() {
            return $this->entry->getThread()->getObjectId();
        }
    
    
        function __construct(ThreadEntry $thread) {
    
            $this->entry = $thread;
    
        }
    
        abstract function trigger();
    
        function isEnabled() {
            return $this->isVisible();
        }
        function isVisible() {
            return true;
        }
    
        /**
         * getJsStub
         *
         * Retrieves a small JavaScript snippet to insert into the rendered page