diff --git a/css/thread.css b/css/thread.css index 2001dde95a01ad6761be77a93eadd390544f494d..6fe012353f3137d0629f15d4ac73667694460c16 100644 --- a/css/thread.css +++ b/css/thread.css @@ -467,9 +467,6 @@ table.thread-entry { table.thread-entry th div span { vertical-align: middle; } -table.thread-entry th div :not(.title) { - font-weight: 600; -} table.thread-entry th div .title { font-weight: 400; } diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 96518633461427d866094de07ea98e176368ea45..eb7ff09bc8a547acbcbff29b615ad2975ca3dd73 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -19,6 +19,7 @@ if(!defined('INCLUDE_DIR')) die('403'); include_once(INCLUDE_DIR.'class.ticket.php'); require_once(INCLUDE_DIR.'class.ajax.php'); require_once(INCLUDE_DIR.'class.note.php'); +include_once INCLUDE_DIR . 'class.thread_actions.php'; class TicketsAjaxAPI extends AjaxController { @@ -783,6 +784,25 @@ class TicketsAjaxAPI extends AjaxController { return self::_changeSelectedTicketsStatus($state, $info, $errors); } + function triggerThreadAction($ticket_id, $thread_id, $action) { + $thread = ThreadEntry::lookup($thread_id, $ticket_id); + if (!$thread) + Http::response(404, 'No such ticket thread entry'); + + $valid = false; + foreach ($thread->getActions() as $group=>$list) { + foreach ($list as $name=>$A) { + if ($A->getId() == $action) { + $valid = true; break; + } + } + } + if (!$valid) + Http::response(400, 'Not a valid action for this thread'); + + $thread->triggerAction($action); + } + private function _changeSelectedTicketsStatus($state, $info=array(), $errors=array()) { $count = $_REQUEST['count'] ?: diff --git a/include/class.file.php b/include/class.file.php index caafae2bf9cab7018b6aee5d4a8503d2f7d84295..82b998a783e969b70918399f7559d0dd78d69f7c 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -208,8 +208,10 @@ class AttachmentFile { if ($bk->sendRedirectUrl('inline')) return; $this->makeCacheable(); - Http::download($this->getName(), $this->getType() ?: 'application/octet-stream', - null, 'inline'); + $type = $this->getType() ?: 'application/octet-stream'; + if (isset($_REQUEST['overridetype'])) + $type = $_REQUEST['overridetype']; + Http::download($this->getName(), $type, null, 'inline'); header('Content-Length: '.$this->getSize()); $this->sendData(false); exit(); diff --git a/include/class.thread.php b/include/class.thread.php index 55273469ce5624f7306149c6761cc0c508a5063b..724471ed761666687721b266ab1243f9b4dfd2da 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -245,6 +245,7 @@ class ThreadEntry { var $thread; var $attachments; + var $_actions; function ThreadEntry($id, $threadId=0, $type='') { $this->load($id, $threadId, $type); @@ -799,7 +800,7 @@ class ThreadEntry { function lookupByEmailHeaders(&$mailinfo, &$seen=false) { // Search for messages using the References header, then the // in-reply-to header - $search = 'SELECT thread_entery_id, mid FROM '.THREAD_ENTRY_EMAIL_TABLE + $search = 'SELECT thread_entry_id, mid FROM '.THREAD_ENTRY_EMAIL_TABLE . ' WHERE mid=%s ' . ' ORDER BY thread_entry_id DESC'; @@ -1079,6 +1080,65 @@ class ThreadEntry { static function add($vars) { return ($entry=self::create($vars)) ? $entry->getId() : 0; } + + // Extensible thread entry actions ------------------------ + /** + * getActions + * + * Retrieve a list of possible actions. This list is shown to the agent + * via drop-down list at the top-right of the thread entry when rendered + * in the UI. + */ + function getActions() { + if (!isset($this->_actions)) { + $this->_actions = array(); + + foreach (self::$action_registry as $group=>$list) { + $T = array(); + $this->_actions[__($group)] = &$T; + foreach ($list as $id=>$action) { + $A = new $action($this); + if ($A->isVisible()) { + $T[$id] = $A; + } + } + unset($T); + } + } + return $this->_actions; + } + + function hasActions() { + foreach ($this->getActions() as $group => $list) { + if (count($list)) + return true; + } + return false; + } + + function triggerAction($name) { + foreach ($this->getActions() as $group=>$list) { + foreach ($list as $id=>$action) { + if (0 === strcasecmp($id, $name)) { + if (!$action->isEnabled()) + return false; + + $action->trigger(); + return true; + } + } + } + return false; + } + + static $action_registry = array(); + + static function registerAction($group, $action) { + if (!isset(self::$action_registry[$group])) + self::$action_registry[$group] = array(); + + self::$action_registry[$group][$action::getId()] = $action; + } } @@ -1253,7 +1313,7 @@ class HtmlThreadEntryBody extends ThreadEntryBody { function getSearchable() { // <br> -> \n - $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body); + $body = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $this->body); # <?php $body = Format::htmldecode(Format::striptags($body)); return Format::searchable($body); } @@ -1558,4 +1618,80 @@ class TicketThread extends ObjectThread { )); } } + +/** + * 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 $thread; + + 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->thread = $thread; + } + + abstract function trigger(); + + function getTicket() { + return $this->thread->getTicket(); + } + + 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->thread->getThread()->getObjectId(), + $this->thread->getId(), + static::getId() + ); + } +} ?> diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php new file mode 100644 index 0000000000000000000000000000000000000000..ce6ac82469df253fe5aecb942759de7f4a814252 --- /dev/null +++ b/include/class.thread_actions.php @@ -0,0 +1,55 @@ +<?php +/********************************************************************* + class.thread_actions.php + + Actions for thread entries. This serves as a simple repository for + drop-down actions which can be triggered on the ticket-view page for an + object's thread. + + Jared Hancock <jared@osticket.com> + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +include_once(INCLUDE_DIR.'class.thread.php'); + +class TEA_ShowEmailHeaders extends ThreadEntryAction { + static $id = 'view_headers'; + static $name = /* trans */ 'View Email Headers'; + static $icon = 'envelope'; + + function isEnabled() { + global $thisstaff; + + return $thisstaff && $thisstaff->isAdmin(); + } + + function isVisible() { + return (bool) $this->thread->getEmailHeader(); + } + + function getJsStub() { + return sprintf("$.dialog('%s');", + $this->getAjaxUrl() + ); + } + + function trigger() { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + return $this->trigger__get(); + } + } + + private function trigger__get() { + $headers = $this->thread->getEmailHeader(); + + include STAFFINC_DIR . 'templates/thread-email-headers.tmpl.php'; + } +} +ThreadEntry::registerAction(/* trans */ 'E-Mail', 'TEA_ShowEmailHeaders'); diff --git a/include/staff/templates/thread-email-headers.tmpl.php b/include/staff/templates/thread-email-headers.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..6e2f45809e0e9dc7d985b92eff1252fcb52ca7a0 --- /dev/null +++ b/include/staff/templates/thread-email-headers.tmpl.php @@ -0,0 +1,15 @@ +<h3><?php echo __('Raw Email Headers'); ?></h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<hr/> + +<pre style="max-height: 300px; overflow-y: scroll"> +<?php echo $headers; ?> +</pre> + +<hr> +<p class="full-width"> + <span class="buttons pull-right"> + <input type="button" name="cancel" class="close" + value="<?php echo __('Close'); ?>"> + </span> +</p> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 409eac800ab1399afc12f26c81faf90be9aec67d..ce64ae1156c060a3a5bb0fb113149dc376c67281 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -1,4 +1,6 @@ <?php +include_once INCLUDE_DIR . 'class.thread_actions.php'; + //Note that ticket obj is initiated in tickets.php. if(!defined('OSTSCPINC') || !$thisstaff || !is_object($ticket) || !$ticket->getId()) die('Invalid path'); @@ -394,7 +396,8 @@ $tcount+= $ticket->getNumNotes(); /* -------- Messages & Responses & Notes (if inline)-------------*/ $types = array('M', 'R', 'N'); if(($thread=$ticket->getThreadEntries($types))) { - foreach($thread as $entry) { ?> + foreach($thread as $entry) { + $tentry = $ticket->getThreadEntry($entry['id']); ?> <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> <tr> <th colspan="4" width="100%"> @@ -405,7 +408,29 @@ $tcount+= $ticket->getNumNotes(); <span style="display:inline-block;padding:0 1em" class="faded title"><?php echo Format::truncate($entry['title'], 100); ?></span> </span> - <span class="pull-right" style="white-space:no-wrap;display:inline-block"> +<?php if ($tentry->hasActions()) { + $actions = $tentry->getActions(); ?> + <div class="pull-right"> + <span class="action-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry['id']; ?>"> + <i class="icon-caret-down"></i> + <span ><i class="icon-cog"></i></span> + </span> + <div id="entry-action-more-<?php echo $entry['id']; ?>" class="action-dropdown anchor-right"> + <ul class="title"> +<?php foreach ($actions as $group => $list) { + foreach ($list as $id => $action) { ?> + <li> + <a class="no-pjax" href="#" onclick="javascript: + <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;"> + <i class="<?php echo $action->getIcon(); ?>"></i> <?php + echo $action->getName(); + ?></a></li> +<?php } + } ?> + </ul> + </div> +<?php } ?> + <span style="vertical-align:middle"> <span style="vertical-align:middle;" class="textra"></span> <span style="vertical-align:middle;" class="tmeta faded title"><?php @@ -420,7 +445,6 @@ $tcount+= $ticket->getNumNotes(); <?php $urls = null; if($entry['attachments'] - && ($tentry = $ticket->getThreadEntry($entry['id'])) && ($urls = $tentry->getAttachmentUrls()) && ($links = $tentry->getAttachmentsLinks())) {?> <tr> diff --git a/scp/ajax.php b/scp/ajax.php index 1c313c18c4afab80c58cbb3579de2cf254a34c76..131243adeb18c39b487631bcb9c5aee7b50044c1 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -142,6 +142,7 @@ $dispatcher = patterns('', url_get('^(?P<tid>\d+)/canned-resp/(?P<cid>\w+).(?P<format>json|txt)', 'cannedResponse'), url_get('^(?P<tid>\d+)/status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeTicketStatus'), url_post('^(?P<tid>\d+)/status$', 'setTicketStatus'), + url('^(?P<tid>\d+)/thread/(?P<thread_id>\d+)/(?P<action>\w+)$', 'triggerThreadAction'), url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'), url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'), url_get('^(?P<tid>\d+)/tasks$', 'tasks'),