diff --git a/include/ajax.config.php b/include/ajax.config.php index 0bf0a4420411304e02d14ae639f450ddb6bdbda6..132da3daf9de1003d3c5625b723880430416f714 100644 --- a/include/ajax.config.php +++ b/include/ajax.config.php @@ -38,7 +38,7 @@ class ConfigAjaxAPI extends AjaxController { list($primary_sl, $primary_locale) = explode('_', $primary); $config=array( - 'lock_time' => ($cfg->getLockTime()*60), + 'lock_time' => $cfg->getTicketLockMode() == Lock::MODE_DISABLED ? 0 : ($cfg->getLockTime()*60), 'html_thread' => (bool) $cfg->isRichTextEnabled(), 'date_format' => $cfg->getDateFormat(true), 'lang' => $lang, diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 30a8b74d74075d8e044f80d5a07ac733adfbc94e..31457ec4f80497849129f1a5f126ac7d8a4c21fb 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -92,7 +92,7 @@ class TicketsAjaxAPI extends AjaxController { function acquireLock($tid) { global $cfg, $thisstaff; - if(!$cfg || !$cfg->getLockTime()) + if(!$cfg || !$cfg->getLockTime() || $cfg->getTicketLockMode() == Lock::MODE_DISABLED) Http::response(418, $this->encode(array('id'=>0, 'retry'=>false))); if(!$tid || !is_numeric($tid) || !$thisstaff) diff --git a/include/class.client.php b/include/class.client.php index 31e185c8bef7c763731c6e492214e5522414ee75..50fda6cef195f8e01e9e4277ade1a5cc31f82d57 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -303,7 +303,8 @@ class EndUser extends BaseAuthenticatedUser { private function getStats() { $basic = Ticket::objects() ->annotate(array('count' => SqlAggregate::COUNT('ticket_id'))) - ->values('status__state', 'topic_id'); + ->values('status__state', 'topic_id') + ->distinct('status_id', 'topic_id'); // Share tickets among the organization for owners only $mine = clone $basic; diff --git a/include/class.config.php b/include/class.config.php index 984a83c1619b23f1200622432333fedb5c898bf7..b3c75485c1ed9d8862656cb4e9c2e8ea1d1dc20f 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -176,6 +176,7 @@ class OsticketConfig extends Config { 'verify_email_addrs' => 1, 'client_avatar' => 'gravatar.mm', 'agent_avatar' => 'gravatar.mm', + 'ticket_lock' => 2, // Lock on activity ); function OsticketConfig($section=null) { @@ -424,6 +425,10 @@ class OsticketConfig extends Config { return $this->get('autolock_minutes'); } + function getTicketLockMode() { + return $this->get('ticket_lock'); + } + function getAgentNameFormat() { return $this->get('agent_name_format'); } @@ -1204,6 +1209,7 @@ class OsticketConfig extends Config { 'show_related_tickets'=>isset($vars['show_related_tickets'])?1:0, 'hide_staff_name'=>isset($vars['hide_staff_name'])?1:0, 'allow_client_updates'=>isset($vars['allow_client_updates'])?1:0, + 'ticket_lock' => $vars['ticket_lock'], )); } diff --git a/include/class.email.php b/include/class.email.php index 4baa862f45c8e3d6eee798cbd20539c31c8cc7ab..92348241cc543f0041e6fa4ca69ab4a5c55d645a 100644 --- a/include/class.email.php +++ b/include/class.email.php @@ -200,10 +200,15 @@ class Email extends VerySimpleModel { Dept::objects() ->filter(array('email_id' => $this->getId())) ->update(array( - 'autoresp_email_id' => 0, 'email_id' => $cfg->getDefaultEmailId() )); + Dept::objects() + ->filter(array('autoresp_email_id' => $this->getId())) + ->update(array( + 'autoresp_email_id' => 0, + )); + return true; } diff --git a/include/class.file.php b/include/class.file.php index e502ff80507e89c91040de4f099c8e46caddc327..580330b64cc8731cd1031c0e5974f1655e281e6b 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -531,11 +531,16 @@ class AttachmentFile extends VerySimpleModel { static function getBackendForFile($file) { global $cfg; - if (!$cfg) + $char = null; + if ($cfg) { + $char = $cfg->getDefaultStorageBackendChar(); + } + try { + return FileStorageBackend::lookup($char ?: 'D', $file); + } + catch (Exception $x) { return new AttachmentChunkedData($file); - - $char = $cfg->getDefaultStorageBackendChar(); - return FileStorageBackend::lookup($char, $file); + } } static function lookupByHash($hash) { diff --git a/include/class.forms.php b/include/class.forms.php index 1ee044aa1d834d7ac6ef848bf86a635a8c693b2b..107cea8bf25acddfc36821ad400f700f7c002e25 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -597,6 +597,8 @@ class FormField { function getClean() { if (!isset($this->_clean)) { $this->_clean = (isset($this->value)) + // XXX: The widget value may be parsed already if this is + // linked to dynamic data via ::getAnswer() ? $this->value : $this->parse($this->getWidget()->value); if ($vs = $this->get('cleaners')) { @@ -2065,6 +2067,8 @@ class PriorityField extends ChoiceField { } function to_php($value, $id=false) { + if ($value instanceof Priority) + return $value; if (is_array($id)) { reset($id); $id = key($id); @@ -2083,6 +2087,13 @@ class PriorityField extends ChoiceField { : $prio; } + function display($prio) { + if (!$prio instanceof Priority) + return parent::display($prio); + return sprintf('<span style="padding: 2px; background-color: %s">%s</span>', + $prio->getColor(), Format::htmlchars($prio->getDesc())); + } + function toString($value) { return ($value instanceof Priority) ? $value->getDesc() : $value; } diff --git a/include/class.lock.php b/include/class.lock.php index d1c5bc3655748f72ff16c092657f32e74c843365..e5da2f048e894bd60733254a0604a5ae51c6fcf9 100644 --- a/include/class.lock.php +++ b/include/class.lock.php @@ -38,6 +38,10 @@ class Lock extends VerySimpleModel { ), ); + const MODE_DISABLED = 0; + const MODE_ON_VIEW = 1; + const MODE_ON_ACTIVITY = 2; + function getId() { return $this->lock_id; } diff --git a/include/class.mailer.php b/include/class.mailer.php index ee22db849f18ff121bad2c9d84313fc3f088de5b..b3c8c2111af544e231a564c3556d91bcfd4ffed1 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -278,7 +278,7 @@ class Mailer { // Round-trip detection - the first section is the local // system's message-id code - $rv['loopback'] = (0 === strcasecmp($rv['code'], + $rv['loopback'] = (0 === strcmp($rv['code'], static::getSystemMessageIdCode())); return $rv; diff --git a/include/class.organization.php b/include/class.organization.php index 0c08a4f30c08281e8b473747944b3c81046a1b0d..e870667b1f10e2132b9bdc51a2fa7c18e65f02aa 100644 --- a/include/class.organization.php +++ b/include/class.organization.php @@ -257,7 +257,7 @@ implements TemplateVariable { } } - function addForm($form, $sort=1, $data) { + function addForm($form, $sort=1, $data=null) { $entry = $form->instanciate($sort, $data); $entry->set('object_type', 'O'); $entry->set('object_id', $this->getId()); diff --git a/include/class.orm.php b/include/class.orm.php index 9798d94819db755e51664e83e739b4ec4dfde6f6..a92d476682f0ed2373adffe3c18dd1f35222ef88 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -919,6 +919,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl var $distinct = array(); var $lock = false; var $chain = array(); + var $options = array(); const LOCK_EXCLUSIVE = 1; const LOCK_SHARED = 2; @@ -986,6 +987,9 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } function order_by($order, $direction=false) { + if ($order === false) + return $this->options(array('nosort' => true)); + $args = func_get_args(); if (in_array($direction, array(self::ASC, self::DESC))) { $args = array($args[0]); @@ -1184,6 +1188,11 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl return $this; } + function options($options) { + $this->options = array_merge($this->options, $options); + return $this; + } + function countSelectFields() { $count = count($this->values) + count($this->annotations); if (isset($this->extra['select'])) @@ -1259,6 +1268,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl // Load defaults from model $model = $this->model; $query = clone $this; + $options += $this->options; if ($options['nosort']) $query->ordering = array(); elseif (!$query->ordering && $model::getMeta('ordering')) @@ -2523,7 +2533,7 @@ class MySqlCompiler extends SqlCompiler { } // If there are annotations, add in these fields to the // GROUP BY clause - if ($queryset->annotations) + if ($queryset->annotations && !$queryset->distinct) $group_by[] = $unaliased; } } @@ -2557,7 +2567,7 @@ class MySqlCompiler extends SqlCompiler { } } // If no group by has been set yet, use the root model pk - if (!$group_by && !$queryset->aggregated) { + if (!$group_by && !$queryset->aggregated && !$queryset->distinct) { foreach ($model::getMeta('pk') as $pk) $group_by[] = $rootAlias .'.'. $pk; } @@ -2580,29 +2590,31 @@ class MySqlCompiler extends SqlCompiler { $joins = $this->getJoins($queryset); + $sql = 'SELECT '.implode(', ', $fields).' FROM ' + .$table.$joins.$where.$group_by.$having.$sort; // UNIONS - $unions=''; if ($queryset->chain) { + // If the main query is sorted, it will need parentheses + if ($parens = (bool) $sort) + $sql = "($sql)"; foreach ($queryset->chain as $qs) { list($qs, $all) = $qs; $q = $qs->getQuery(array('nosort' => true)); // Rewrite the parameter numbers so they fit the parameter numbers // of the current parameters of the $compiler $self = $this; - $sql = preg_replace_callback("/:(\d+)/", + $S = preg_replace_callback("/:(\d+)/", function($m) use ($self, $q) { $self->params[] = $q->params[$m[1]-1]; return ':'.count($self->params); }, $q->sql); // Wrap unions in parentheses if they are windowed or sorted - if ($qs->isWindowed() || count($qs->getSortFields())) - $sql = "($sql)"; - $unions .= ' UNION '.($all ? 'ALL ' : '').$sql; + if ($parens || $qs->isWindowed() || count($qs->getSortFields())) + $S = "($S)"; + $sql .= ' UNION '.($all ? 'ALL ' : '').$S; } } - $sql = 'SELECT '.implode(', ', $fields).' FROM ' - .$table.$joins.$where.$group_by.$having.$unions.$sort; if ($queryset->limit) $sql .= ' LIMIT '.$queryset->limit; if ($queryset->offset) diff --git a/include/class.thread.php b/include/class.thread.php index 68d1d69358e1feb9db91de5ca05423fbd3dd9cbb..ffffe918d51e9229a7925578729a4756a2b92dbe 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -278,8 +278,6 @@ class Thread extends VerySimpleModel { * - body - (string) email message body (decoded) */ function postEmail($mailinfo) { - global $ost; - // +==================+===================+=============+ // | Orig Thread-Type | Reply Thread-Type | Requires | // +==================+===================+=============+ @@ -303,27 +301,6 @@ class Thread extends VerySimpleModel { return false; } - // Mail sent by this system will have a message-id format of - // <code-random-mailbox@domain.tld> - // where code is a predictable string based on the SECRET_SALT of - // this osTicket installation. If this incoming mail matches the - // code, then it very likely originated from this system and looped - @list($code) = explode('-', $mailinfo['mid'], 2); - if (0 === strcasecmp(ltrim($code, '<'), substr(md5('mail'.SECRET_SALT), -9))) { - // This mail was sent by this system. It was received due to - // some kind of mail delivery loop. It should not be considered - // a response to an existing thread entry - if ($ost) $ost->log(LOG_ERR, _S('Email loop detected'), sprintf( - _S('It appears as though <%s> is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'), - $mailinfo['email']), - - // This is quite intentional -- don't continue the loop - false, - // Force the message, even if logging is disabled - true); - return true; - } - $vars = array( 'mid' => $mailinfo['mid'], 'header' => $mailinfo['header'], @@ -477,13 +454,13 @@ class Thread extends VerySimpleModel { */ function lookupByEmailHeaders(&$mailinfo) { $possibles = array(); - foreach (array('in-reply-to', 'references') as $header) { + foreach (array('mid', 'in-reply-to', 'references') as $header) { $matches = array(); if (!isset($mailinfo[$header]) || !$mailinfo[$header]) continue; // Header may have multiple entries (usually separated by // spaces ( ) - elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header], + elseif (!preg_match_all('/<([^>@]+@[^>]+)>/', $mailinfo[$header], $matches)) continue; @@ -491,12 +468,12 @@ class Thread extends VerySimpleModel { // (parent) on the far right. // @see rfc 1036, section 2.2.5 // @see http://www.jwz.org/doc/threading.html - $possibles = array_merge($possibles, array_reverse($matches[0])); + $possibles = array_merge($possibles, array_reverse($matches[1])); } // Add the message id if it is embedded in the body $match = array(); - if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`', + if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`', $mailinfo['message'], $match) && !in_array($match[1], $possibles) ) { @@ -510,7 +487,9 @@ class Thread extends VerySimpleModel { // from this help desk, the 'loopback' property will be set // to true. $mid_info = Mailer::decodeMessageId($mid); - if ($mid_info['loopback'] && isset($mid_info['uid']) + if (!$mid_info || !$mid_info['loopback']) + continue; + if (isset($mid_info['uid']) && @$mid_info['threadId'] && ($t = Thread::lookup($mid_info['threadId'])) ) { @@ -630,6 +609,8 @@ implements TemplateVariable { ); function postEmail($mailinfo) { + global $ost; + if (!($thread = $this->getThread())) // Kind of hard to continue a discussion without a thread ... return false; @@ -638,6 +619,26 @@ implements TemplateVariable { // Reporting success so the email can be moved or deleted. return true; + // Mail sent by this system will have a predictable message-id + // If this incoming mail matches the code, then it very likely + // originated from this system and looped + $info = Mailer::decodeMessageId($mailinfo['mid']); + if ($info && $info['loopback']) { + // This mail was sent by this system. It was received due to + // some kind of mail delivery loop. It should not be considered + // a response to an existing thread entry + if ($ost) + $ost->log(LOG_ERR, _S('Email loop detected'), sprintf( + _S('It appears as though <%s> is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'), + $mailinfo['email']), + + // This is quite intentional -- don't continue the loop + false, + // Force the message, even if logging is disabled + true); + return $this; + } + return $thread->postEmail($mailinfo); } @@ -1127,6 +1128,7 @@ implements TemplateVariable { // in-reply-to header if ($entry = ThreadEntry::objects() ->filter(array('email_info__mid' => $mailinfo['mid'])) + ->order_by(false) ->first() ) { $seen = true; @@ -1134,13 +1136,13 @@ implements TemplateVariable { } $possibles = array(); - foreach (array('in-reply-to', 'references') as $header) { + foreach (array('mid', 'in-reply-to', 'references') as $header) { $matches = array(); if (!isset($mailinfo[$header]) || !$mailinfo[$header]) continue; // Header may have multiple entries (usually separated by // spaces ( ) - elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header], + elseif (!preg_match_all('/<([^>@]+@[^>]+)>/', $mailinfo[$header], $matches)) continue; @@ -1148,13 +1150,13 @@ implements TemplateVariable { // (parent) on the far right. // @see rfc 1036, section 2.2.5 // @see http://www.jwz.org/doc/threading.html - $possibles = array_merge($possibles, array_reverse($matches[0])); + $possibles = array_merge($possibles, array_reverse($matches[1])); } // Add the message id if it is embedded in the body $match = array(); - if (preg_match('`(?:data-mid="|Ref-Mid: )([^"\s]*)(?:$|")`', - $mailinfo['message'], $match) + if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`', + (string) $mailinfo['message'], $match) && !in_array($match[1], $possibles) ) { $possibles[] = $match[1]; @@ -1168,7 +1170,9 @@ implements TemplateVariable { // from this help desk, the 'loopback' property will be set // to true. $mid_info = Mailer::decodeMessageId($mid); - if ($mid_info['loopback'] && isset($mid_info['uid']) + if (!$mid_info || !$mid_info['loopback']) + continue; + if (isset($mid_info['uid']) && @$mid_info['entryId'] && ($t = ThreadEntry::lookup($mid_info['entryId'])) && ($t->thread_id == $mid_info['threadId']) @@ -1198,7 +1202,8 @@ implements TemplateVariable { $mid = "$left@$right"; } $entries = ThreadEntry::objects() - ->filter(array('email_info__mid' => $mid)); + ->filter(array('email_info__mid' => $mid)) + ->order_by(false); foreach ($entries as $t) { // Capture the first match thread item if (!$thread) @@ -1248,24 +1253,6 @@ implements TemplateVariable { } } - // Search for the message-id token in the body - // *DEPRECATED* the current algo on outgoing mail will use - // Mailer::getMessageId as the message id tagged here - if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`', - $mailinfo['message'], $match)) { - // Support new Message-Id format - if (($info = Mailer::decodeMessageId($match[1])) - && $info['loopback'] - && $info['entryId'] - ) { - return ThreadEntry::lookup($info['entryId']); - } - // Support old (deprecated) reference format - if ($thread = ThreadEntry::lookupByRefMessageId($match[1], - $mailinfo['email'])) - return $thread; - } - return null; } diff --git a/include/class.user.php b/include/class.user.php index 8d5b6836859e2aa0531bdbd659f1c680bb61c8ea..1dd4736b2740c2ecdbed95b228bef50d02f637b0 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -207,7 +207,9 @@ implements TemplateVariable { $user = static::lookupByEmail($vars['email']); if (!$user && $create) { $name = $vars['name']; - if (!$name) + if (is_array($name)) + $name = implode(', ', $name); + elseif (!$name) list($name) = explode('@', $vars['email'], 2); $user = User::create(array( @@ -503,7 +505,10 @@ implements TemplateVariable { foreach ($forms as $entry) { if (($f=$entry->getDynamicForm()) && $f->get('type') == 'U') { if (($name = $f->getField('name'))) { - $this->name = $name->getClean(); + $name = $name->getClean(); + if (is_array($name)) + $name = implode(', ', $name); + $this->name = $name; } if (($email = $f->getField('email'))) { diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index fef80c6008f3bae8555da341d00dd55a237e316e..cf3c5e1d92d1c83fd9b02a9bea18bc12f7e3470e 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -95,10 +95,12 @@ if ($settings['keywords']) { $tickets->filter(array('number__startswith'=>$q)); } else { //Deep search! // Use the search engine to perform the search - $tickets = $ost->searcher->find($q, $tickets)->distinct('ticket_id'); + $tickets = $ost->searcher->find($q, $tickets); } } +$tickets->distinct('ticket_id'); + TicketForm::ensureDynamicDataView(); $total=$tickets->count(); diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php index 8767a0ec8c790103f7549558308590983d0aa26c..817d20b6b82b6f9b10e18e78cd3b96a3c853aa42 100644 --- a/include/staff/filter.inc.php +++ b/include/staff/filter.inc.php @@ -342,7 +342,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); }; $(function() { $('#dynamic-actions').sortable({helper: fixHelper, opacity: 0.5}); - var next = <?php echo $maxi; ?>; + var next = <?php echo $maxi ?: 0; ?>; $('#add-rule').click(function() { var clone = $('#new-rule-template tr').clone(); clone.find('[data-name=rulew]').attr('name', 'rules['+next+'][w]'); @@ -351,5 +351,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); clone.appendTo('#rules'); next++; }); +<?php if (!$info['rules']) { ?> + $('#add-rule').trigger('click').trigger('click'); +<?php } ?> }); </script> diff --git a/include/staff/settings-access.inc.php b/include/staff/settings-access.inc.php deleted file mode 100644 index c7cbc5034e79f4347a99778595c6c20f3a2cbc4c..0000000000000000000000000000000000000000 --- a/include/staff/settings-access.inc.php +++ /dev/null @@ -1,225 +0,0 @@ -<?php -if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied'); - -?> -<h2><?php echo __('Access Control Settings'); ?></h2> -<form action="settings.php?t=access" method="post" id="save"> -<?php csrf_token(); ?> -<input type="hidden" name="t" value="access" > -<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> - <thead> - <tr> - <th colspan="2"> - <h4><?php echo __('Configure Access to this Help Desk'); ?></h4> - </th> - </tr> - </thead> - <tbody> - <tr> - <th colspan="2"> - <em><b><?php echo __('Agent Authentication Settings'); ?></b></em> - </th> - </tr> - <tr><td><?php echo __('Password Expiration Policy'); ?>:</th> - <td> - <select name="passwd_reset_period"> - <option value="0"> — <?php echo __('No expiration'); ?> —</option> - <?php - for ($i = 1; $i <= 12; $i++) { - echo sprintf('<option value="%d" %s>%s</option>', - $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''), - sprintf(_N('Monthly', 'Every %d months', $i), $i)); - } - ?> - </select> - <font class="error"><?php echo $errors['passwd_reset_period']; ?></font> - <i class="help-tip icon-question-sign" href="#password_expiration_policy"></i> - </td> - </tr> - <tr><td><?php echo __('Allow Password Resets'); ?>:</th> - <td> - <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>> - <i class="help-tip icon-question-sign" href="#allow_password_resets"></i> - </td> - </tr> - <tr><td><?php echo __('Reset Token Expiration'); ?>:</th> - <td> - <input type="text" name="pw_reset_window" size="6" value="<?php - echo $config['pw_reset_window']; ?>"> - <em><?php echo __('minutes'); ?></em> - <i class="help-tip icon-question-sign" href="#reset_token_expiration"></i> - <font class="error"><?php echo $errors['pw_reset_window']; ?></font> - </td> - </tr> - <tr><td><?php echo __('Agent Excessive Logins'); ?>:</td> - <td> - <select name="staff_max_logins"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_max_logins']==$i)?'selected="selected"':''), $i); - } - ?> - </select> <?php echo __( - 'failed login attempt(s) allowed before a lock-out is enforced'); ?> - <br/> - <select name="staff_login_timeout"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_login_timeout']==$i)?'selected="selected"':''), $i); - } - ?> - </select> <?php echo __('minutes locked out'); ?> - </td> - </tr> - <tr><td><?php echo __('Agent Session Timeout'); ?>:</td> - <td> - <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>"> - <?php echo __('minutes'); ?> <em><?php echo __('(0 to disable)'); ?></em>. <i class="help-tip icon-question-sign" href="#staff_session_timeout"></i> - </td> - </tr> - <tr><td><?php echo __('Bind Agent Session to IP'); ?>:</td> - <td> - <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>> - <i class="help-tip icon-question-sign" href="#bind_staff_session_to_ip"></i> - </td> - </tr> - <tr> - <th colspan="2"> - <em><b><?php echo __('End User Authentication Settings'); ?></b></em> - </th> - </tr> - <tr><td><?php echo __('Registration Required'); ?>:</td> - <td><input type="checkbox" name="clients_only" <?php - if ($config['clients_only']) - echo 'checked="checked"'; ?>/> <?php echo __( - 'Require registration and login to create tickets'); ?> - <i class="help-tip icon-question-sign" href="#registration_method"></i> - </td> - <tr><td><?php echo __('Registration Method'); ?>:</td> - <td><select name="client_registration"> -<?php foreach (array( - 'disabled' => __('Disabled — All users are guests'), - 'public' => __('Public — Anyone can register'), - 'closed' => __('Private — Only agents can register users'),) - as $key=>$val) { ?> - <option value="<?php echo $key; ?>" <?php - if ($config['client_registration'] == $key) - echo 'selected="selected"'; ?>><?php echo $val; - ?></option><?php - } ?> - </select> - <i class="help-tip icon-question-sign" href="#registration_method"></i> - </td> - </tr> - <tr><td><?php echo __('User Excessive Logins'); ?>:</td> - <td> - <select name="client_max_logins"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_max_logins']==$i)?'selected="selected"':''), $i); - } - - ?> - </select> <?php echo __( - 'failed login attempt(s) allowed before a lock-out is enforced'); ?> - <br/> - <select name="client_login_timeout"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_login_timeout']==$i)?'selected="selected"':''), $i); - } - ?> - </select> <?php echo __('minutes locked out'); ?> - </td> - </tr> - <tr><td><?php echo __('User Session Timeout'); ?>:</td> - <td> - <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>"> - <i class="help-tip icon-question-sign" href="#client_session_timeout"></i> - </td> - </tr> - <tr><td><?php echo __('Client Quick Access'); ?>:</td> - <td><input type="checkbox" name="client_verify_email" <?php - if ($config['client_verify_email']) - echo 'checked="checked"'; ?>/> <?php echo __( - 'Require email verification on "Check Ticket Status" page'); ?> - <i class="help-tip icon-question-sign" href="#client_verify_email"></i> - </td> - </tr> - </tbody> - <thead> - <tr> - <th colspan="2"> - <h4><?php echo __('Authentication and Registration Templates'); ?></h4> - </th> - </tr> - </thead> - <tbody> -<?php -$res = db_query('select distinct(`type`), content_id, notes, name, updated from ' - .PAGE_TABLE - .' where isactive=1 group by `type`'); -$contents = array(); -while (list($type, $id, $notes, $name, $u) = db_fetch_row($res)) - $contents[$type] = array($id, $name, $notes, $u); - -$manage_content = function($title, $content) use ($contents) { - list($id, $name, $notes, $upd) = $contents[$content]; - $notes = explode('. ', $notes); - $notes = $notes[0]; - ?><tr><td colspan="2"> - <div style="padding:2px 5px"> - <a href="#ajax.php/content/<?php echo $id; ?>/manage" - onclick="javascript: - $.dialog($(this).attr('href').substr(1), 201); - return false;" class="pull-left"><i class="icon-file-text icon-2x" - style="color:#bbb;"></i> </a> - <span style="display:inline-block;width:90%;width:calc(100% - 32px);padding-left:10px;line-height:1.2em"> - <a href="#ajax.php/content/<?php echo $id; ?>/manage" - onclick="javascript: - $.dialog($(this).attr('href').substr(1), 201); - return false;"><?php - echo Format::htmlchars($title); ?></a><br/> - <span class="faded"><?php - echo Format::display($notes); ?> - <em>(<?php echo sprintf(__('Last Updated %s'), Format::datetime($upd)); - ?>)</em></span> - </div></td></tr><?php -}; ?> - <tr> - <th colspan="2"> - <em><b><?php echo __( - 'Authentication and Registration Templates'); ?></b></em> - </th> - </tr> - <?php $manage_content(__('Agents'), 'pwreset-staff'); ?> - <?php $manage_content(__('Clients'), 'pwreset-client'); ?> - <?php $manage_content(__('Guest Ticket Access'), 'access-link'); ?> - <tr> - <th colspan="2"> - <em><b><?php echo __('Sign In Pages'); ?></b></em> - </th> - </tr> - <?php $manage_content(__('Agent Login Banner'), 'banner-staff'); ?> - <?php $manage_content(__('Client Sign-In Page'), 'banner-client'); ?> - <tr> - <th colspan="2"> - <em><b><?php echo __('User Account Registration'); ?></b></em> - </th> - </tr> - <?php $manage_content(__('Please Confirm Email Address Page'), 'registration-confirm'); ?> - <?php $manage_content(__('Confirmation Email'), 'registration-client'); ?> - <?php $manage_content(__('Account Confirmed Page'), 'registration-thanks'); ?> - <tr> - <th colspan="2"> - <em><b><?php echo __('Agent Account Registration'); ?></b></em> - </th> - </tr> - <?php $manage_content(__('Agent Welcome Email'), 'registration-staff'); ?> -</tbody> -</table> -<p style="text-align:center"> - <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>"> - <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>"> -</p> -</form> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index 8edf21818a2268df33dfbfea2c977ede1afea26f..4fe241476415916aac3d6295ee943fd12ef6caf9 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -145,6 +145,23 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <span class="error"><?php echo $errors['default_help_topic']; ?></span> </td> </tr> + <tr> + <td width="180"><?php echo __('Lock Semantics'); ?>:</td> + <td> + <select name="ticket_lock" <?php if ($cfg->getLockTime() == 0) echo 'disabled="disabled"'; ?>> +<?php foreach (array( + Lock::MODE_DISABLED => __('Disabled'), + Lock::MODE_ON_VIEW => __('Lock on view'), + Lock::MODE_ON_ACTIVITY => __('Lock on activity'), +) as $v => $desc) { ?> + <option value="<?php echo $v; ?>" <?php + if ($config['ticket_lock'] == $v) echo 'selected="selected"'; + ?>><?php echo $desc; ?></option> +<?php } ?> + </select> + <div class="error"><?php echo $errors['ticket_lock']; ?></div> + </td> + </tr> <tr> <td><?php echo __('Maximum <b>Open</b> Tickets');?>:</td> <td> diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php index 7e5e1638824a5491a1ec11d1cdf8b51299b853c0..a3f3bdfc725a80b0c636933286c3b50b63d03e01 100644 --- a/include/staff/templates/thread-entries.tmpl.php +++ b/include/staff/templates/thread-entries.tmpl.php @@ -4,6 +4,13 @@ $events = $events->getIterator(); $events->rewind(); $event = $events->current(); $htmlId = $options['html-id'] ?: ('thread-'.$this->getId()); + +$thread_attachments = array(); +foreach (Attachment::objects()->filter(array( + 'thread_entry__thread__id' => $this->getId(), +))->select_related('thread_entry', 'file') as $att) { + $thread_attachments[$att->object_id][] = $att; +} ?> <div id="<?php echo $htmlId; ?>"> <div id="thread-items" data-thread-id="<?php echo $this->getId(); ?>"> @@ -58,16 +65,16 @@ $htmlId = $options['html-id'] ?: ('thread-'.$this->getId()); // Set inline image urls. <?php $urls = array(); - foreach (AttachmentFile::objects()->filter(array( - 'attachments__thread_entry__thread__id' => $this->getId(), - 'attachments__inline' => true, - )) as $file) { - $urls[strtolower($file->getKey())] = array( - 'download_url' => $file->getDownloadUrl(), - 'filename' => $file->name, + foreach ($thread_attachments as $eid=>$atts) { + foreach ($atts as $A) { + if (!$A->inline) + continue; + $urls[strtolower($A->file->getKey())] = array( + 'download_url' => $A->file->getDownloadUrl(), + 'filename' => $A->getFilename(), ); - } + } ?> $('#'+container).data('imageUrls', <?php echo JsonDataEncoder::encode($urls); ?>); // Trigger thread processing. diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index 3f3bfee7c97316a62a87ac00716eafdc7e513a32..4a86b68ee0f17bffd1c9647c8fca4bae081a2731 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -70,10 +70,11 @@ if ($user) // The strangeness here is because .has_attachments is an annotation from // Thread::getEntries(); however, this template may be used in other // places such as from thread entry editing - if (isset($entry->has_attachments) ? $entry->has_attachments - : $entry->attachments->filter(array('inline'=>0))->count()) { ?> + $atts = isset($thread_attachments) ? $thread_attachments[$entry->id] : $entry->attachments; + if (isset($atts) && $atts) { +?> <div class="attachments"><?php - foreach ($entry->attachments as $A) { + foreach ($atts as $A) { if ($A->inline) continue; $size = ''; @@ -87,12 +88,13 @@ if ($user) target="_blank"><?php echo Format::htmlchars($A->getFilename()); ?></a><?php echo $size;?> </span> -<?php } ?> - </div> -<?php } ?> +<?php } + echo '</div>'; + } +?> </div> <?php - if ($urls = $entry->getAttachmentUrls()) { ?> + if (!isset($thread_attachments) && ($urls = $entry->getAttachmentUrls())) { ?> <script type="text/javascript"> $('#thread-entry-<?php echo $entry->getId(); ?>') .data('urls', <?php diff --git a/include/staff/templates/ticket-preview.tmpl.php b/include/staff/templates/ticket-preview.tmpl.php index 168847fc8ee4fdfda4a9b590d3b69cf6b78b3941..859bf0a8957b812634a774b1db0e7e2c4477f9b8 100644 --- a/include/staff/templates/ticket-preview.tmpl.php +++ b/include/staff/templates/ticket-preview.tmpl.php @@ -8,6 +8,7 @@ $staff=$ticket->getStaff(); $lock=$ticket->getLock(); $role=$thisstaff->getRole($ticket->getDeptId()); $error=$msg=$warn=null; +$thread = $ticket->getThread(); if($lock && $lock->getStaffId()==$thisstaff->getId()) $warn.=' <span class="Icon lockedTicket">' @@ -34,12 +35,12 @@ echo '<ul class="tabs" id="ticket-preview">'; echo ' <li class="active"><a id="preview_tab" href="#preview" ><i class="icon-list-alt"></i> '.__('Ticket Summary').'</a></li>'; -if ($ticket->getThread()->getNumCollaborators()) { +if ($thread && $thread->getNumCollaborators()) { echo sprintf(' <li><a id="collab_tab" href="#collab" ><i class="icon-fixed-width icon-group faded"></i> '.__('Collaborators (%d)').'</a></li>', - $ticket->getThread()->getNumCollaborators()); + $thread->getNumCollaborators()); } echo '</ul>'; echo '<div id="ticket-preview_container">'; @@ -121,7 +122,7 @@ echo '</div>'; // ticket preview content. <table border="0" cellspacing="" cellpadding="1"> <colgroup><col style="min-width: 250px;"></col></colgroup> <?php - if (($collabs=$ticket->getThread()->getCollaborators())) {?> + if ($thread && ($collabs=$thread->getCollaborators())) {?> <?php foreach($collabs as $collab) { echo sprintf('<tr><td %s><i class="icon-%s"></i> @@ -141,7 +142,7 @@ echo '</div>'; // ticket preview content. echo sprintf('<span><a class="collaborators" href="#tickets/%d/collaborators">%s</a></span>', $ticket->getId(), - $ticket->getThread()->getNumCollaborators() + $thread && $thread->getNumCollaborators() ? __('Manage Collaborators') : __('Add Collaborator') ); ?> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 6ec03ae8992c1da3ac8d7bd6aa08a789c5902d93..156b2c323556a57f4de5771baae88a64eb123fc0 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -16,6 +16,8 @@ $user = $ticket->getOwner(); //Ticket User (EndUser) $team = $ticket->getTeam(); //Assigned team. $sla = $ticket->getSLA(); $lock = $ticket->getLock(); //Ticket lock obj +if (!$lock && $cfg->getTicketLockMode() == Lock::MODE_ON_VIEW) + $lock = $ticket->acquireLock($thisstaff->getId()); $mylock = ($lock && $lock->getStaffId() == $thisstaff->getId()) ? $lock : null; $id = $ticket->getId(); //Ticket ID. diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index a388e5cad52f243bf16189a66e3b0df7f45eb49f..5adab6cec0eb49ac93584102a36b4b8c646bd90b 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -196,21 +196,6 @@ $pageNav = new Pagenate($count, $page, PAGE_LIMIT); $pageNav->setURL('tickets.php', $args); $tickets = $pageNav->paginate($tickets); -// Rewrite $tickets to use a nested query, which will include the LIMIT part -// in order to speed the result -// -// ATM, advanced search with keywords doesn't support the subquery approach -if ($use_subquery) { - $orig_tickets = clone $tickets; - $tickets2 = TicketModel::objects(); - $tickets2->values = $tickets->values; - $tickets2->filter(array('ticket_id__in' => $tickets->values_flat('ticket_id'))); - - // Transfer the order_by from the original tickets - $tickets2->order_by($orig_tickets->getSortFields()); - $tickets = $tickets2; -} - // Apply requested sorting $queue_sort_key = sprintf(':Q%s:%s:sort', ObjectModel::OBJECT_TYPE_TICKET, $queue_name); @@ -299,6 +284,17 @@ case 'updated': break; } +// Rewrite $tickets to use a nested query, which will include the LIMIT part +// in order to speed the result +$orig_tickets = clone $tickets; +$tickets2 = TicketModel::objects(); +$tickets2->values = $tickets->values; +$tickets2->filter(array('ticket_id__in' => $tickets->values_flat('ticket_id'))); + +// Transfer the order_by from the original tickets +$tickets2->order_by($orig_tickets->getSortFields()); +$tickets = $tickets2; + // Save the query to the session for exporting $_SESSION[':Q:tickets'] = $tickets; @@ -307,6 +303,7 @@ TicketForm::ensureDynamicDataView(); // Select pertinent columns // ------------------------------------------------------------ $tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__priority__priority_color', 'cdata__priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate', 'isanswered', 'staff__firstname', 'staff__lastname', 'team__name'); + // Add in annotations $tickets->annotate(array( 'collab_count' => TicketThread::objects() @@ -590,7 +587,7 @@ $(function() { +'?count='+count +'&_uid='+new Date().getTime(); $.dialog(url, [201], function (xhr) { - $.pjax.reload('#pjax-container'); + $.pjax({url: 'tickets.php', container: '#pjax-container'}); }); } return false; diff --git a/js/redactor-plugins.js b/js/redactor-plugins.js index 7ecef2bba40bb6859f9094ef66fff9cb1513f7a6..0a65831274553f2a10524a05df64d2237fadebca 100644 --- a/js/redactor-plugins.js +++ b/js/redactor-plugins.js @@ -354,7 +354,7 @@ RedactorPlugins.fullscreen = function() func: this.table.show, observe: { element: 'table', - in: { + 'in': { attr: { 'class': 'redactor-dropdown-link-inactive', 'aria-disabled': true, diff --git a/scp/css/scp.css b/scp/css/scp.css index a933d2855f952b678ccf3fbbc3e9b6d3da7c3947..4556855d36102a2e1085a66dcfdafae46cf568fd 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1047,6 +1047,9 @@ img.avatar { border-top-color: rgba(0,0,0,0.2); border-radius: 0 0 6px 6px; } +.thread-body .attachments:empty { + display: none; +} .thread-body .attachments .filesize { margin-left: 0.5em; line-height: 1em; diff --git a/scp/emails.php b/scp/emails.php index 82da3ad7c1929dc49c60ec5835a5b057d30e7f53..9ff1c70a771ad9942977a415e8b2cae8d4794ffa 100644 --- a/scp/emails.php +++ b/scp/emails.php @@ -50,14 +50,8 @@ if($_POST){ } else { $count=count($_POST['ids']); - $sql='SELECT count(dept_id) FROM '.DEPT_TABLE.' dept ' - .' WHERE email_id IN ('.implode(',', db_input($_POST['ids'])).') ' - .' OR autoresp_email_id IN ('.implode(',', db_input($_POST['ids'])).')'; - - list($depts)=db_fetch_row(db_query($sql)); - if($depts>0) { - $errors['err'] = __('One or more of the selected emails is being used by a department. Remove association first!'); - } elseif(!strcasecmp($_POST['a'], 'delete')) { + switch (strtolower($_POST['a'])) { + case 'delete': $i=0; foreach($_POST['ids'] as $k=>$v) { if($v!=$cfg->getDefaultEmailId() && ($e=Email::lookup($v)) && $e->delete()) @@ -73,8 +67,9 @@ if($_POST){ elseif(!$errors['err']) $errors['err'] = sprintf(__('Unable to delete %s'), _N('selected email', 'selected emails', $count)); + break; - } else { + default: $errors['err'] = __('Unknown action - get technical help.'); } } diff --git a/scp/js/ticket.js b/scp/js/ticket.js index 61ba8c7b84f9364b1863042dd32a4986dacbf598..b16cc8ea386dca8cf6931a9bd1e1b6b81aabeaf9 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -20,30 +20,29 @@ if (!this.$element.data('lockObjectId')) return; this.objectId = this.$element.data('lockObjectId'); - this.lockId = options.lockId || this.$element.data('lockId') || undefined; this.fails = 0; this.disabled = false; getConfig().then(function(c) { if (c.lock_time) - this.setup(); + this.setup(options.lockId || this.$element.data('lockId') || undefined); }.bind(this)); } Lock.prototype = { constructor: Lock, + registry: [], - setup: function() { + setup: function(lockId) { // When something inside changes or is clicked which requires a lock, // attempt to fetch one (lazily) $(':input', this.$element).on('keyup, change', this.acquire.bind(this)); $(':submit', this.$element).click(this.ensureLocked.bind(this)); - $(document).on('pjax:start', this.shutdown.bind(this)); // If lock already held, assume full time of lock remains, but warn // user about pending expiration - if (this.lockId) { + if (lockId) { getConfig().then(function(c) { - this.lockTimeout(c.lock_time - 20); + this.update({id: lockId, time: c.lock_time - 10}); }.bind(this)); } }, @@ -126,12 +125,20 @@ type: 'POST', url: 'ajax.php/lock/{0}/release'.replace('{0}', this.lockId), data: 'delete', - async: false, cache: false, + success: this.clearAll.bind(this), complete: this.destroy.bind(this) }); }, + clearAll: function() { + // Clear all other current locks with the same ID as this + $.each(Lock.prototype.registry, function(i, l) { + if (l.lockId && l.lockId == this.lockId) + l.shutdown(); + }.bind(this)); + }, + shutdown: function() { clearTimeout(this.warning); clearTimeout(this.retryTimer); @@ -159,6 +166,7 @@ // Set up release on away navigation $(document).off('.exclusive'); $(document).on('pjax:click.exclusive', $.proxy(this.release, this)); + Lock.prototype.registry.push(this); } this.lockId = lock.id; diff --git a/scp/tickets.php b/scp/tickets.php index e4c1fc10345bcd549ed8ca1e2fbbcbfa862f17aa..b8fc7ae449b7a25c9f72e46c2e0231f491872364 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -25,6 +25,7 @@ require_once(INCLUDE_DIR.'class.export.php'); // For paper sizes $page=''; $ticket = $user = null; //clean start. +$redirect = false; //LOCKDOWN...See if the id provided is actually valid and if the user has access. if($_REQUEST['id']) { if(!($ticket=Ticket::lookup($_REQUEST['id']))) @@ -131,6 +132,7 @@ if($_POST && !$errors): // Go back to the ticket listing page on reply $ticket = null; + $redirect = 'tickets.php'; } elseif(!$errors['err']) { $errors['err']=__('Unable to post the reply. Correct the errors below and try again!'); @@ -173,6 +175,7 @@ if($_POST && !$errors): Draft::deleteForNamespace('ticket.note.'.$ticket->getId(), $thisstaff->getId()); + $redirect = 'tickets.php'; } else { if(!$errors['err']) @@ -187,6 +190,7 @@ if($_POST && !$errors): $errors['err']=__('Permission Denied. You are not allowed to edit tickets'); elseif($ticket->update($_POST,$errors)) { $msg=__('Ticket updated successfully'); + $redirect = 'tickets.php?id='.$ticket->getId(); $_REQUEST['a'] = null; //Clear edit action - going back to view. //Check to make sure the staff STILL has access post-update (e.g dept change). if(!$ticket->checkStaffPerm($thisstaff)) @@ -335,6 +339,12 @@ if($_POST && !$errors): $thisstaff ->resetStats(); //We'll need to reflect any changes just made! endif; +if ($redirect) { + if ($msg) + Messages::success($msg); + Http::redirect($redirect); +} + /*... Quick stats ...*/ $stats= $thisstaff->getTicketsStats();