diff --git a/bootstrap.php b/bootstrap.php index c37e3f6f04d6080f31e1c4ed15f0e4313e6bb2fd..d91679af855da07e45a6f0b94e3d18b8b7fb859e 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -89,6 +89,7 @@ class Bootstrap { define('TICKET_LOCK_TABLE',$prefix.'ticket_lock'); define('TICKET_EVENT_TABLE',$prefix.'ticket_event'); define('TICKET_EMAIL_INFO_TABLE',$prefix.'ticket_email_info'); + define('TICKET_COLLABORATOR_TABLE', $prefix.'ticket_collaborator'); define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE); diff --git a/include/ajax.forms.php b/include/ajax.forms.php index cdd643a1b44f77d4e30c51bf3cd67fc5886acd77..2142e8e4faa947b5060539e79a8ef9d2b535025a 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -36,7 +36,5 @@ class DynamicFormsAjaxAPI extends AjaxController { else $field->save(); } - } - ?> diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index aa4d99129a6f7d41e2dfc762384c3bf3bd60fa3c..93e90758c2963eebcefa43518b757e5c473d8eef 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -449,6 +449,144 @@ class TicketsAjaxAPI extends AjaxController { return $resp; } + //Collaborators utils + function addCollaborator($tid) { + global $thisstaff; + + if (!($ticket=Ticket::lookup($tid)) + || !$ticket->checkStaffAccess($thisstaff)) + Http::response(404, 'No such ticket'); + + //If not a post then assume new collaborator form + if(!$_POST) + return self::_addcollaborator($ticket); + + $user = $form = null; + if (isset($_POST['id']) && $_POST['id']) { //Existing user/ + $user = User::lookup($_POST['id']); + } else { //We're creating a new user! + $form = UserForm::getUserForm()->getForm($_POST); + $user = User::fromForm($form); + } + + $errors = $info = array(); + if ($user && ($c=$ticket->addCollaborator($user, $errors))) { + $info =array('msg' => sprintf('%s added as a collaborator', + $c->getName())); + + return self::_collaborators($ticket, $info); + } + + if($errors && $errors['err']) { + $info +=array('error' => $errors['err']); + } else { + $info +=array('error' =>'Unable to add collaborator - try again'); + } + + + return self::_addcollaborator($ticket, $user, $form, $info); + } + + function updateCollaborator($cid) { + global $thisstaff; + + if(!($c=Collaborator::lookup($cid)) + || !($user=$c->getUser()) + || !($ticket=$c->getTicket()) + || !$ticket->checkStaffAccess($thisstaff) + ) + Http::response(404, 'Unknown collaborator'); + + $errors = array(); + if(!$user->updateInfo($_POST, $errors)) + return self::_collaborator($c ,$user->getForms($_POST), $errors); + + $info = array('msg' => sprintf('%s updated successfully', + $c->getName())); + + return self::_collaborators($ticket, $info); + } + + function viewCollaborator($cid) { + global $thisstaff; + + if(!($collaborator=Collaborator::lookup($cid)) + || !($ticket=$collaborator->getTicket()) + || !$ticket->checkStaffAccess($thisstaff)) + Http::response(404, 'Unknown collaborator'); + + return self::_collaborator($collaborator); + } + + function showCollaborators($tid) { + global $thisstaff; + + if(!($ticket=Ticket::lookup($tid)) + || !$ticket->checkStaffAccess($thisstaff)) + Http::response(404, 'No such ticket'); + + if($ticket->getCollaborators()) + return self::_collaborators($ticket); + + return self::_addcollaborator($ticket); + } + + + + function _addcollaborator($ticket, $user=null, $form=null, $info=array()) { + + $info += array( + 'title' => sprintf('Ticket #%s: Add a collaborator', $ticket->getNumber()), + 'action' => sprintf('#tickets/%d/add-collaborator', $ticket->getId()) + ); + + return self::_userlookup($user, $form, $info); + } + + + function updateCollaborators($tid) { + global $thisstaff; + + if(!($ticket=Ticket::lookup($tid)) + || !$ticket->checkStaffAccess($thisstaff)) + Http::response(404, 'No such ticket'); + + $errors = $info = array(); + if ($ticket->updateCollaborators($_POST, $errors)) { + $info +=array('msg' => 'Collaborators updated successfully'); + } elseif($errors && $errors['err']) { + $info +=array('error' => $errors['err']); + } + + return self::_collaborators($ticket, $info); + } + + + + function _collaborator($collaborator, $form=null, $info=array()) { + + $info += array('action' => '#collaborators/'.$collaborator->getId()); + + $user = $collaborator->getUser(); + + ob_start(); + include(STAFFINC_DIR . 'templates/user.tmpl.php'); + $resp = ob_get_contents(); + ob_end_clean(); + + return $resp; + } + + function _collaborators($ticket, $info=array()) { + + ob_start(); + include(STAFFINC_DIR . 'templates/collaborators.tmpl.php'); + $resp = ob_get_contents(); + ob_end_clean(); + + return $resp; + } + function viewUser($tid) { global $thisstaff; @@ -516,6 +654,11 @@ class TicketsAjaxAPI extends AjaxController { 'title' => sprintf('Change user for ticket #%s', $ticket->getNumber()) ); + return self::_userlookup($user, $info); + } + + function _userlookup($user, $form, $info) { + ob_start(); include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php'); $resp = ob_get_contents(); @@ -524,6 +667,5 @@ class TicketsAjaxAPI extends AjaxController { } - } ?> diff --git a/include/ajax.users.php b/include/ajax.users.php index 04b29bec611ee3b19d167645590950b4d3cd40ab..a1096d6f5604f3f749311de45fb0b8ca5f92b343 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -66,22 +66,10 @@ class UsersAjaxAPI extends AjaxController { function addUser() { - $valid = true; $form = UserForm::getUserForm()->getForm($_POST); - if (!$form->isValid()) - $valid = false; - - if (($field=$form->getField('email')) - && $field->getClean() - && User::lookup(array('emails__address'=>$field->getClean()))) { - $field->addError('Email is assigned to another user'); - $valid = false; - } - - if ($valid && ($user = User::fromForm($form->getClean()))) + if (($user = User::fromForm($form))) Http::response(201, $user->to_json()); - $info = array('error' =>'Error adding user - try again!'); return self::_lookupform($form, $info); diff --git a/include/api.tickets.php b/include/api.tickets.php index f0758d5d2ea75324620ce12acbf30fe76222fb06..45cbc2db97eccb120b0f22f7f47ba97ec8026ced 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -39,7 +39,10 @@ class TicketApiController extends ApiController { if(!strcasecmp($format, 'email')) { $supported = array_merge($supported, array('header', 'mid', 'emailId', 'ticketId', 'reply-to', 'reply-to-name', - 'in-reply-to', 'references')); + 'in-reply-to', 'references', + 'recipients' => array("*" => array("name", "email")) + )); + $supported['attachments']['*'][] = 'cid'; } diff --git a/include/class.collaborator.php b/include/class.collaborator.php new file mode 100644 index 0000000000000000000000000000000000000000..e04d8571226f539cf82546a0c641a87eb1a026e3 --- /dev/null +++ b/include/class.collaborator.php @@ -0,0 +1,151 @@ +<?php +/********************************************************************* + class.collaborator.php + + Ticket collaborator + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2013 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: +**********************************************************************/ +require_once(INCLUDE_DIR . 'class.user.php'); + +class Collaborator { + + var $ht; + + var $user; + var $ticket; + + function __construct($id) { + + $this->load($id); + } + + function load($id) { + + if(!$id && !($id=$this->getId())) + return; + + $sql='SELECT * FROM '.TICKET_COLLABORATOR_TABLE + .' WHERE id='.db_input($id); + + $this->ht = db_fetch_array(db_query($sql)); + $this->ticket = $this->user = null; + } + + function reload() { + return $this->load(); + } + + function __call($name, $args) { + + if(!($user=$this->getUser()) || !method_exists($user, $name)) + return null; + + if($args) + return call_user_func_array(array($user, $name), $args); + + return call_user_func(array($user, $name)); + } + + function getId() { + return $this->ht['id']; + } + + function isActive() { + return ($this->ht['isactive']); + } + + function getTicketId() { + return $this->ht['ticket_id']; + } + + function getTicket() { + if(!$this->ticket && $this->getTicketId()) + $this->ticket = Ticket::lookup($this->getTicketId()); + + return $this->ticket; + } + + function getUserId() { + return $this->ht['user_id']; + } + + function getUser() { + + if(!$this->user && $this->getUserId()) + $this->user = User::lookup($this->getUserId()); + + return $this->user; + } + + static function add($info, &$errors) { + + if(!$info || !$info['ticketId'] || !$info['userId']) + $errors['err'] = 'Invalid or missing information'; + elseif(($c=self::lookup($info))) + $errors['err'] = sprintf('%s is already a collaborator', + $c->getName()); + + if($errors) return false; + + $sql='INSERT INTO '.TICKET_COLLABORATOR_TABLE + .' SET isactive=1, updated=NOW() ' + .' ,ticket_id='.db_input($info['ticketId']) + .' ,user_id='.db_input($info['userId']); + + if(db_query($sql) && ($id=db_insert_id())) + return self::lookup($id); + + $errors['err'] = 'Unable to add collaborator. Internal error'; + + return false; + } + + static function forTicket($tid, $criteria=array()) { + + $collaborators = array(); + + $sql='SELECT id FROM '.TICKET_COLLABORATOR_TABLE + .' WHERE ticket_id='.db_input($tid); + + if(isset($criteria['isactive'])) + $sql.=' AND isactive='.db_input($criteria['isactive']); + + //TODO: sort by name of the user + + if(($res=db_query($sql)) && db_num_rows($res)) + while(list($id)=db_fetch_row($res)) + $collaborators[] = self::lookup($id); + + return $collaborators; + } + + static function getIdByInfo($info) { + + $sql='SELECT id FROM '.TICKET_COLLABORATOR_TABLE + .' WHERE ticket_id='.db_input($info['ticketId']) + .' AND user_id='.db_input($info['userId']); + + list($id) = db_fetch_row(db_query($sql)); + + return $id; + } + + static function lookup($criteria) { + $id = is_numeric($criteria) + ? $criteria : self::getIdByInfo($criteria); + + return ($id + && ($c = new Collaborator($id)) + && $c->getId() == $id) + ? $c : null; + } +} +?> diff --git a/include/class.config.php b/include/class.config.php index ca426bd2c4661f8f417f961cbebc884f3d977cfd..ac60110e8f2ad06423f2fd551ac109247b8e425d 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -351,14 +351,13 @@ class OsticketConfig extends Config { function getDefaultEmail() { if(!$this->defaultEmail && $this->getDefaultEmailId()) - $this->defaultEmail=Email::lookup($this->getDefaultEmailId()); + $this->defaultEmail = Email::lookup($this->getDefaultEmailId()); return $this->defaultEmail; } function getDefaultEmailAddress() { - $email=$this->getDefaultEmail(); - return $email?$email->getAddress():null; + return ($email=$this->getDefaultEmail()) ? $email->getAddress() : null; } function getDefaultSLAId() { @@ -368,7 +367,7 @@ class OsticketConfig extends Config { function getDefaultSLA() { if(!$this->defaultSLA && $this->getDefaultSLAId()) - $this->defaultSLA=SLA::lookup($this->getDefaultSLAId()); + $this->defaultSLA = SLA::lookup($this->getDefaultSLAId()); return $this->defaultSLA; } @@ -379,15 +378,18 @@ class OsticketConfig extends Config { function getAlertEmail() { - if(!$this->alertEmail && $this->get('alert_email_id')) - $this->alertEmail= new Email($this->get('alert_email_id')); + if(!$this->alertEmail) + if(!($this->alertEmail = Email::lookup($this->getAlertEmailId()))) + $this->alertEmail = $this->getDefaultEmail(); + return $this->alertEmail; } function getDefaultSMTPEmail() { if(!$this->defaultSMTPEmail && $this->get('default_smtp_id')) - $this->defaultSMTPEmail= new Email($this->get('default_smtp_id')); + $this->defaultSMTPEmail = Email::lookup($this->get('default_smtp_id')); + return $this->defaultSMTPEmail; } diff --git a/include/class.dept.php b/include/class.dept.php index 8f1542b737d631a07024a6303dd80516963c5f28..0822e056f1c9890e71694bfc0ee129fab7798b16 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -77,9 +77,11 @@ class Dept { } function getEmail() { + global $cfg; - if(!$this->email && $this->getEmailId()) - $this->email=Email::lookup($this->getEmailId()); + if(!$this->email) + if(!($this->email = Email::lookup($this->getEmailId()))) + $this->email = $cfg->getDefaultEmail(); return $this->email; } @@ -138,19 +140,21 @@ class Dept { } function getTemplate() { + global $cfg; - if(!$this->template && $this->getTemplateId()) - $this->template = EmailTemplateGroup::lookup($this->getTemplateId()); + if (!$this->template) { + if (!($this->template = EmailTemplateGroup::lookup($this->getTemplateId()))) + $this->template = $cfg->getDefaultTemplate(); + } return $this->template; } function getAutoRespEmail() { - if(!$this->autorespEmail && $this->ht['autoresp_email_id'] && ($email=Email::lookup($this->ht['autoresp_email_id']))) - $this->autorespEmail=$email; - else // Defualt to dept email if autoresp is not specified or deleted. - $this->autorespEmail=$this->getEmail(); + if (!$this->autorespEmail && $this->ht['autoresp_email_id']) + if (!($this->autorespEmail = Email::lookup($this->ht['autoresp_email_id']))) + $this->autorespEmail = $this->getEmail(); return $this->autorespEmail; } diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 3c4306d1eeffe17cb65a2b2207f18be1312d6f11..2b05c3a593fbaef7620f5259bc1728f065834153 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -269,9 +269,9 @@ class MailFetcher { $sender=$headerinfo->from[0]; //Just what we need... - $header=array('name' =>@$sender->personal, + $header=array('name' => $this->mime_decode(@$sender->personal), 'email' => trim(strtolower($sender->mailbox).'@'.$sender->host), - 'subject'=>@$headerinfo->subject, + 'subject'=> $this->mime_decode(@$headerinfo->subject), 'mid' => trim(@$headerinfo->message_id), 'header' => $this->getHeader($mid), 'in-reply-to' => $headerinfo->in_reply_to, @@ -283,22 +283,33 @@ class MailFetcher { $header['reply-to-name'] = $replyto[0]->personal; } - //Try to determine target email - useful when fetched inbox has - // aliases that are independent emails within osTicket. - $emailId = 0; + // Put together a list of recipients $tolist = array(); if($headerinfo->to) $tolist = array_merge($tolist, $headerinfo->to); if($headerinfo->cc) $tolist = array_merge($tolist, $headerinfo->cc); - if($headerinfo->bcc) - $tolist = array_merge($tolist, $headerinfo->bcc); - foreach($tolist as $addr) - if(($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) - break; + $header['recipients'] = array(); + foreach($tolist as $addr) { + if(!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) { + $header['recipients'][] = array( + 'name' => $this->mime_decode(@$addr->personal), + 'email' => strtolower($addr->mailbox).'@'.$addr->host); + } elseif(!$header['emailId']) { + $header['emailId'] = $emailId; + } + } - $header['emailId'] = $emailId; + //BCCed? + if(!$header['emailId']) { + unset($header['recipients']); //Nuke the recipients - we were bcced + if ($headerinfo->bcc) { + foreach($headerinfo->bcc as $addr) + if (($header['emailId'] = Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) + break; + } + } // Ensure we have a message-id. If unable to read it out of the // email, use the hash of the entire email headers @@ -485,18 +496,18 @@ class MailFetcher { } $vars = $mailinfo; - $vars['name']=$this->mime_decode($mailinfo['name']); - $vars['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; - $vars['message']=Format::stripEmptyLines($this->getBody($mid)); - $vars['emailId']=$mailinfo['emailId']?$mailinfo['emailId']:$this->getEmailId(); + $vars['name'] = $mailinfo['name']; + $vars['subject'] = $mailinfo['subject'] ? $mailinfo['subject'] : '[No Subject]'; + $vars['message'] = Format::stripEmptyLines($this->getBody($mid)); + $vars['emailId'] = $mailinfo['emailId'] ? $mailinfo['emailId'] : $this->getEmailId(); //Missing FROM name - use email address. if(!$vars['name']) - $vars['name'] = $vars['email']; + list($vars['name']) = explode('@', $vars['email']); //An email with just attachments can have empty body. if(!$vars['message']) - $vars['message'] = '-'; + $vars['message'] = '--'; if($ost->getConfig()->useEmailPriority()) $vars['priorityId']=$this->getPriority($mid); @@ -565,6 +576,7 @@ class MailFetcher { return null; } + return $ticket; } diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 32b0083a6df2c077defd19ac1560627b20ab877c..dfc4c6b3f2932f2a97ca0b7234215b2ba70cbfca 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -154,6 +154,13 @@ class Mail_Parse { return Mail_Parse::parseAddressList($header); } + function getBccAddressList(){ + if (!($header = $this->struct->headers['bcc'])) + return null; + + return Mail_Parse::parseAddressList($header); + } + function getMessageId(){ return $this->struct->headers['message-id']; } @@ -378,19 +385,35 @@ class EmailDataParser { } //TO Address:Try to figure out the email address... associated with the incoming email. - $emailId = 0; - if(($tolist = $parser->getToAddressList())) { - foreach ($tolist as $toaddr) { - if(($emailId=Email::getIdByEmail($toaddr->mailbox.'@'.$toaddr->host))) - break; + $data['emailId'] = 0; + $data['recipients'] = array(); + $tolist = array(); + if(($to = $parser->getToAddressList())) + $tolist = array_merge($tolist, $to); + + if(($cc = $parser->getCcAddressList())) + $tolist = array_merge($tolist, $cc); + + foreach ($tolist as $addr) { + if(!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) { + $data['recipients'][] = array( + 'name' => trim(@$addr->personal, '"'), + 'email' => strtolower($addr->mailbox).'@'.$addr->host); + } elseif(!$data['emailId']) { + $data['emailId'] = $emailId; } } - //maybe we got CC'ed?? - if(!$emailId && ($cclist=$parser->getCcAddressList())) { - foreach ($cclist as $ccaddr) { - if(($emailId=Email::getIdByEmail($ccaddr->mailbox.'@'.$ccaddr->host))) - break; + + //maybe we got BCC'ed?? + if(!$data['emailId']) { + unset($data['recipients']); + $emailId = 0; + if($bcc = $parser->getBccAddressList()) + foreach ($bcc as $addr) { + if(($emailId=Email::getIdByEmail($addr->mailbox.'@'.$addr->host))) + break; } + $data['emailId'] = $emailId; } $data['subject'] = $parser->getSubject(); @@ -398,7 +421,6 @@ class EmailDataParser { $data['header'] = $parser->getHeader(); $data['mid'] = $parser->getMessageId(); $data['priorityId'] = $parser->getPriority(); - $data['emailId'] = $emailId; $data['in-reply-to'] = $parser->struct->headers['in-reply-to']; $data['references'] = $parser->struct->headers['references']; diff --git a/include/class.template.php b/include/class.template.php index a66a74f9ce3e28dcf9c9f6d48fafa90d7fdf3f45..e135034cd4943d45190d70b710e9868f4df6a43f 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -40,6 +40,9 @@ class EmailTemplateGroup { 'ticket.reply'=>array( 'name'=>'Response/Reply Template', 'desc'=>'Template used on ticket response/reply'), + 'ticket.activity.notice'=>array( + 'name'=>'New Activity Notice', + 'desc'=>'Template used to notify collaborators on ticket activity (e.g CC on reply)'), 'ticket.alert'=>array( 'name'=>'New Ticket Alert', 'desc'=>'Alert sent to staff, if enabled, on new ticket.'), @@ -206,6 +209,10 @@ class EmailTemplateGroup { return $this->getMsgTemplate('ticket.reply'); } + function getActivityNoticeMsgTemplate() { + return $this->getMsgTemplate('ticket.activity.notice'); + } + function getOverlimitMsgTemplate() { return $this->getMsgTemplate('ticket.overlimit'); } diff --git a/include/class.thread.php b/include/class.thread.php index 13c2e0dfe5ef1c1f4c463ed83daff8c0c3e53fe4..a68db4ddf94d6e51c573bccd90fa89f2c257a1d6 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -785,13 +785,17 @@ Class ThreadEntry { } $vars['body'] = Format::sanitize($vars['body']); + $poster = $vars['poster']; + if ($poster && is_object($poster)) + $poster = $poster->getName(); + $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' .' ,thread_type='.db_input($vars['type']) .' ,ticket_id='.db_input($vars['ticketId']) .' ,title='.db_input(Format::sanitize($vars['title'], true)) .' ,body='.db_input($vars['body']) .' ,staff_id='.db_input($vars['staffId']) - .' ,poster='.db_input($vars['poster']) + .' ,poster='.db_input($poster) .' ,source='.db_input($vars['source']); if(isset($vars['pid'])) diff --git a/include/class.ticket.php b/include/class.ticket.php index b8dbe21871cd18db35c9df11f7baa26e6722136b..648424ca61cd8d4f2df4856629745875f64cffbb 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -32,6 +32,8 @@ include_once(INCLUDE_DIR.'class.sla.php'); include_once(INCLUDE_DIR.'class.canned.php'); require_once(INCLUDE_DIR.'class.dynamic_forms.php'); require_once(INCLUDE_DIR.'class.user.php'); +require_once(INCLUDE_DIR.'class.collaborator.php'); + class Ticket { @@ -64,7 +66,7 @@ class Ticket { $sql='SELECT ticket.*, lock_id, dept_name ' .' ,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate ' - .' ,count(attach.attach_id) as attachments ' + .' ,count(attach.attach_id) as attachments, count(collab.id) as collaborators ' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) ' @@ -72,6 +74,8 @@ class Ticket { .'ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (' .'ticket.ticket_id=attach.ticket_id) ' + .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab ON (' + .'ticket.ticket_id=collab.ticket_id) ' .' WHERE ticket.ticket_id='.db_input($id) .' GROUP BY ticket.ticket_id'; @@ -98,6 +102,7 @@ class Ticket { $this->stats = null; $this->topic = null; $this->thread = null; + $this->collaborators = null; //REQUIRED: Preload thread obj - checked on lookup! $this->getThread(); @@ -375,9 +380,11 @@ class Ticket { } function getDept() { + global $cfg; - if(!$this->dept && $this->getDeptId()) - $this->dept= Dept::lookup($this->getDeptId()); + if(!$this->dept) + if(!($this->dept = Dept::lookup($this->getDeptId()))) + $this->dept = $cfg->getDefaultDept(); return $this->dept; } @@ -562,7 +569,79 @@ class Ticket { return $this->getThread()->getEntries($type, $order); } + //Collaborators + function getNumCollaborators() { + return $this->ht['collaborators']; + } + + function getActiveCollaborators() { + return $this->getCollaborators(array('isactive'=>1)); + } + + + function getCollaborators($criteria=array()) { + + if($criteria) + return Collaborator::forTicket($this->getId(), $criteria); + + if(!$this->collaborators && $this->getNumCollaborators()) + $this->collaborators = Collaborator::forTicket($this->getId()); + + return $this->collaborators; + } + + function addCollaborator($user, &$errors) { + + if(!$user) return null; + + $vars = array( + 'ticketId' => $this->getId(), + 'userId' => $user->getId()); + if(!($c=Collaborator::add($vars, $errors))) + return null; + + $this->ht['collaborators'] +=1; + $this->collaborators = null; + + + return $c; + } + + //XXX: Ugly for now + function updateCollaborators($vars, &$errors) { + + + //Deletes + if($vars['del'] && ($ids=array_filter($vars['del']))) { + $sql='DELETE FROM '.TICKET_COLLABORATOR_TABLE + .' WHERE ticket_id='.db_input($this->getId()) + .' AND id IN('.implode(',', db_input($ids)).')'; + if(db_query($sql)) + $this->ht['collaborators'] -= db_affected_rows(); + } + + //statuses + $cids = null; + if($vars['cid'] && ($cids=array_filter($vars['cid']))) { + $sql='UPDATE '.TICKET_COLLABORATOR_TABLE + .' SET updated=NOW(), isactive=1 ' + .' WHERE ticket_id='.db_input($this->getId()) + .' AND id IN('.implode(',', db_input($cids)).')'; + db_query($sql); + } + + $sql='UPDATE '.TICKET_COLLABORATOR_TABLE + .' SET updated=NOW(), isactive=0 ' + .' WHERE ticket_id='.db_input($this->getId()); + if($cids) + $sql.=' AND id NOT IN('.implode(',', db_input($cids)).')'; + db_query($sql); + + $this->collaborators = null; + + return true; + } /* -------------------- Setters --------------------- */ function setLastMsgId($msgid) { @@ -750,22 +829,20 @@ class Ticket { /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/ $this->reload(); //get the new goodies. - $dept= $this->getDept(); - - if(!$dept || !($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - if(!$tpl) return false; //bail out...missing stuff. - - if(!$dept || !($email=$dept->getAutoRespEmail())) - $email =$cfg->getDefaultEmail(); + if(!$cfg + || !($dept=$this->getDept()) + || !($tpl = $dept->getTemplate()) + || !($email=$dept->getAutoRespEmail())) { + return false; //bail out...missing stuff. + } $options = array( 'inreplyto'=>$message->getEmailMessageId(), 'references'=>$message->getEmailReferences()); //Send auto response - if enabled. - if($autorespond && $email && $cfg->autoRespONNewTicket() + if($autorespond + && $cfg->autoRespONNewTicket() && $dept->autoRespONNewTicket() && ($msg=$tpl->getAutoRespMsgTemplate())) { @@ -781,12 +858,10 @@ class Ticket { null, $options); } - if(!($email=$cfg->getAlertEmail())) - $email =$cfg->getDefaultEmail(); - //Send alert to out sleepy & idle staff. - if($alertstaff && $email + if ($alertstaff && $cfg->alertONNewTicket() + && ($email=$cfg->getAlertEmail()) && ($msg=$tpl->getNewTicketAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('message' => $message)); @@ -794,9 +869,8 @@ class Ticket { $recipients=$sentlist=array(); //Alert admin?? if($cfg->alertAdminONNewTicket()) { - $alert = str_replace('%{recipient}', 'Admin', $msg['body']); - $email->sendAlert($cfg->getAdminEmail(), $msg['subj'], - $alert, null, $options); + $alert = $this->replaceVars($msg, array('recipient' => 'Admin')); + $email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[]=$cfg->getAdminEmail(); } @@ -811,13 +885,10 @@ class Ticket { foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; - $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, - null, $options); + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } - - } return true; @@ -830,18 +901,14 @@ class Ticket { $msg=sprintf('Max open tickets (%d) reached for %s ', $cfg->getMaxOpenTickets(), $this->getEmail()); $ost->logWarning('Max. Open Tickets Limit ('.$this->getEmail().')', $msg); - if(!$sendNotice || !$cfg->sendOverLimitNotice()) return true; + if(!$sendNotice || !$cfg->sendOverLimitNotice()) + return true; //Send notice to user. - $dept = $this->getDept(); - - if(!$dept || !($tpl=$dept->getTemplate())) - $tpl=$cfg->getDefaultTemplate(); - - if(!$dept || !($email=$dept->getAutoRespEmail())) - $email=$cfg->getDefaultEmail(); - - if($tpl && ($msg=$tpl->getOverlimitMsgTemplate()) && $email) { + if(($dept = $this->getDept()) + && ($tpl=$dept->getTemplate()) + && ($msg=$tpl->getOverlimitMsgTemplate()) + && ($email=$dept->getAutoRespEmail())) { $msg = $this->replaceVars($msg->asArray(), array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); @@ -862,7 +929,42 @@ class Ticket { } function onResponse() { - db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1,lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId())); + db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1, lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId())); + $this->reload(); + } + + function activityNotice($vars) { + global $cfg; + + if (!$vars + || !$vars['variables'] + || !($collaborators=$this->getActiveCollaborators()) + || !($dept=$this->getDept()) + || !($tpl=$dept->getTemplate()) + || !($msg=$tpl->getActivityNoticeMsgTemplate()) + || !($email=$dept->getEmail())) + return; + + + $msg = $this->replaceVars($msg->asArray(), $vars['variables']); + + if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; + + $attachments = ($cfg->emailAttachments() && $vars['attachments'])?$vars['attachments']:array(); + $options = array(); + if($vars['inreplyto']) + $options['inreplyto'] = $vars['inreplyto']; + if($vars['references']) + $options['references'] = $vars['references']; + + foreach($collaborators as $collaborator) { + $msg = $this->replaceVars($msg, array('recipient' => $collaborator)); + $email->send($collaborator->getEmail(), $msg['subj'], $msg['body'], $attachments, + $options); + } + + return; } function onMessage($autorespond=true, $message=null) { @@ -889,18 +991,14 @@ class Ticket { if(!$autorespond || !$cfg->autoRespONNewMessage()) return; //no autoresp or alerts. - $this->reload(); - - - if(!$dept || !($tpl = $dept->getTemplate())) - $tpl = $cfg->getDefaultTemplate(); - - if(!$dept || !($email = $dept->getAutoRespEmail())) - $email = $cfg->getDefaultEmail(); + $dept = $this->getDept(); + $email = $dept->getAutoRespEmail(); //If enabled...send confirmation to user. ( New Message AutoResponse) - if($email && $tpl && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { + if($email + && ($tpl=$dept->getTemplate()) + && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); @@ -940,14 +1038,10 @@ class Ticket { if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts! $dept = $this->getDept(); - - //Get template. - if(!$dept || !($tpl = $dept->getTemplate())) - $tpl = $cfg->getDefaultTemplate(); - - //Email to use! - if(!($email=$cfg->getAlertEmail())) - $email = $cfg->getDefaultEmail(); + if(!$dept + || !($tpl = $dept->getTemplate()) + || !($email = $cfg->getAlertEmail())) + return true; //recipients $recipients=array(); @@ -962,7 +1056,7 @@ class Ticket { } //Get the message template - if($email && $recipients && $tpl && ($msg=$tpl->getAssignedAlertMsgTemplate())) { + if($recipients && ($msg=$tpl->getAssignedAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $comments, @@ -977,9 +1071,8 @@ class Ticket { 'references'=>$note->getEmailReferences()); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; - $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, - null, $options); + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } @@ -994,20 +1087,15 @@ class Ticket { $whine = false; //check if we need to send alerts. - if(!$whine || !$cfg->alertONOverdueTicket()) + if(!$whine + || !$cfg->alertONOverdueTicket() + || !($dept = $this->getDept())) return true; - $dept = $this->getDept(); - //Get department-defined or default template. - if(!$dept || !($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - //Email to use! - if(!($email=$cfg->getAlertEmail())) - $email =$cfg->getDefaultEmail(); - //Get the message template - if($tpl && ($msg=$tpl->getOverdueAlertMsgTemplate()) && $email) { + if(($tpl = $dept->getTemplate()) + && ($msg=$tpl->getOverdueAlertMsgTemplate()) + && ($email=$cfg->getAlertEmail())) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $comments)); @@ -1032,9 +1120,8 @@ class Ticket { $sentlist=array(); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; - $alert = str_replace("%{recipient}", $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, - null); + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null); $sentlist[] = $staff->getEmail(); } @@ -1200,19 +1287,12 @@ class Ticket { $this->logEvent('transferred'); //Send out alerts if enabled AND requested - if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) return true; //no alerts!! - - - //Get template. - if(!($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - //Email to use! - if(!($email=$cfg->getAlertEmail())) - $email =$cfg->getDefaultEmail(); + if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept())) + return true; //no alerts!! - //Get the message template - if($tpl && ($msg=$tpl->getTransferAlertMsgTemplate()) && $email) { + if(($email=$cfg->getAlertEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getTransferAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $comments, 'staff' => $thisstaff)); @@ -1240,9 +1320,8 @@ class Ticket { 'references'=>$note->getEmailReferences()); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; - $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, - null, $options); + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } @@ -1377,6 +1456,13 @@ class Ticket { $this->setLastMsgId($message->getId()); + //Add email recipients as collaborators + if($vars['recipients']) { + foreach($vars['recipients'] as $recipient) + if(($user=User::fromVars($recipient))) + $this->addCollaborator($user, $errors); + } + if(!$alerts) return $message; //Our work is done... $autorespond = true; @@ -1387,17 +1473,22 @@ class Ticket { $dept = $this->getDept(); - if(!$dept || !($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - if(!($email=$cfg->getAlertEmail())) - $email =$cfg->getDefaultEmail(); + $variables = array( + 'message' => $message, + 'poster' => ($vars['poster'] ? $vars['poster'] : $this->getName()) + ); + $options = array( + 'inreplyto' => $message->getEmailMessageId(), + 'references' => $message->getEmailReferences()); //If enabled...send alert to staff (New Message Alert) - if($cfg->alertONNewMessage() && $tpl && $email && ($msg=$tpl->getNewMessageAlertMsgTemplate())) { + if($cfg->alertONNewMessage() + && ($email = $cfg->getAlertEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg = $tpl->getNewMessageAlertMsgTemplate())) { $attachments = $message->getAttachments(); - $msg = $this->replaceVars($msg->asArray(), array('message' => $message)); + $msg = $this->replaceVars($msg->asArray(), $variables); //Build list of recipients and fire the alerts. $recipients=array(); @@ -1415,18 +1506,21 @@ class Ticket { $recipients[]=$manager; $sentlist=array(); //I know it sucks...but..it works. - $options = array( - 'inreplyto'=>$message->getEmailMessageId(), - 'references'=>$message->getEmailReferences()); foreach( $recipients as $k=>$staff) { if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; - $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, - $attachments, $options); + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], $attachments, $options); $sentlist[] = $staff->getEmail(); } } + unset($variables['message']); + $variables['message'] = $message->asVar(); + + $info = $options + array('variables' => $variables); + $this->activityNotice($info); + + return $message; } @@ -1455,13 +1549,9 @@ class Ticket { $dept = $this->getDept(); - if(!($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - if(!$dept || !($email=$dept->getEmail())) - $email = $cfg->getDefaultEmail(); - - if($tpl && ($msg=$tpl->getAutoReplyMsgTemplate()) && $email) { + if(($email=$dept->getEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getAutoReplyMsgTemplate())) { if($dept && $dept->isPublic()) $signature=$dept->getSignature(); @@ -1493,7 +1583,7 @@ class Ticket { if(!$vars['poster'] && $thisstaff) - $vars['poster'] = $thisstaff->getName(); + $vars['poster'] = $thisstaff; if(!$vars['staffId'] && $thisstaff) $vars['staffId'] = $thisstaff->getId(); @@ -1506,45 +1596,48 @@ class Ticket { $this->setStatus($vars['reply_ticket_status']); $this->onResponse(); //do house cleaning.. - $this->reload(); /* email the user?? - if disabled - the bail out */ if(!$alert) return $response; $dept = $this->getDept(); - if(!($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - if(!$dept || !($email=$dept->getEmail())) - $email = $cfg->getDefaultEmail(); - - if($tpl && ($msg=$tpl->getReplyMsgTemplate()) && $email) { + if($thisstaff && $vars['signature']=='mine') + $signature=$thisstaff->getSignature(); + elseif($vars['signature']=='dept' && $dept && $dept->isPublic()) + $signature=$dept->getSignature(); + else + $signature=''; - if($thisstaff && $vars['signature']=='mine') - $signature=$thisstaff->getSignature(); - elseif($vars['signature']=='dept' && $dept && $dept->isPublic()) - $signature=$dept->getSignature(); - else - $signature=''; + $variables = array( + 'response' => $response, + 'signature' => $signature, + 'staff' => $thisstaff, + 'poster' => $thisstaff); + $options = array( + 'inreplyto' => $response->getEmailMessageId(), + 'references' => $response->getEmailReferences()); - //Set attachments if emailing. - $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); + if(($email=$dept->getEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getReplyMsgTemplate())) { - $msg = $this->replaceVars($msg->asArray(), - array('response' => $response, 'signature' => $signature, 'staff' => $thisstaff)); + $msg = $this->replaceVars($msg->asArray(), $variables); if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - - $options = array( - 'inreplyto' => $response->getEmailMessageId(), - 'references' => $response->getEmailReferences()); - //TODO: setup 5 param (options... e.g mid trackable on replies) + $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments, $options); } + if($vars['emailcollab']) { + unset($variables['response']); + $variables['message'] = $response->asVar(); + $info = $options + array('variables' => $variables); + $this->activityNotice($info); + } + return $response; } @@ -1618,14 +1711,9 @@ class Ticket { if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept())) return $note; - if(!($tpl = $dept->getTemplate())) - $tpl= $cfg->getDefaultTemplate(); - - if(!($email=$cfg->getAlertEmail())) - $email =$cfg->getDefaultEmail(); - - - if($tpl && ($msg=$tpl->getNoteAlertMsgTemplate()) && $email) { + if(($email=$cfg->getAlertEmail()) + && ($tpl = $dept->getTemplate()) + && ($msg=$tpl->getNoteAlertMsgTemplate())) { $attachments = $note->getAttachments(); @@ -1658,9 +1746,8 @@ class Ticket { || in_array($staff->getEmail(), $sentlist) //No duplicates. || $note->getStaffId() == $staff->getId()) //No need to alert the poster! continue; - $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, $attachments, - $options); + $alert = $this->replaceVars($msg, array('recipient' => $staff)); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], $attachments, $options); $sentlist[] = $staff->getEmail(); } } @@ -2031,7 +2118,7 @@ class Ticket { if (!$user) { $user_form = UserForm::getUserForm()->getForm($vars); if (!$user_form->isValid($field_filter) - || !($user=User::fromForm($user_form->getClean()))) + || !($user=User::fromVars($user_form->getClean()))) $errors['user'] = 'Incomplete client information'; } } @@ -2225,19 +2312,15 @@ class Ticket { $ticket->reload(); - if(!$cfg->notifyONNewStaffTicket() || !isset($vars['alertuser'])) + if(!$cfg->notifyONNewStaffTicket() + || !isset($vars['alertuser']) + || !($dept=$ticket->getDept())) return $ticket; //No alerts. //Send Notice to user --- if requested AND enabled!! - - $dept=$ticket->getDept(); - if(!$dept || !($tpl=$dept->getTemplate())) - $tpl=$cfg->getDefaultTemplate(); - - if(!$dept || !($email=$dept->getEmail())) - $email =$cfg->getDefaultEmail(); - - if($tpl && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) && $email) { + if(($tpl=$dept->getTemplate()) + && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) + && ($email=$dept->getEmail())) { $message = $vars['message']; if($response) { diff --git a/include/class.user.php b/include/class.user.php index 2d837dad0cf22c7039631027267cb42e6f485126..85bffe026d3769715c466219d487749de92f6fca 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -76,32 +76,46 @@ class User extends UserModel { $this->default_email = UserEmail::lookup($ht['default_email_id']); } - static function fromForm($data=false) { + static function fromVars($vars) { // Try and lookup by email address - $user = User::lookup(array('emails__address'=>$data['email'])); + $user = User::lookup(array('emails__address'=>$vars['email'])); if (!$user) { $user = User::create(array( - 'name'=>$data['name'], + 'name'=>$vars['name'], 'created'=>new SqlFunction('NOW'), 'updated'=>new SqlFunction('NOW'), 'default_email'=> - UserEmail::create(array('address'=>$data['email'])) + UserEmail::create(array('address'=>$vars['email'])) )); $user->save(true); $user->emails->add($user->default_email); - // Attach initial custom fields - $uf = UserForm::getInstance(); - foreach ($uf->getFields() as $f) - if (isset($data[$f->get('name')])) - $uf->setAnswer($f->get('name'), $data[$f->get('name')]); - $uf->setClientId($user->id); - $uf->save(); + $user->addDynamicData($vars); } return $user; } + static function fromForm($form) { + + if(!$form) return null; + + //Validate the form + $valid = true; + if (!$form->isValid()) + $valid = false; + + //Make sure the email is not in-use + if (($field=$form->getField('email')) + && $field->getClean() + && User::lookup(array('emails__address'=>$field->getClean()))) { + $field->addError('Email is assigned to another user'); + $valid = false; + } + + return $valid ? self::fromVars($form->getClean()) : null; + } + function getEmail() { return $this->default_email->address; } @@ -148,6 +162,18 @@ class User extends UserModel { return $a; } + function addDynamicData($data) { + + $uf = UserForm::getInstance(); + $uf->setClientId($this->id); + foreach ($uf->getFields() as $f) + if (isset($data[$f->get('name')])) + $uf->setAnswer($f->get('name'), $data[$f->get('name')]); + $uf->save(); + + return $uf; + } + function getDynamicData() { if (!isset($this->_entries)) { $this->_entries = DynamicFormEntry::forClient($this->id)->all(); @@ -341,6 +367,10 @@ class PersonsName { return $this->name; } + function getName() { + return $this; + } + function asVar() { return $this->__toString(); } @@ -421,3 +451,5 @@ class UserEmail extends UserEmailModel { return $email; } } + +?> diff --git a/include/i18n/en_US/templates/email/ticket.activity.notice.yaml b/include/i18n/en_US/templates/email/ticket.activity.notice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4248931a815db98cd8cd3a7ce35a2048ee07da2c --- /dev/null +++ b/include/i18n/en_US/templates/email/ticket.activity.notice.yaml @@ -0,0 +1,37 @@ +# +# Email template: ticket.activity.notice.yaml +# +# Notice sent to collaborators on ticket activity e.g reply or message +# +--- +notes: | + Notice sent to collaborators on ticket activity e.g reply or message. + +subject: "Re: %{ticket.subject}" +body: | + <img src="cid:6fe1efdea357534d238b86e7860a7c5a" alt="osTicket Logo (kangaroo)" width="113" height="64" + style="float: right; width: 113px; margin: 0px 0px 10px 10px;"> + <h3><span style="color: rgb(127, 127, 127); font-weight: normal; font-family: Georgia; font-size: 30pt" + >Stay in the loop</span></h3> <strong><br> + Dear %{recipient.name.first}, + </strong> + <br> + <br> + <div> + <em>%{poster.name}</em> just logged a message to a ticket in which you participate. + </div> + <br> + %{message} + <br> + <br> + <hr> + <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;"> + <em>You're getting this email because you are a collaborator + on ticket <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);" + >#%{ticket.number}</a>. To participate, simply reply to this email + or <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);" + >click here</a> for a complete archive of the ticket thread.</em> + </div> + <div style="text-align: center;"> <a href="http://osticket.com/"><img + src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" alt="Powered by osTicket" + width="126" height="19" style="width: 126px;"></a> </div> diff --git a/include/staff/templates/collaborators.tmpl.php b/include/staff/templates/collaborators.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..a10bf0c105b5d9ea5a116a09600d66ac29ca93eb --- /dev/null +++ b/include/staff/templates/collaborators.tmpl.php @@ -0,0 +1,57 @@ +<h3>Ticket Collaborators</h3> +<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b> +<?php +if($info && $info['msg']) { + echo sprintf('<p id="msg_notice" style="padding-top:2px;">%s</p>', $info['msg']); +} ?> +<hr/> +<?php +if(($users=$ticket->getCollaborators())) {?> +<div id="manage_collaborators"> +<form method="post" class="collaborators" action="#tickets/<?php echo $ticket->getId(); ?>/collaborators"> + <table border="0" cellspacing="1" cellpadding="1" width="100%"> + <?php + foreach($users as $user) { + $checked = $user->isActive() ? 'checked="checked"' : ''; + echo sprintf('<tr> + <td> + <input type="checkbox" name="cid[]" id="c%d" value="%d" %s> + <a class="collaborator" href="#collaborators/%d/view">%s</a> + <span class="faded"><em>%s</em></span></td> + <td width="10"> + <input type="hidden" name="del[]" id="d%d" value=""> + <a class="remove" href="#d%d">×</a></td> + <td width="30"> </td> + </tr>', + $user->getId(), + $user->getId(), + $checked, + $user->getId(), + $user->getName(), + $user->getEmail(), + $user->getId(), + $user->getId()); + } + ?> + </table> + <hr style="margin-top:1em"/> + <div><a class="collaborator" + href="#tickets/<?php echo $ticket->getId(); ?>/add-collaborator" >Add New Collaborator</a></div> + <div id="savewarning" style="display:none; padding-top:2px;"><p id="msg_warning">You have made changes that you need to save.</p></div> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" value="Done" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Save Changes"> + </span> + </p> +</form> +<div class="clear"></div> +</div> +<?php +} else { + echo "Bro, not sure how you got here!"; +} +?> diff --git a/include/staff/templates/user-lookup.tmpl.php b/include/staff/templates/user-lookup.tmpl.php index de8dedb4e79b8d513dedf64c5134d6f1a579f783..8a6edd8f8771ca1cd583f3d1801eb1f72925969d 100644 --- a/include/staff/templates/user-lookup.tmpl.php +++ b/include/staff/templates/user-lookup.tmpl.php @@ -10,7 +10,7 @@ if ($info['error']) { echo sprintf('<p id="msg_notice">%s</p>', $info['msg']); } ?> <div id="selected-user-info" style="display:<?php echo $user ? 'block' :'none'; ?>;margin:5px;"> -<form method="get" class="user" action="#users/lookup"> +<form method="post" class="user" action="<?php echo $info['action'] ? $info['action'] : '#users/lookup'; ?>"> <input type="hidden" id="user-id" name="id" value="<?php echo $user ? $user->getId() : 0; ?>"/> <i class="icon-user icon-4x pull-left icon-border"></i> <a class="action-button pull-right" style="overflow:inherit" @@ -30,7 +30,7 @@ if ($info['error']) { </form> </div> <div id="new-user-form" style="display:<?php echo $user ? 'none' :'block'; ?>;"> -<form method="post" class="user" action="#users/lookup/form"> +<form method="post" class="user" action="<?php echo $info['action'] ? $info['action'] : '#users/lookup/form'; ?>"> <table width="100%"> <?php if(!$form) $form = UserForm::getInstance(); diff --git a/include/staff/templates/user.tmpl.php b/include/staff/templates/user.tmpl.php index 92bbe3754effd5161a51e2f3a1ebca4d9e65c910..37528cc52d13deff96f20c7062231503c43ec45a 100644 --- a/include/staff/templates/user.tmpl.php +++ b/include/staff/templates/user.tmpl.php @@ -30,7 +30,7 @@ if ($info['error']) { <div id="user-form" style="display:<?php echo $forms ? 'block' : 'none'; ?>;"> <div><p id="msg_info"><i class="icon-info-sign"></i> Please note that updates will be reflected system-wide.</p></div> <?php -$action = '#users/'.$user->getId(); +$action = $info['action'] ? $info['action'] : ('#users/'.$user->getId()); if ($ticket && $ticket->getOwnerId() == $user->getId()) $action = '#tickets/'.$ticket->getId().'/user'; ?> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index de1fb3a0f217ac7d788e1ff92315118f3f0828d9..3866de15be2f54303d12e983e5d6dab78dce8042 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -405,20 +405,55 @@ $tcount+= $ticket->getNumNotes(); <input type="hidden" name="a" value="reply"> <span class="error"></span> <table style="width:100%" border="0" cellspacing="0" cellpadding="3"> + <tbody id="to_sec"> <tr> <td width="120"> <label><strong>TO:</strong></label> </td> <td> <?php - echo sprintf('<span id="user-to-name">%s</span> <em><<span id="user-to-email">%s</span>></em>', - $ticket->getName(), $ticket->getReplyToEmail()); + $to =sprintf('%s <%s>', $ticket->getName(), $ticket->getReplyToEmail()); + $emailReply = (!isset($info['emailreply']) || $info['emailreply']); ?> - - <label><input type='checkbox' value='1' name="emailreply" id="remailreply" - <?php echo ((!$info['emailreply'] && !$errors) || isset($info['emailreply']))?'checked="checked"':''; ?>> Email Reply</label> + <select id="emailreply" name="emailreply"> + <option value="1" <?php echo $emailReply ? 'selected="selected"' : ''; ?>><?php echo $to; ?></option> + <option value="0" <?php echo !$emailReply ? 'selected="selected"' : ''; ?> + >—Do Not Email Reply—</option> + </select> + </td> + </tr> + </tbody> + <?php + if(1) { //Make CC optional feature? NO, for now. + ?> + <tbody id="cc_sec" style="display:<?php echo $emailReply? + 'table-row-group':'none'; ?>;"> + <tr> + <td width="120"> + <label><strong>CC:</strong></label> + </td> + <td> + <?php + if($ticket->getNumCollaborators()) { ?> + <input type='checkbox' value='1' name="emailcollab" id="emailcollab" + <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?>> + <?php + echo sprintf('<a class="collaborators" + href="#tickets/%d/collaborators/manage">Collaborators (%d)</a>', + $ticket->getId(), + $ticket->getNumCollaborators()); + } else { + echo sprintf('<div><a class="collaborators" + href="#tickets/%d/collaborators/manage" >Add Collaborators</a></div>', + $ticket->getId()); + } + ?> </td> </tr> + </tbody> + <?php + } ?> + <tbody id="resp_sec"> <?php if($errors['response']) {?> <tr><td width="120"> </td><td class="error"><?php echo $errors['response']; ?> </td></tr> @@ -518,7 +553,7 @@ $tcount+= $ticket->getNumNotes(); </tr> <?php } ?> - </div> + </tbody> </table> <p style="padding-left:165px;"> <input class="btn_sm" type="submit" value="Post Reply"> diff --git a/include/upgrader/streams/core/collaboration.patch.sql b/include/upgrader/streams/core/collaboration.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..4f6adf91d8e1ca1ae2ce502e1ac317a1fda219f2 --- /dev/null +++ b/include/upgrader/streams/core/collaboration.patch.sql @@ -0,0 +1,25 @@ +/** + * @version v1.8.1 Collaboration (CC/BCC support) + * @signature f353145f8f4f48ea7f0d8e87083bb57c + * + * Adds the database structure for collaboration table + * + */ + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_collaborator`; +CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1', + `ticket_id` int(11) unsigned NOT NULL DEFAULT '0', + `user_id` int(11) unsigned NOT NULL DEFAULT '0', + `role` char(1) NOT NULL DEFAULT 'E', + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `collab` (`ticket_id`,`user_id`) +) DEFAULT CHARSET=utf8; + + +-- Finish +UPDATE `%TABLE_PREFIX%config` + SET `value` = 'f353145f8f4f48ea7f0d8e87083bb57c' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/scp/ajax.php b/scp/ajax.php index 106f3366a723a6e25a815796209c1dc5fed07f71..0ea64116e77289b22e9c5aba5a7efac971f8b6ee 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -75,9 +75,16 @@ $dispatcher = patterns('', url_post('^(?P<tid>\d+)/lock', 'acquireLock'), url_post('^(?P<tid>\d+)/lock/(?P<id>\d+)/renew', 'renewLock'), url_post('^(?P<tid>\d+)/lock/(?P<id>\d+)/release', 'releaseLock'), + url_get('^(?P<tid>\d+)/collaborators/manage$', 'showCollaborators'), + url_post('^(?P<tid>\d+)/collaborators$', 'updateCollaborators'), + url('^(?P<tid>\d+)/add-collaborator$', 'addCollaborator'), url_get('^lookup', 'lookup'), url_get('^search', 'search') )), + url('^/collaborators/', patterns('ajax.tickets.php:TicketsAjaxAPI', + url_get('^(?P<cid>\d+)/view$', 'viewCollaborator'), + url_post('^(?P<cid>\d+)$', 'updateCollaborator') + )), url('^/draft/', patterns('ajax.draft.php:DraftAjaxAPI', url_post('^(?P<id>\d+)$', 'updateDraft'), url_delete('^(?P<id>\d+)$', 'deleteDraft'), diff --git a/scp/js/scp.js b/scp/js/scp.js index b61e62057084e164bf1aac9e146f13f292ba9b3b..b80cb1a3b83f41f7825ff3e0b6aad8019c7af002 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -399,13 +399,13 @@ $(document).ready(function(){ }); }); - $.userLookup = function (url, callback) { + $.dialog = function (url, code, cb) { $('.dialog#popup .body').load(url, function () { $('#overlay').show(); $('.dialog#popup').show(); - $(document).off('.user'); - $(document).on('submit.user', '.dialog#popup form.user',function(e) { + $(document).off('.dialog'); + $(document).on('submit.dialog', '.dialog#popup form', function(e) { e.preventDefault(); var $form = $(this); var $dialog = $form.closest('.dialog'); @@ -415,12 +415,11 @@ $(document).ready(function(){ data: $form.serialize(), cache: false, success: function(resp, status, xhr) { - if (xhr && xhr.status == 201) { - var user = $.parseJSON(xhr.responseText); + if (xhr && xhr.status == code) { $('div.body', $dialog).empty(); $dialog.hide(); $('#overlay').hide(); - if(callback) callback(user); + if(cb) cb(xhr.responseText); } else { $('div.body', $dialog).html(resp); $('#msg_notice, #msg_error', $dialog).delay(5000).slideUp(); @@ -434,6 +433,13 @@ $(document).ready(function(){ }); }; + $.userLookup = function (url, cb) { + $.dialog(url, 201, function (resp) { + var user = $.parseJSON(resp); + if(cb) cb(user); + }); + }; + $('#advanced-search').delegate('#status', 'change', function() { switch($(this).val()) { case 'closed': diff --git a/scp/js/ticket.js b/scp/js/ticket.js index c2110121a8f2e202a6749b6f56fb741cd09d1a7b..c91cacaa9eff51205a25265bdfa210d5e7c3864f 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -363,6 +363,72 @@ jQuery(function($) { return false; }); + //Collaborators + $(document).on('click', 'a.collaborator, a.collaborators', function(e) { + e.preventDefault(); + var url = 'ajax.php/'+$(this).attr('href').substr(1); + $.dialog(url, 201, function (resp) { + }); + return false; + }); + + $(document).on('click', 'form.collaborators a#addcollaborator', function (e) { + e.preventDefault(); + $('div#manage_collaborators').hide(); + $('div#add_collaborator').fadeIn(); + return false; + }); + + $(document).on('click', 'form.collaborators a.remove', function (e) { + e.preventDefault(); + var fObj = $(this).closest('form'); + $('input'+$(this).attr('href')) + .val($(this).attr('href').substr(2)) + .trigger('change'); + $(this).closest('tr').addClass('strike'); + + return false; + }); + + $(document).on('change', 'form.collaborators input:checkbox, input[name="del[]"]', function (e) { + var fObj = $(this).closest('form'); + $('div#savewarning', fObj).fadeIn(); + $('input:submit', fObj).css('color', 'red'); + }); + + $(document).on('click', 'form.collaborators input:reset', function(e) { + var fObj = $(this).closest('form'); + fObj.find('input[name="del[]"]').val(''); + fObj.find('tr').removeClass('strike'); + $('div#savewarning', fObj).hide(); + $('input:submit', fObj).removeAttr('style'); + }); + + + $(document).on('click', 'form.collaborators input.cancel', function (e) { + e.preventDefault(); + var $elem = $(this); + + if($elem.attr('data-href')) { + var href = $elem.data('href').substr(1); + $('.dialog.collaborators .body').load('ajax.php/'+href, function () { + }); + } else { + + $('div#manage_collaborators').show(); + $('div#add_collaborator').hide(); + } + return false; + }); + + $(document).on('change', 'form#reply select#emailreply', function(e) { + var $cc = $('form#reply tbody#cc_sec'); + if($(this).val() == 0) + $cc.hide(); + else + $cc.show(); + }); + var showNonLocalImage = function(div) { var $div = $(div), $img = $div.append($('<img>') diff --git a/scp/js/tips.js b/scp/js/tips.js index 44c924711c1b73bc012674acefce634f007b2eca..205eb6fc9d2df8edbbd17d3ad43dbfab76145d36 100644 --- a/scp/js/tips.js +++ b/scp/js/tips.js @@ -93,7 +93,6 @@ jQuery(function() { tip_timer = setTimeout(function() { $('.tip_box').remove(); $('body').append(the_tip.hide().fadeIn()); - console.log($(window).width(), tip_content.width(), the_tip.position()) if ($(window).width() < tip_content.outerWidth() + the_tip.position().left) { the_tip.css({'left':x_pos-tip_content.outerWidth()-40+'px'}); tip_box.addClass('right'); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 8ab5676efd090558868eef5065c14b19882af0ed..cd260b93e439881d1c2bef6040eccc4f9d00b3b2 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -621,6 +621,19 @@ CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( KEY `pid` (`pid`) ) DEFAULT CHARSET=utf8; +CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1', + `ticket_id` int(11) unsigned NOT NULL DEFAULT '0', + `user_id` int(11) unsigned NOT NULL DEFAULT '0', + `role` char(1) NOT NULL DEFAULT 'E', + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `collab` (`ticket_id`,`user_id`) +) DEFAULT CHARSET=utf8; + + + DROP TABLE IF EXISTS `%TABLE_PREFIX%timezone`; CREATE TABLE `%TABLE_PREFIX%timezone` ( `id` int(11) unsigned NOT NULL auto_increment,