Skip to content
Snippets Groups Projects
class.thread.php 79.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •         }
        }
    
        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() {
    
            // 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);
    
            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 {
    
        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;
    
            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());