diff --git a/include/class.client.php b/include/class.client.php index 1e82ebe411a214ecbc1e8f94dd33f2e5c120d836..83684cae3e51155a81f6c39a4ae95821429e399f 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -321,6 +321,7 @@ class EndUser extends BaseAuthenticatedUser { } private function getStats() { + global $cfg; $basic = Ticket::objects() ->annotate(array('count' => SqlAggregate::COUNT('ticket_id'))) ->values('status__state', 'topic_id') @@ -338,10 +339,11 @@ class EndUser extends BaseAuthenticatedUser { // one index. Therefore, to scan two indexes (by user_id and // thread.collaborators.user_id), we need two queries. A union will // help out with that. - $mine->union($collab->filter(array( - 'thread__collaborators__user_id' => $this->getId(), - Q::not(array('user_id' => $this->getId())) - ))); + if ($cfg->collaboratorTicketsVisibility()) + $mine->union($collab->filter(array( + 'thread__collaborators__user_id' => $this->getId(), + Q::not(array('user_id' => $this->getId())) + ))); if ($orgid = $this->getOrgId()) { // Also generate a separate query for all the tickets owned by diff --git a/include/class.config.php b/include/class.config.php index 8cd3012e74920e916c7a45d518d7fc903cd153cc..41ee9a88779583742ecb9e977143a068b09a6ab6 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -197,6 +197,7 @@ class OsticketConfig extends Config { 'agent_name_format' => 'full', # First Last 'client_name_format' => 'original', # As entered 'auto_claim_tickets'=> true, + 'collaborator_ticket_visibility' => true, 'system_language' => 'en_US', 'default_storage_bk' => 'D', 'message_autoresponder_collabs' => true, @@ -919,6 +920,10 @@ class OsticketConfig extends Config { return $this->get('auto_claim_tickets'); } + function collaboratorTicketsVisibility() { + return $this->get('collaborator_ticket_visibility'); + } + function getDefaultTicketQueueId() { return $this->get('default_ticket_queue'); } @@ -1271,6 +1276,7 @@ class OsticketConfig extends Config { 'max_open_tickets'=>$vars['max_open_tickets'], 'enable_captcha'=>isset($vars['enable_captcha'])?1:0, 'auto_claim_tickets'=>isset($vars['auto_claim_tickets'])?1:0, + 'collaborator_ticket_visibility'=>isset($vars['collaborator_ticket_visibility'])?1:0, 'show_related_tickets'=>isset($vars['show_related_tickets'])?1:0, 'allow_client_updates'=>isset($vars['allow_client_updates'])?1:0, 'ticket_lock' => $vars['ticket_lock'], diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 7644cbb7eaee141a20de00e0ae705336ff3770ed..18f42a58bd4061612e2a30598e66718adde047c2 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -335,23 +335,31 @@ class MailFetcher { $header['system_emails'] = array(); $header['recipients'] = array(); + $header['thread_entry_recipients'] = array(); foreach($tolist as $source => $list) { foreach($list as $addr) { if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) { //Skip virtual Delivered-To addresses if ($source == 'delivered-to') continue; + $name = $this->mime_decode(@$addr->personal); + $email = strtolower($addr->mailbox).'@'.$addr->host; $header['recipients'][] = array( 'source' => sprintf(_S("Email (%s)"),$source), - 'name' => $this->mime_decode(@$addr->personal), - 'email' => strtolower($addr->mailbox).'@'.$addr->host); + 'name' => $name, + 'email' => $email); + + $header['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email); } elseif ($emailId) { $header['system_emails'][] = $emailId; + $system_email = Email::lookup($emailId); + $header['thread_entry_recipients']['to'][] = (string) $system_email; if (!$header['emailId']) $header['emailId'] = $emailId; } } } + $header['thread_entry_recipients']['to'] = array_unique($header['thread_entry_recipients']['to']); //See if any of the recipients is a delivered to address if ($tolist['delivered-to']) { diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 58eb2a984e157f17ba0a0d87b3b295aedfeb420e..7d474c411f3ad9da889408f396e7c2fb9b617f9f 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -648,23 +648,31 @@ class EmailDataParser { $tolist['delivered-to'] = $dt; $data['system_emails'] = array(); + $data['thread_entry_recipients'] = array(); foreach ($tolist as $source => $list) { foreach($list as $addr) { if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) { //Skip virtual Delivered-To addresses if ($source == 'delivered-to') continue; + $name = $this->mime_decode(@$addr->personal); + $email = strtolower($addr->mailbox).'@'.$addr->host; $data['recipients'][] = array( 'source' => sprintf(_S("Email (%s)"), $source), - 'name' => trim(@$addr->personal, '"'), - 'email' => strtolower($addr->mailbox).'@'.$addr->host); + 'name' => $name, + 'email' => $email); + + $data['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email); } elseif ($emailId) { $data['system_emails'][] = $emailId; + $system_email = Email::lookup($emailId); + $data['thread_entry_recipients']['to'][] = (string) $system_email; if (!$data['emailId']) $data['emailId'] = $emailId; } } } + $data['thread_entry_recipients']['to'] = array_unique($data['thread_entry_recipients']['to']); /* * In the event that the mail was delivered to the system although none of the system diff --git a/include/class.thread.php b/include/class.thread.php index f658a25ba762f63b537ebd48e96dd9f569158311..f8455faf51dd6a402f9ab2e1442f13d0f7ec6088 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -428,6 +428,7 @@ implements Searchable { 'ip' => '', 'reply_to' => $entry, 'recipients' => $mailinfo['recipients'], + 'thread_entry_recipients' => $mailinfo['thread_entry_recipients'], 'to-email-id' => $mailinfo['to-email-id'], 'autorespond' => !isset($mailinfo['passive']), ); @@ -1504,8 +1505,19 @@ implements TemplateVariable { )); //add recipients to thread entry - if ($vars['recipients']) - $entry->recipients = json_encode($vars['recipients']); + if ($vars['thread_entry_recipients']) { + $count = 0; + foreach ($vars['thread_entry_recipients'] as $key => $value) + $count = $count + count($value); + + if ($count > 1) + $entry->flags |= ThreadEntry::FLAG_REPLY_ALL; + else + $entry->flags |= ThreadEntry::FLAG_REPLY_USER; + + $entry->recipients = json_encode($vars['thread_entry_recipients']); + } + if (Collaborator::getIdByUserId($vars['userId'], $vars['threadId'])) $entry->flags |= ThreadEntry::FLAG_COLLABORATOR; @@ -2061,12 +2073,15 @@ class ThreadEvents extends InstrumentedList { function log($object, $state, $data=null, $user=null, $annul=null) { global $thisstaff, $thisclient; - if ($object instanceof Ticket) + if ($object && ($object instanceof Ticket)) // TODO: Use $object->createEvent() (nolint) $event = ThreadEvent::forTicket($object, $state, $user); - elseif ($object instanceof Task) + elseif ($object && ($object instanceof Task)) $event = ThreadEvent::forTask($object, $state, $user); + if (is_null($event)) + return; + # Annul previous entries if requested (for instance, reopening a # ticket will annul an 'closed' entry). This will be useful to # easily prevent repeated statistics. @@ -2218,7 +2233,7 @@ class CollaboratorEvent extends ThreadEvent { } $desc = sprintf($base, implode(', ', $collabs)); break; - case isset($data['add']) && $mode!=self::MODE_CLIENT: + case isset($data['add']): $base = __('<b>{somebody}</b> added <strong>%s</strong> as collaborators {timestamp}'); $collabs = array(); if ($data['add']) { @@ -2861,17 +2876,10 @@ implements TemplateVariable { function addResponse($vars, &$errors) { $vars['threadId'] = $this->getId(); $vars['userId'] = 0; - $vars['pid'] = $this->getLastMessage()->getId(); + if ($message = $this->getLastMessage()) + $vars['pid'] = $message->getId(); $vars['flags'] = 0; - switch ($vars['reply-to']) { - case 'all': - $vars['flags'] |= ThreadEntry::FLAG_REPLY_ALL; - break; - case 'user': - $vars['flags'] |= ThreadEntry::FLAG_REPLY_USER; - break; - } if (!($resp = ResponseThreadEntry::add($vars, $errors))) return $resp; diff --git a/include/class.ticket.php b/include/class.ticket.php index bec6136bfee9b6ce72375cc15cca22f2db1b0400..c9811934a7189c9b979d7ca364c5016b6577fd5f 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1629,14 +1629,6 @@ implements RestrictedAccess, Threadable, Searchable { $poster = User::lookup($entry->user_id); $posterEmail = $poster->getEmail()->address; - $recipients = array(); - if($vars['ccs']) { - foreach ($vars['ccs'] as $cc) { - $collab = Collaborator::getIdByUserId($cc, $this->getThread()->getId()); - $recipients[] = Collaborator::lookup($collab); - } - } - $vars = array_merge($vars, array( 'message' => (string) $entry, 'poster' => $poster ?: _S('A collaborator'), @@ -1658,35 +1650,22 @@ implements RestrictedAccess, Threadable, Searchable { } } - $collaborators = array(); - $collabsCc = array(); - foreach ($recipients as $recipient) { - if(get_class($recipient) == 'Collaborator') { - if ($recipient->isCc()) - $collabsCc[] = $recipient->getEmail()->address; - } + foreach ($recipients as $key => $recipient) { + $recipient = $recipient->getContact(); if(get_class($recipient) == 'TicketOwner') - $owner = $recipient; - } + $owner = $recipient; - foreach ($collabsCc as $cc) { - if (in_array($cc, $skip)) - continue; - elseif ($cc != $posterEmail) - $collaborators[] = $cc; - } + if ((get_class($recipient) == 'Collaborator' ? $recipient->getUserId() : $recipient->getId()) == $entry->user_id) + unset($recipients[$key]); + } - //the ticket user is a recipient + //see if the ticket user is a recipient if ($owner->getEmail()->address != $poster->getEmail()->address && !in_array($owner->getEmail()->address, $skip)) $owner_recip = $owner->getEmail()->address; - $collaborators['cc'] = $collaborators; - - //collaborator email sent out - if ($collaborators['cc'] || $owner_recip) { - //say dear collaborator if the ticket user is not a recipient - if (!$owner_recip) { + //say dear collaborator if the ticket user is not a recipient + if (!$owner_recip) { $nameFormats = array_keys(PersonsName::allFormats()); $names = array(); foreach ($nameFormats as $key => $value) { @@ -1694,16 +1673,13 @@ implements RestrictedAccess, Threadable, Searchable { } $names = array_merge($names, array('recipient' => $recipient)); $cnotice = $this->replaceVars($msg, $names); - } - - //otherwise address email to ticket user - else + } + //otherwise address email to ticket user + else $cnotice = $this->replaceVars($msg, array('recipient' => $owner)); - //if the ticket user is a recipient, put them in to address otherwise, cc all recipients - $email->send($owner_recip ? $owner_recip : '', $cnotice['subj'], $cnotice['body'], $attachments, - $options, $collaborators); - } + $email->send($recipients, $cnotice['subj'], $cnotice['body'], $attachments, + $options); } function onMessage($message, $autorespond=true, $reopen=true) { @@ -2691,6 +2667,26 @@ implements RestrictedAccess, Threadable, Searchable { if ($vars['userId'] == $this->user_id) $isMsg = true; + // Get active recipients of the response + // Initial Message from Tickets created by Agent + if ($vars['reply-to']) + $recipients = $this->getRecipients($vars['reply-to'], $vars['ccs']); + // Messages from Web Portal + elseif (strcasecmp($origin, 'email')) { + $recipients = $this->getRecipients('all'); + foreach ($recipients as $key => $recipient) { + if (!$recipientContact = $recipient->getContact()) + continue; + + $userId = $recipientContact->getUserId() ?: $recipientContact->getId(); + // Do not list the poster as a recipient + if ($userId == $vars['userId']) + unset($recipients[$key]); + } + } + if ($recipients && $recipients instanceof MailingList) + $vars['thread_entry_recipients'] = $recipients->getEmailAddresses(); + if (!($message = $this->getThread()->addMessage($vars, $errors))) return null; @@ -2747,7 +2743,9 @@ implements RestrictedAccess, Threadable, Searchable { $this->onMessage($message, ($autorespond && $alerts), $reopen); //must be called b4 sending alerts to staff. - if ($autorespond && $alerts && $cfg && $cfg->notifyCollabsONNewMessage()) { + if ($autorespond && $alerts + && $cfg && $cfg->notifyCollabsONNewMessage() + && strcasecmp($origin, 'email')) { //when user replies, this is where collabs notified $this->notifyCollaborators($message, array('signature' => '')); } @@ -2902,14 +2900,14 @@ implements RestrictedAccess, Threadable, Searchable { if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; - // Add new collaboratorss (if any). + // Add new collaborators (if any). if (isset($vars['ccs']) && count($vars['ccs'])) - $this->addCollaborators($vars['ccs']); + $this->addCollaborators($vars['ccs'], array(), $errors); // Get active recipients of the response $recipients = $this->getRecipients($vars['reply-to'], $vars['ccs']); if ($recipients instanceof MailingList) - $vars['recipients'] = $recipients->getEmailAddresses(); + $vars['thread_entry_recipients'] = $recipients->getEmailAddresses(); if (!($response = $this->getThread()->addResponse($vars, $errors))) return null; @@ -3845,6 +3843,10 @@ implements RestrictedAccess, Threadable, Searchable { // Start tracking ticket lifecycle events (created should come first!) $ticket->logEvent('created', null, $thisstaff ?: $user); + // Add collaborators (if any) + if (isset($vars['ccs']) && count($vars['ccs'])) + $ticket->addCollaborators($vars['ccs'], array(), $errors); + // Add organizational collaborators if ($org && $org->autoAddCollabs()) { $pris = $org->autoAddPrimaryContactsAsCollabs(); @@ -4068,10 +4070,6 @@ implements RestrictedAccess, Threadable, Searchable { // Effective role for the department $role = $ticket->getRole($thisstaff); - // Add collaborators (if any) - if (isset($vars['ccs']) && count($vars['ccs'])) - $ticket->addCollaborators($vars['ccs'], array(), $errors); - $alert = strcasecmp('none', $vars['reply-to']); // post response - if any $response = null; diff --git a/include/class.util.php b/include/class.util.php index 962ef6f7a33efd342c3365b7c723d520ef9bc4fc..a3bc2305d130c8b7bf5885cc4df99f4dcf1cae22 100644 --- a/include/class.util.php +++ b/include/class.util.php @@ -20,6 +20,10 @@ implements EmailContact { $this->type = $type; } + function getContact() { + return $this->contact; + } + function getId() { return $this->contact->getId(); } diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 92b131690ce0d877012568fa01e0a7234a79d4ac..9e678e31cbaa4a2ff9fec9d63fcb38ed172540b9 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -77,7 +77,11 @@ if ($settings['status']) // unique values $visibility = $basic_filter->copy() ->values_flat('ticket_id') - ->filter(array('user_id' => $thisclient->getId())) + ->filter(array('user_id' => $thisclient->getId())); + +// Add visibility of Tickets where the User is a Collaborator if enabled +if ($cfg->collaboratorTicketsVisibility()) + $visibility = $visibility ->union($basic_filter->copy() ->values_flat('ticket_id') ->filter(array('thread__collaborators__user_id' => $thisclient->getId())) diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml index 32783896b5ee0083d2ea481d79a851c787fc0d3a..4d1f67f410f8f2561a68d39affc5d88e93a91559 100644 --- a/include/i18n/en_US/config.yaml +++ b/include/i18n/en_US/config.yaml @@ -67,6 +67,7 @@ core: assigned_alert_team_lead: 0 assigned_alert_team_members: 0 auto_claim_tickets: 1 + collaborator_ticket_visibility: 1 show_related_tickets: 1 show_assigned_tickets: 1 show_answered_tickets: 0 diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml index b13bd17c4f3bc9b2739ddb52f02c69c2d3ca497a..7d4a72fd2981ca81347d052670c93e470adac79e 100644 --- a/include/i18n/en_US/help/tips/settings.ticket.yaml +++ b/include/i18n/en_US/help/tips/settings.ticket.yaml @@ -94,6 +94,15 @@ claim_tickets: <br><br> Reopened tickets are always assigned to the last respondent. +collaborator_ticket_visibility: + title: Collaborator Tickets Visibility + content: > + If Enabled, Users will have visibility to ALL Tickets they participate in + when signing into the Web Portal. + <br><br> + If Disabled, Users will only be able to see their own Tickets + when signing into the Web Portal. + assigned_tickets: title: Assigned Tickets content: > diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index dd259ea8a1e350c30344ac528e42a3453d423de8..210b6d3f319dd33f9bb6bc20e3b8dca22348b2c5 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -206,6 +206,13 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <?php echo __('Enable'); ?> <i class="help-tip icon-question-sign" href="#claim_tickets"></i> </td> </tr> + <tr> + <td><?php echo __('Collaborator Tickets Visibility'); ?>:</td> + <td> + <input type="checkbox" name="collaborator_ticket_visibility" <?php echo $config['collaborator_ticket_visibility']?'checked="checked"':''; ?>> + <?php echo __('Enable'); ?> <i class="help-tip icon-question-sign" href="#collaborator_ticket_visibility"></i> + </td> + </tr> <tr> <th colspan="2"> <em><b><?php echo __('Attachments');?></b>: <?php echo __('Size and maximum uploads setting mainly apply to web tickets.');?></em> diff --git a/include/staff/templates/thread-email-recipients.tmpl.php b/include/staff/templates/thread-email-recipients.tmpl.php index 3bae51c02fab5ae3b20fc24c1c7e22f78427a8c2..6935c1fa873ec8cad13273bc580a05f960147b49 100644 --- a/include/staff/templates/thread-email-recipients.tmpl.php +++ b/include/staff/templates/thread-email-recipients.tmpl.php @@ -11,8 +11,8 @@ if (!$_REQUEST['mode']) { ?> $recipients = Format::htmlchars($recipients); foreach ($recipients as $k => $v) { echo sprintf('<tr><td nowrap width="5" valign="top"><b>%s</b>:</td><td>%s</td></tr>', - ucfirst($k), - implode('<br>', $v) + ucfirst($k), + is_array($v) ? implode('<br>', $v) : $v ); } ?> diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index 3407e534b966dd6d9a588e45367e70a3731c789f..f45f20b028afdd17fd5b6b16fc11e84042c14bbf 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -55,10 +55,10 @@ if ($user && $cfg->isAvatarsEnabled()) <span class="label label-bare"><?php echo __('Resent'); ?></span> <?php } if ($entry->flags & ThreadEntry::FLAG_REPLY_ALL) { ?> - <span class="label label-bare"><?php echo __('Reply All'); ?></span> + <span class="label label-bare"><i class="icon-group"></i></span> <?php } if ($entry->flags & ThreadEntry::FLAG_REPLY_USER) { ?> - <span class="label label-bare"><?php echo __('Reply to User'); ?></span> + <span class="label label-bare"><i class="icon-user"></i></span> <?php } if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR && $entry->type == 'M') { ?> <span class="label label-bare"><?php echo __('Cc Collaborator'); ?></span> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index ef7aff8e979fca11da68859188131edfba87c29e..c1747a63f2125fa75e7438ca67d500b83cf76426 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -770,8 +770,8 @@ if ($errors['err'] && isset($_POST['a'])) { <td> <div style="margin-bottom:2px;"> <?php - echo sprintf('<span><a id="show_ccs" - class="icon-caret-right"></i> %s </a> + echo sprintf('<span><a id="show_ccs"> + <i id="arrow-icon" class="icon-caret-right"></i> %s </a> <a class="manage-collaborators collaborators preview noclick %s" @@ -1265,7 +1265,7 @@ $(function() { }); $('#show_ccs').click(function() { - var show = $(this); + var show = $('#arrow-icon'); var collabs = $('a#managecollabs'); $('#ccs').slideToggle('fast', function(){ if ($(this).is(":hidden")) { @@ -1286,12 +1286,12 @@ $(function() { $('#collabselection').select2({ width: '350px', allowClear: true, - sorter: (data) => { + sorter: function(data) { return data.filter(function (item) { return !item.selected; }); }, - templateResult: (e) => { + templateResult: function(e) { var $e = $( '<span><i class="icon-user"></i> ' + e.text + '</span>' );