Newer
Older
static function forTicket($ticket, $state, $user=false) {
global $thisstaff;
if($thisstaff && !$ticket->getStaffId())
$staff = $thisstaff->getId();
else
$staff = $ticket->getStaffId();
$inst = self::create(array(
'team_id' => $ticket->getTeamId(),
'dept_id' => $ticket->getDeptId(),
'topic_id' => $ticket->getTopicId(),
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',
* $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) {
// TODO: Use $object->createEvent() (nolint)
$event = ThreadEvent::forTicket($object, $state, $user);
$event = ThreadEvent::forTask($object, $state, $user);
# 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) {
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);
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
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);
}
}
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
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) {
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);
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
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;
$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;
}
}
}
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);
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
}
}
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')),
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
'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;
}
}
}
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);
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
}
}
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() {
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');
static function fromFormattedText($text, $format=false, $options=array()) {
switch ($format) {
case 'text':
return new HtmlThreadEntryBody($text, array('strip-embedded'=>false) + $options);
static function clean($text, $format=null) {
global $cfg;
$format = $format ?: ($cfg->isRichTextEnabled() ? 'html' : 'text');
$body = static::fromFormattedText($text, $format);
return $body->getClean();
}
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>';
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() {
// 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 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 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'));
}
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);
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
$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=array()) {
//Add ticket Id.
$vars['threadId'] = $this->getId();
return NoteThreadEntry::add($vars, $errors);
}
function addMessage($vars, &$errors) {
$vars['threadId'] = $this->getId();
$vars['staffId'] = 0;
if (!($message = MessageThreadEntry::add($vars, $errors)))
return $message;
$this->lastmessage = SqlFunction::NOW();
$this->save(true);
}
function addResponse($vars, &$errors) {
$vars['threadId'] = $this->getId();
$vars['userId'] = 0;
if ($message = $this->getLastMessage())
$vars['pid'] = $message->getId();
if (!($resp = ResponseThreadEntry::add($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=false) {
assert($ticket !== false);
$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 getObJectId() {
return $this->entry->getThread()->getObjectId();
}
function __construct(ThreadEntry $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