Newer
Older
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
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}'));
}
}
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';
}
static $types = array('text', 'html');
var $body;
var $type;
var $embedded_images = array();
var $options = array(
'strip-embedded' => true
);
function __construct($body, $type='text', $options=array()) {
$type = strtolower($type);
if (!in_array($type, static::$types))
throw new Exception("$type: Unsupported ThreadEntryBody type");
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) . ' ... '
$this->type = $type;
$this->options = array_merge($this->options, $options);
function isEmpty() {
return !$this->body || $this->body == '-';
}
function convertTo($type) {
if ($type === $this->type)
return $this;
$conv = $this->type . ':' . strtolower($type);
switch ($conv) {
case 'text:html':
return new ThreadEntryBody(sprintf('<pre>%s</pre>',
Format::htmlchars($this->body)), $type);
case 'html:text':
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,
// Strip the quoted part of the body
if ((list($msg) = explode($tag, $this->body, 2)) && trim($msg)) {
// Capture a list of dropped inline images
if ($images_before) {
preg_match_all('/src=("|\'|\b)cid:(\S+)\1/', $this->body,
$images_after, PREG_PATTERN_ORDER);
$this->stripped_images = array_diff($images_before[2],
$images_after[2]);
}
}
}
function getStrippedImages() {
return $this->stripped_images;
function getEmbeddedHtmlImages() {
return $this->embedded_images;
}
function getType() {
return $this->type;
}
function getClean() {
return trim($this->body);
}
function __toString() {
return (string) $this->body;
}
function toHtml() {
return $this->display('html');
}
function prepend($what) {
$this->body = $what . $this->body;
}
function append($what) {
$this->body .= $what;
}
function asVar() {
// Email template, assume HTML
return $this->display('email');
}
function display($format=false) {
throw new Exception('display: Abstract display() method not implemented');
static function fromFormattedText($text, $format=false, $options=array()) {
switch ($format) {
case 'text':
return new HtmlThreadEntryBody($text, array('strip-embedded'=>false) + $options);
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>';
return '<div style="white-space:pre-wrap">'
return nl2br($escaped);
return '<pre>'.$escaped.'</pre>';
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) : '';
// 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);
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;
return Format::display($this->body, true, !$this->options['balanced']);
/* 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()) {
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;
}
}
/* 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'));
}
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()) {
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;
}
}
/* 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'));
}
return self::add($vars, $errors);
static function add($vars, &$errors=array()) {
//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;
}
class ObjectThread extends Thread
implements TemplateVariable {
static $types = array(
ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
ObjectModel::OBJECT_TYPE_TICKET => 'TicketThread',
var $counts;
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;
$this->getCounts();
return $this->counts[MessageThreadEntry::ENTRY_TYPE];
$this->getCounts();
return $this->counts[ResponseThreadEntry::ENTRY_TYPE];
$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);
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
$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;
// XXX: PUNT
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
));
$entries = clone $this->getEntries();
return $entries->filter(array(
'type' => ResponseThreadEntry::ENTRY_TYPE
));
$entries = clone $this->getEntries();
return $entries->filter(array(
'type' => NoteThreadEntry::ENTRY_TYPE
));
}
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;
if (!($message = MessageThreadEntry::create($vars, $errors)))
return $message;
$this->lastmessage = SqlFunction::NOW();
$this->save(true);
}
function addResponse($vars, &$errors) {
$vars['threadId'] = $this->getId();
$vars['userId'] = 0;
if (!($resp = ResponseThreadEntry::create($vars, $errors)))
return $resp;
$this->lastresponse = SqlFunction::NOW();
$this->save(true);
}
function getVar($name) {
switch ($name) {
$entry = $this->entries->filter(array(
'type' => MessageThreadEntry::ENTRY_TYPE,
'flags__hasbit' => ThreadEntry::FLAG_ORIGINAL_MESSAGE,
))
->order_by('id')
->first();
if ($entry)
return $entry->getBody();
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);
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) {
$id = is_object($ticket) ? $ticket->getId() : $ticket;
$thread = parent::create(array(
'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';
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) {
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
}
abstract function trigger();
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(),
static::getId()
);
}
}
function getThreadId();
function getThread();
function postThreadEntry($type, $vars, $options=array());
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
/**
* ThreadActivity
*
* Object to thread activity
*
*/
class ThreadActivity implements TemplateVariable {
var $title;
var $desc;
function __construct($title, $desc) {
$this->title = $title;
$this->desc = $desc;
}
function getTitle() {
return $this->title;
}
function getDescription() {
return $this->desc;
}
function asVar() {
return (string) $this->getTitle();
}
function getVar($tag) {
if ($tag && is_callable(array($this, 'get'.ucfirst($tag))))
return call_user_func(array($this, 'get'.ucfirst($tag)));
return false;
}
static function getVarScope() {
return array(
'title' => __('Activity Title'),
'description' => __('Activity Description'),
);
}
}