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: **********************************************************************/ include_once(INCLUDE_DIR.'class.thread.php'); include_once(INCLUDE_DIR.'class.staff.php'); include_once(INCLUDE_DIR.'class.client.php'); include_once(INCLUDE_DIR.'class.team.php'); include_once(INCLUDE_DIR.'class.email.php'); include_once(INCLUDE_DIR.'class.dept.php'); include_once(INCLUDE_DIR.'class.topic.php'); include_once(INCLUDE_DIR.'class.lock.php'); include_once(INCLUDE_DIR.'class.file.php'); include_once(INCLUDE_DIR.'class.attachment.php'); include_once(INCLUDE_DIR.'class.banlist.php'); include_once(INCLUDE_DIR.'class.template.php'); include_once(INCLUDE_DIR.'class.variable.php'); include_once(INCLUDE_DIR.'class.priority.php'); 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 { var $id; var $number; var $ht; var $lastMsgId; var $status; var $dept; //Dept obj var $sla; // SLA obj var $staff; //Staff obj var $client; //Client Obj var $team; //Team obj var $topic; //Topic obj var $tlock; //TicketLock obj var $thread; //Thread obj. function Ticket($id) { $this->id = 0; $this->load($id); } function load($id=0) { if(!$id && !($id=$this->getId())) return false; $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(distinct attach.attach_id) as attachments' .' 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) ' .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON ( ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON ( ticket.ticket_id=attach.ticket_id) ' .' WHERE ticket.ticket_id='.db_input($id) .' GROUP BY ticket.ticket_id'; //echo $sql; if(!($res=db_query($sql)) || !db_num_rows($res)) return false; $this->ht = db_fetch_array($res); $this->id = $this->ht['ticket_id']; $this->number = $this->ht['number']; $this->_answers = array(); $this->loadDynamicData(); //Reset the sub classes (initiated ondemand)...good for reloads. $this->status= null; $this->staff = null; $this->client = null; $this->team = null; $this->dept = null; $this->sla = null; $this->tlock = null; $this->stats = null; $this->topic = null; $this->thread = null; $this->collaborators = null; return true; } function loadDynamicData() { if (!$this->_answers) { foreach (DynamicFormEntry::forTicket($this->getId(), true) as $form) { foreach ($form->getAnswers() as $answer) { $tag = mb_strtolower($answer->getField()->get('name')) ?: 'field.' . $answer->getField()->get('id'); $this->_answers[$tag] = $answer; } } } return $this->_answers; } function reload() { return $this->load(); } function hasState($state) { return (strcasecmp($this->getState(), $state)==0); } function isOpen() { return $this->hasState('open'); } function isReopened() { return ($this->getReopenDate()); } function isReopenable() { return $this->getStatus()->isReopenable(); } function isClosed() { return $this->hasState('closed'); } function isArchived() { return $this->hasState('archived'); } function isDeleted() { return $this->hasState('deleted'); } function isAssigned() { return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId())); } function isOverdue() { return ($this->ht['isoverdue']); } function isAnswered() { return ($this->ht['isanswered']); } function isLocked() { return ($this->getLockId()); } function checkStaffAccess($staff) { if(!is_object($staff) && !($staff=Staff::lookup($staff))) return false; // Staff has access to the department. if (!$staff->showAssignedOnly() && $staff->canAccessDept($this->getDeptId())) return true; // Only consider assignment if the ticket is open if (!$this->isOpen()) return false; // Check ticket access based on direct or team assignment if ($staff->getId() == $this->getStaffId() || ($this->getTeamId() && $staff->isTeamMember($this->getTeamId()) )) return true; // No access bro! return false; } function checkUserAccess($user) { if (!$user || !($user instanceof EndUser)) return false; //Ticket Owner if ($user->getId() == $this->getUserId()) return true; //Collaborator? // 1) If the user was authorized via this ticket. if ($user->getTicketId() == $this->getId() && !strcasecmp($user->getRole(), 'collaborator')) return true; // 2) Query the database to check for expanded access... if (Collaborator::lookup(array( 'userId' => $user->getId(), 'ticketId' => $this->getId()))) return true; return false; } //Getters function getId() { return $this->id; } function getNumber() { return $this->number; } function getOwnerId() { return $this->ht['user_id']; } function getOwner() { if (!isset($this->owner) && ($u=User::lookup($this->getOwnerId()))) $this->owner = new TicketOwner(new EndUser($u), $this); return $this->owner; } function getEmail(){ if ($o = $this->getOwner()) return $o->getEmail(); return null; } function getReplyToEmail() { //TODO: Determine the email to use (once we enable multi-email support) return $this->getEmail(); } function getAuthToken() { # XXX: Support variable email address (for CCs) return md5($this->getId() . strtolower($this->getEmail()) . SECRET_SALT); } function getName(){ if ($o = $this->getOwner()) return $o->getName(); return null; } function getSubject() { return (string) $this->_answers['subject']; } /* Help topic title - NOT object -> $topic */ function getHelpTopic() { if(!$this->ht['helptopic'] && ($topic=$this->getTopic())) $this->ht['helptopic'] = $topic->getFullName(); return $this->ht['helptopic']; } function getCreateDate() { return $this->ht['created']; } function getOpenDate() { return $this->getCreateDate(); } function getReopenDate() { return $this->ht['reopened']; } function getUpdateDate() { return $this->ht['updated']; } function getDueDate() { return $this->ht['duedate']; } function getSLADueDate() { return $this->ht['sla_duedate']; } function getEstDueDate() { //Real due date if(($duedate=$this->getDueDate())) return $duedate; //return sla due date (If ANY) return $this->getSLADueDate(); } function getCloseDate() { return $this->ht['closed']; } function getStatusId() { return $this->ht['status_id']; } function getStatus() { if (!$this->status && $this->getStatusId()) $this->status = TicketStatus::lookup($this->getStatusId()); return $this->status; } function getState() { if (!$this->getStatus()) return ''; return $this->getStatus()->getState(); } function getDeptId() { return $this->ht['dept_id']; } function getDeptName() { if(!$this->ht['dept_name'] && ($dept = $this->getDept())) $this->ht['dept_name'] = $dept->getName(); return $this->ht['dept_name']; } function getPriorityId() { global $cfg; if (($a = $this->_answers['priority']) && ($b = $a->getValue())) return $b->getId(); return $cfg->getDefaultPriorityId(); } function getPriority() { if (($a = $this->_answers['priority']) && ($b = $a->getValue())) return $b->getDesc(); return ''; } function getPhoneNumber() { return (string)$this->getOwner()->getPhoneNumber(); } function getSource() { return $this->ht['source']; } function getIP() { return $this->ht['ip_address']; } function getHashtable() { return $this->ht; } function getUpdateInfo() { global $cfg; $info=array('source' => $this->getSource(), 'topicId' => $this->getTopicId(), 'slaId' => $this->getSLAId(), 'user_id' => $this->getOwnerId(), 'duedate' => $this->getDueDate() ? Format::userdate($cfg->getDateFormat(), Misc::db2gmtime($this->getDueDate())) :'', 'time' => $this->getDueDate()?(Format::userdate('G:i', Misc::db2gmtime($this->getDueDate()))):'', ); return $info; } function getLockId() { return $this->ht['lock_id']; } function getLock() { if(!$this->tlock && $this->getLockId()) $this->tlock= TicketLock::lookup($this->getLockId(), $this->getId()); return $this->tlock; } function acquireLock($staffId, $lockTime) { if(!$staffId or !$lockTime) //Lockig disabled? return null; //Check if the ticket is already locked. if(($lock=$this->getLock()) && !$lock->isExpired()) { if($lock->getStaffId()!=$staffId) //someone else locked the ticket. return null; //Lock already exits...renew it $lock->renew($lockTime); //New clock baby. return $lock; } //No lock on the ticket or it is expired $this->tlock = null; //clear crap $this->ht['lock_id'] = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock.. //load and return the newly created lock if any! return $this->getLock(); } function getDept() { global $cfg; if(!$this->dept) if(!($this->dept = Dept::lookup($this->getDeptId()))) $this->dept = $cfg->getDefaultDept(); return $this->dept; } function getUserId() { return $this->getOwnerId(); } function getUser() { if(!isset($this->user) && $this->getOwner()) $this->user = new EndUser($this->getOwner()); return $this->user; } function getStaffId() { return $this->ht['staff_id']; } function getStaff() { if(!$this->staff && $this->getStaffId()) $this->staff= Staff::lookup($this->getStaffId()); return $this->staff; } function getTeamId() { return $this->ht['team_id']; } function getTeam() { if(!$this->team && $this->getTeamId()) $this->team = Team::lookup($this->getTeamId()); return $this->team; } function getAssignee() { if($staff=$this->getStaff()) return $staff->getName(); if($team=$this->getTeam()) return $team->getName(); return ''; } function getAssignees() { $assignees=array(); if($staff=$this->getStaff()) $assignees[] = $staff->getName(); if($team=$this->getTeam()) $assignees[] = $team->getName(); return $assignees; } function getAssigned($glue='/') { $assignees = $this->getAssignees(); return $assignees?implode($glue, $assignees):''; } function getTopicId() { return $this->ht['topic_id']; } function getTopic() { if(!$this->topic && $this->getTopicId()) $this->topic = Topic::lookup($this->getTopicId()); return $this->topic; } function getSLAId() { return $this->ht['sla_id']; } function getSLA() { if(!$this->sla && $this->getSLAId()) $this->sla = SLA::lookup($this->getSLAId()); return $this->sla; } function getLastRespondent() { $sql ='SELECT resp.staff_id ' .' FROM '.TICKET_THREAD_TABLE.' resp ' .' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) ' .' WHERE resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 ' .' AND resp.thread_type="R"' .' ORDER BY resp.created DESC LIMIT 1'; if(!($res=db_query($sql)) || !db_num_rows($res)) return null; list($id)=db_fetch_row($res); return Staff::lookup($id); } function getLastMessageDate() { return $this->ht['lastmessage']; } function getLastMsgDate() { return $this->getLastMessageDate(); } function getLastResponseDate() { return $this->ht['lastresponse']; } function getLastRespDate() { return $this->getLastResponseDate(); } function getLastMsgId() { return $this->lastMsgId; } function getLastMessage() { if (!isset($this->last_message)) { if($this->getLastMsgId()) $this->last_message = Message::lookup( $this->getLastMsgId(), $this->getId()); if (!$this->last_message) $this->last_message = Message::lastByTicketId($this->getId()); } return $this->last_message; } function getThread() { if(!$this->thread) $this->thread = Thread::lookup($this); return $this->thread; } function getThreadCount() { return $this->getNumMessages() + $this->getNumResponses(); } function getNumMessages() { return $this->getThread()->getNumMessages(); } function getNumResponses() { return $this->getThread()->getNumResponses(); } function getNumNotes() { return $this->getThread()->getNumNotes(); } function getMessages() { return $this->getThreadEntries('M'); } function getResponses() { return $this->getThreadEntries('R'); } function getNotes() { return $this->getThreadEntries('N'); } function getClientThread() { return $this->getThreadEntries(array('M', 'R')); } function getThreadEntry($id) { return $this->getThread()->getEntry($id); } function getThreadEntries($type, $order='') { return $this->getThread()->getEntries($type, $order); } //Collaborators function getNumCollaborators() { return count($this->getCollaborators()); } function getNumActiveCollaborators() { if (!isset($this->ht['active_collaborators'])) $this->ht['active_collaborators'] = count($this->getActiveCollaborators()); return $this->ht['active_collaborators']; } function getActiveCollaborators() { return $this->getCollaborators(array('isactive'=>1)); } function getCollaborators($criteria=array()) { if ($criteria) return Collaborator::forTicket($this->getId(), $criteria); if (!isset($this->collaborators)) $this->collaborators = Collaborator::forTicket($this->getId()); return $this->collaborators; } //UserList of recipients (owner + collaborators) function getRecipients() { if (!isset($this->recipients)) { $list = new UserList(); $list->add($this->getOwner()); if ($collabs = $this->getActiveCollaborators()) { foreach ($collabs as $c) $list->add($c); } $this->recipients = $list; } return $this->recipients; } function addCollaborator($user, $vars, &$errors) { if (!$user || $user->getId()==$this->getOwnerId()) return null; $vars = array_merge(array( 'ticketId' => $this->getId(), 'userId' => $user->getId()), $vars); if (!($c=Collaborator::add($vars, $errors))) return null; $this->collaborators = null; $this->recipients = null; return $c; } //XXX: Ugly for now function updateCollaborators($vars, &$errors) { global $thisstaff; if (!$thisstaff) return; //Deletes if($vars['del'] && ($ids=array_filter($vars['del']))) { $collabs = array(); foreach ($ids as $k => $cid) { if (($c=Collaborator::lookup($cid)) && $c->getTicketId() == $this->getId() && $c->remove()) $collabs[] = $c; } $this->logNote(_S('Collaborators Removed'), implode("
", $collabs), $thisstaff, false); } //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); unset($this->ht['active_collaborators']); $this->collaborators = null; return true; } /* -------------------- Setters --------------------- */ function setLastMsgId($msgid) { return $this->lastMsgId=$msgid; } function setLastMessage($message) { $this->last_message = $message; $this->setLastMsgId($message->getId()); } //DeptId can NOT be 0. No orphans please! function setDeptId($deptId) { //Make sure it's a valid department// if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId()) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), dept_id='.db_input($deptId) .' WHERE ticket_id='.db_input($this->getId()); return (db_query($sql) && db_affected_rows()); } //Set staff ID...assign/unassign/release (id can be 0) function setStaffId($staffId) { if(!is_numeric($staffId)) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId) .' WHERE ticket_id='.db_input($this->getId()); if (!db_query($sql) || !db_affected_rows()) return false; $this->staff = null; $this->ht['staff_id'] = $staffId; return true; } function setSLAId($slaId) { if ($slaId == $this->getSLAId()) return true; return db_query( 'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId) .' WHERE ticket_id='.db_input($this->getId())) && db_affected_rows(); } /** * Selects the appropriate service-level-agreement plan for this ticket. * When tickets are transfered between departments, the SLA of the new * department should be applied to the ticket. This would be useful, * for instance, if the ticket is transferred to a different department * which has a shorter grace period, the ticket should be considered * overdue in the shorter window now that it is owned by the new * department. * * $trump - if received, should trump any other possible SLA source. * This is used in the case of email filters, where the SLA * specified in the filter should trump any other SLA to be * considered. */ function selectSLAId($trump=null) { global $cfg; # XXX Should the SLA be overridden if it was originally set via an # email filter? This method doesn't consider such a case if ($trump && is_numeric($trump)) { $slaId = $trump; } elseif ($this->getDept() && $this->getDept()->getSLAId()) { $slaId = $this->getDept()->getSLAId(); } elseif ($this->getTopic() && $this->getTopic()->getSLAId()) { $slaId = $this->getTopic()->getSLAId(); } else { $slaId = $cfg->getDefaultSLAId(); } return ($slaId && $this->setSLAId($slaId)) ? $slaId : false; } //Set team ID...assign/unassign/release (id can be 0) function setTeamId($teamId) { if(!is_numeric($teamId)) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId) .' WHERE ticket_id='.db_input($this->getId()); return (db_query($sql) && db_affected_rows()); } //Status helper. function setStatus($status, $comments='', $set_closing_agent=true) { global $thisstaff; if ($status && is_numeric($status)) $status = TicketStatus::lookup($status); if (!$status || !$status instanceof TicketStatus) return false; // XXX: intercept deleted status and do hard delete if (!strcasecmp($status->getState(), 'deleted')) return $this->delete($comments); if ($this->getStatusId() == $status->getId()) return true; $sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '. ' ,status_id='.db_input($status->getId()); $ecb = null; switch($status->getState()) { case 'closed': $sql.=', closed=NOW(), duedate=NULL '; if ($thisstaff && $set_closing_agent) $sql.=', staff_id='.db_input($thisstaff->getId()); $this->clearOverdue(); $ecb = function($t) { $t->reload(); $t->logEvent('closed'); $t->deleteDrafts(); }; return $this->close(); break; case 'open': // TODO: check current status if it allows for reopening if ($this->isClosed()) { $sql .= ',closed=NULL, reopened=NOW() '; $ecb = function ($t) { $t->logEvent('reopened', 'closed'); }; } // If the ticket is not open then clear answered flag if (!$this->isOpen()) $sql .= ', isanswered = 0 '; break; default: return false; } $sql.=' WHERE ticket_id='.db_input($this->getId()); if (!db_query($sql) || !db_affected_rows()) return false; // Log status change b4 reload — if currently has a status. (On new // ticket, the ticket is opened and thereafter the status is set to // the requested status). if ($current_status = $this->getStatus()) { $note = sprintf(__('Status changed from %s to %s by %s'), $this->getStatus(), $status, $thisstaff ?: 'SYSTEM'); $alert = false; if ($comments) { $note .= sprintf('
%s', $comments); // Send out alerts if comments are included $alert = true; } $this->logNote(__('Status Changed'), $note, $thisstaff, $alert); } // Log events via callback if ($ecb) $ecb($this); return true; } function setState($state, $alerts=false) { switch(strtolower($state)) { case 'open': return $this->setStatus('open'); break; case 'closed': return $this->setStatus('closed'); break; case 'answered': return $this->setAnsweredState(1); break; case 'unanswered': return $this->setAnsweredState(0); break; case 'overdue': return $this->markOverdue(); break; case 'notdue': return $this->clearOverdue(); break; case 'unassined': return $this->unassign(); } return false; } function setAnsweredState($isanswered) { $sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered) .' WHERE ticket_id='.db_input($this->getId()); return (db_query($sql) && db_affected_rows()); } //Close the ticket function close() { global $thisstaff; $sql='UPDATE '.TICKET_TABLE.' SET closed=NOW(),isoverdue=0, duedate=NULL, updated=NOW(), status='.db_input('closed'); if($thisstaff) //Give the closing staff credit. $sql.=', staff_id='.db_input($thisstaff->getId()); $sql.=' WHERE ticket_id='.db_input($this->getId()); if(!db_query($sql) || !db_affected_rows()) return false; $this->reload(); $this->logEvent('closed'); return true; } function reopen() { global $cfg; if (!$this->isClosed()) return false; // Set status to open based on current closed status settings // If the closed status doesn't have configured "reopen" status then use the // the default ticket status. if (!($status=$this->getStatus()->getReopenStatus())) $status = $cfg->getDefaultTicketStatusId(); return $status ? $this->setStatus($status, 'Reopened') : false; } function onNewTicket($message, $autorespond=true, $alertstaff=true) { global $cfg; //Log stuff here... if(!$autorespond && !$alertstaff) return true; //No alerts to send. /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/ $this->reload(); //get the new goodies. 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(), 'thread'=>$message); //Send auto response - if enabled. if($autorespond && $cfg->autoRespONNewTicket() && $dept->autoRespONNewTicket() && ($msg=$tpl->getAutoRespMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('message' => $message, 'recipient' => $this->getOwner(), 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'') ); $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'], null, $options); } //Send alert to out sleepy & idle staff. if ($alertstaff && $cfg->alertONNewTicket() && ($email=$dept->getAlertEmail()) && ($msg=$tpl->getNewTicketAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('message' => $message)); $recipients=$sentlist=array(); //Exclude the auto responding email just incase it's from staff member. if ($message->isAutoReply()) $sentlist[] = $this->getEmail(); //Alert admin?? if($cfg->alertAdminONNewTicket()) { $alert = $this->replaceVars($msg, array('recipient' => 'Admin')); $email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[]=$cfg->getAdminEmail(); } //Only alerts dept members if the ticket is NOT assigned. if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) { if(($members=$dept->getMembersForAlerts())) $recipients=array_merge($recipients, $members); } if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager())) $recipients[]= $manager; // Account manager if ($cfg->alertAcctManagerONNewMessage() && ($org = $this->getOwner()->getOrganization()) && ($acct_manager = $org->getAccountManager())) { if ($acct_manager instanceof Team) $recipients = array_merge($recipients, $acct_manager->getMembers()); else $recipients[] = $acct_manager; } foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } return true; } function onOpenLimit($sendNotice=true) { global $ost, $cfg; //Log the limit notice as a warning for admin. $msg=sprintf(_S('Maximum open tickets (%1$d) reached for %2$s'), $cfg->getMaxOpenTickets(), $this->getEmail()); $ost->logWarning(sprintf(_S('Maximum Open Tickets Limit (%s)'),$this->getEmail()), $msg); if(!$sendNotice || !$cfg->sendOverLimitNotice()) return true; //Send notice to user. 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():'')); $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']); } $user = $this->getOwner(); //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think. $alert=sprintf(__('Maximum open tickets reached for %s.'), $this->getEmail())."\n" .sprintf(__('Open tickets: %d'), $user->getNumOpenTickets())."\n" .sprintf(__('Max allowed: %d'), $cfg->getMaxOpenTickets()) ."\n\n".__("Notice sent to the user."); $ost->alertAdmin(__('Overlimit Notice'), $alert); return true; } function onResponse($response, $options=array()) { db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1, lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId())); $this->reload(); $vars = array_merge($options, array( 'activity' => _S('New Response'), 'threadentry' => $response)); $this->onActivity($vars); } /* * Notify collaborators on response or new message * */ function notifyCollaborators($entry, $vars = array()) { global $cfg; if (!$entry instanceof ThreadEntry || !($recipients=$this->getRecipients()) || !($dept=$this->getDept()) || !($tpl=$dept->getTemplate()) || !($msg=$tpl->getActivityNoticeMsgTemplate()) || !($email=$dept->getEmail())) return; //Who posted the entry? $skip = array(); if ($entry instanceof Message) { $poster = $entry->getUser(); // Skip the person who sent in the message $skip[$entry->getUserId()] = 1; // Skip all the other recipients of the message foreach ($entry->getAllEmailRecipients() as $R) { foreach ($recipients as $R2) { if ($R2->getEmail() == ($R->mailbox.'@'.$R->hostname)) { $skip[$R2->getUserId()] = true; break; } } } } else { $poster = $entry->getStaff(); // Skip the ticket owner $skip[$this->getUserId()] = 1; } $vars = array_merge($vars, array( 'message' => (string) $entry, 'poster' => $poster ?: _S('A collaborator'), ) ); $msg = $this->replaceVars($msg->asArray(), $vars); $attachments = $cfg->emailAttachments()?$entry->getAttachments():array(); $options = array('inreplyto' => $entry->getEmailMessageId(), 'thread' => $entry); foreach ($recipients as $recipient) { // Skip folks who have already been included on this part of // the conversation if (isset($skip[$recipient->getUserId()])) continue; $notice = $this->replaceVars($msg, array('recipient' => $recipient)); $email->send($recipient, $notice['subj'], $notice['body'], $attachments, $options); } return; } function onMessage($message, $autorespond=true) { global $cfg; db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId())); // Auto-assign to closing staff or last respondent // If the ticket is closed and auto-claim is not enabled then put the // ticket back to unassigned pool. if ($this->isClosed() && !$cfg->autoClaimTickets()) { $this->setStaffId(0); } elseif(!($staff=$this->getStaff()) || !$staff->isAvailable()) { // Ticket has no assigned staff - if auto-claim is enabled then // try assigning it to the last respondent (if available) // otherwise leave the ticket unassigned. if ($cfg->autoClaimTickets() //Auto claim is enabled. && ($lastrep=$this->getLastRespondent()) && $lastrep->isAvailable()) { $this->setStaffId($lastrep->getId()); //direct assignment; } else { $this->setStaffId(0); //unassign - last respondent is not available. } } // Reopen if closed AND reopenable // We're also checking autorespond flag because we don't want to // reopen closed tickets on auto-reply from end user. This is not to // confused with autorespond on new message setting if ($autorespond && $this->isClosed() && $this->isReopenable()) $this->reopen(); /********** double check auto-response ************/ if (!($user = $message->getUser())) $autorespond=false; elseif ($autorespond && (Email::getIdByEmail($user->getEmail()))) $autorespond=false; elseif ($autorespond && ($dept=$this->getDept())) $autorespond=$dept->autoRespONNewMessage(); if(!$autorespond || !$cfg->autoRespONNewMessage() || !$message) return; //no autoresp or alerts. $this->reload(); $dept = $this->getDept(); $email = $dept->getAutoRespEmail(); //If enabled...send confirmation to user. ( New Message AutoResponse) if($email && ($tpl=$dept->getTemplate()) && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array( 'recipient' => $user, 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); $options = array( 'inreplyto'=>$message->getEmailMessageId(), 'thread'=>$message); $email->sendAutoReply($user, $msg['subj'], $msg['body'], null, $options); } } function onActivity($vars, $alert=true) { global $cfg, $thisstaff; //TODO: do some shit if (!$alert // Check if alert is enabled || !$cfg->alertONNewActivity() || !($dept=$this->getDept()) || !($email=$cfg->getAlertEmail()) || !($tpl = $dept->getTemplate()) || !($msg=$tpl->getNoteAlertMsgTemplate())) return; // Alert recipients $recipients=array(); //Last respondent. if ($cfg->alertLastRespondentONNewActivity()) $recipients[] = $this->getLastRespondent(); // Assigned staff / team if ($cfg->alertAssignedONNewActivity()) { if (isset($vars['assignee']) && $vars['assignee'] instanceof Staff) $recipients[] = $vars['assignee']; elseif ($this->isOpen() && ($assignee = $this->getStaff())) $recipients[] = $assignee; if ($team = $this->getTeam()) $recipients = array_merge($recipients, $team->getMembers()); } // Dept manager if ($cfg->alertDeptManagerONNewActivity() && $dept && $dept->getManagerId()) $recipients[] = $dept->getManager(); $options = array(); $staffId = $thisstaff ? $thisstaff->getId() : 0; if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) { $options = array( 'inreplyto' => $vars['threadentry']->getEmailMessageId(), 'references' => $vars['threadentry']->getEmailReferences(), 'thread' => $vars['threadentry']); // Activity details if (!$vars['comments']) $vars['comments'] = $vars['threadentry']; // Staff doing the activity $staffId = $vars['threadentry']->getStaffId() ?: $staffId; } $msg = $this->replaceVars($msg->asArray(), array( 'note' => $vars['threadentry'], // For compatibility 'activity' => $vars['activity'], 'comments' => $vars['comments'])); $isClosed = $this->isClosed(); $sentlist=array(); foreach ($recipients as $k=>$staff) { if (!is_object($staff) // Don't bother vacationing staff. || !$staff->isAvailable() // No need to alert the poster! || $staffId == $staff->getId() // No duplicates. || isset($sentlist[$staff->getEmail()]) // Make sure staff has access to ticket || ($isClosed && !$this->checkStaffAccess($staff)) ) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); $sentlist[$staff->getEmail()] = 1; } } function onAssign($assignee, $comments, $alert=true) { global $cfg, $thisstaff; if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign? //Assignee must be an object of type Staff or Team if(!$assignee || !is_object($assignee)) return false; $this->reload(); $comments = $comments ?: _S('Ticket assignment'); $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)'); //Log an internal note - no alerts on the internal note. $note = $this->logNote( sprintf(_S('Ticket Assigned to %s'), $assignee->getName()), $comments, $assigner, false); //See if we need to send alerts if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts! $dept = $this->getDept(); if(!$dept || !($tpl = $dept->getTemplate()) || !($email = $dept->getAlertEmail())) return true; //recipients $recipients=array(); if ($assignee instanceof Staff) { if ($cfg->alertStaffONAssignment()) $recipients[] = $assignee; } elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) { if ($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembers())) $recipients = array_merge($recipients, $members); elseif ($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead())) $recipients[] = $lead; } //Get the message template if ($recipients && ($msg=$tpl->getAssignedAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $comments, 'assignee' => $assignee, 'assigner' => $assigner )); //Send the alerts. $sentlist=array(); $options = array( 'inreplyto'=>$note->getEmailMessageId(), 'references'=>$note->getEmailReferences(), 'thread'=>$note); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } return true; } function onOverdue($whine=true, $comments="") { global $cfg; if($whine && ($sla=$this->getSLA()) && !$sla->alertOnOverdue()) $whine = false; //check if we need to send alerts. if(!$whine || !$cfg->alertONOverdueTicket() || !($dept = $this->getDept())) return true; //Get the message template if(($tpl = $dept->getTemplate()) && ($msg=$tpl->getOverdueAlertMsgTemplate()) && ($email = $dept->getAlertEmail())) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $comments)); //recipients $recipients=array(); //Assigned staff or team... if any if($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) { if($this->getStaffId()) $recipients[]=$this->getStaff(); elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers())) $recipients=array_merge($recipients, $members); } elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) { //Only alerts dept members if the ticket is NOT assigned. if ($members = $dept->getMembersForAlerts()) $recipients = array_merge($recipients, $members); } //Always alert dept manager?? if($cfg->alertDeptManagerONOverdueTicket() && $dept && ($manager=$dept->getManager())) $recipients[]= $manager; $sentlist=array(); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); $email->sendAlert($staff, $alert['subj'], $alert['body'], null); $sentlist[] = $staff->getEmail(); } } return true; } //ticket obj as variable = ticket number. function asVar() { return $this->getNumber(); } function getVar($tag) { global $cfg; if($tag && is_callable(array($this, 'get'.ucfirst($tag)))) return call_user_func(array($this, 'get'.ucfirst($tag))); switch(mb_strtolower($tag)) { case 'phone': case 'phone_number': return $this->getPhoneNumber(); break; case 'auth_token': return $this->getAuthToken(); break; case 'client_link': return sprintf('%s/view.php?t=%s', $cfg->getBaseUrl(), $this->getNumber()); break; case 'staff_link': return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId()); break; case 'create_date': return Format::date( $cfg->getDateTimeFormat(), Misc::db2gmtime($this->getCreateDate()), $cfg->getTZOffset(), $cfg->observeDaylightSaving()); break; case 'due_date': $duedate =''; if($this->getEstDueDate()) $duedate = Format::date( $cfg->getDateTimeFormat(), Misc::db2gmtime($this->getEstDueDate()), $cfg->getTZOffset(), $cfg->observeDaylightSaving()); return $duedate; break; case 'close_date': $closedate =''; if($this->isClosed()) $closedate = Format::date( $cfg->getDateTimeFormat(), Misc::db2gmtime($this->getCloseDate()), $cfg->getTZOffset(), $cfg->observeDaylightSaving()); return $closedate; break; case 'user': return $this->getOwner(); break; default: if (isset($this->_answers[$tag])) // The answer object is retrieved here which will // automatically invoke the toString() method when the // answer is coerced into text return $this->_answers[$tag]; } return false; } //Replace base variables. function replaceVars($input, $vars = array()) { global $ost; $vars = array_merge($vars, array('ticket' => $this)); return $ost->replaceTemplateVariables($input, $vars); } function markUnAnswered() { return (!$this->isAnswered() || $this->setAnsweredState(0)); } function markAnswered() { return ($this->isAnswered() || $this->setAnsweredState(1)); } function markOverdue($whine=true) { global $cfg; if($this->isOverdue()) return true; $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1, updated=NOW() ' .' WHERE ticket_id='.db_input($this->getId()); if(!db_query($sql) || !db_affected_rows()) return false; $this->logEvent('overdue'); $this->onOverdue($whine); return true; } function clearOverdue() { if(!$this->isOverdue()) return true; //NOTE: Previously logged overdue event is NOT annuled. $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() '; //clear due date if it's in the past if($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime()) $sql.=', duedate=NULL'; //Clear SLA if est. due date is in the past if($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime()) $sql.=', sla_id=0 '; $sql.=' WHERE ticket_id='.db_input($this->getId()); return (db_query($sql) && db_affected_rows()); } //Dept Tranfer...with alert.. done by staff function transfer($deptId, $comments, $alert = true) { global $cfg, $thisstaff; if(!$thisstaff || !$thisstaff->canTransferTickets()) return false; $currentDept = $this->getDeptName(); //Current department if(!$deptId || !$this->setDeptId($deptId)) return false; // Reopen ticket if closed if($this->isClosed()) $this->reopen(); $this->reload(); $dept = $this->getDept(); // Set SLA of the new department if(!$this->getSLAId() || $this->getSLA()->isTransient()) $this->selectSLAId(); // Make sure the new department allows assignment to the // currently assigned agent (if any) if ($this->isAssigned() && ($staff=$this->getStaff()) && $dept->assignMembersOnly() && !$dept->isMember($staff)) { $this->setStaffId(0); } /*** log the transfer comments as internal note - with alerts disabled - ***/ $title=sprintf(_S('Ticket transferred from %1$s to %2$s'), $currentDept, $this->getDeptName()); $comments=$comments?$comments:$title; $note = $this->logNote($title, $comments, $thisstaff, false); $this->logEvent('transferred'); //Send out alerts if enabled AND requested if (!$alert || !$cfg->alertONTransfer()) return true; //no alerts!! if (($email = $dept->getAlertEmail()) && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getTransferAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $comments, 'staff' => $thisstaff)); //recipients $recipients=array(); //Assigned staff or team... if any if($this->isAssigned() && $cfg->alertAssignedONTransfer()) { if($this->getStaffId()) $recipients[]=$this->getStaff(); elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers())) $recipients = array_merge($recipients, $members); } elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) { //Only alerts dept members if the ticket is NOT assigned. if(($members=$dept->getMembersForAlerts())) $recipients = array_merge($recipients, $members); } //Always alert dept manager?? if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager())) $recipients[]= $manager; $sentlist=array(); $options = array( 'inreplyto'=>$note->getEmailMessageId(), 'references'=>$note->getEmailReferences(), 'thread'=>$note); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } return true; } function claim() { global $thisstaff; if (!$thisstaff || !$this->isOpen() || $this->isAssigned()) return false; $dept = $this->getDept(); if ($dept->assignMembersOnly() && !$dept->isMember($thisstaff)) return false; $comments = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName()); return $this->assignToStaff($thisstaff->getId(), $comments, false); } function assignToStaff($staff, $note, $alert=true) { if(!is_object($staff) && !($staff=Staff::lookup($staff))) return false; if (!$staff->isAvailable() || !$this->setStaffId($staff->getId())) return false; $this->onAssign($staff, $note, $alert); $this->logEvent('assigned'); return true; } function assignToTeam($team, $note, $alert=true) { if(!is_object($team) && !($team=Team::lookup($team))) return false; if (!$team->isActive() || !$this->setTeamId($team->getId())) return false; //Clear - staff if it's a closed ticket // staff_id is overloaded -> assigned to & closed by. if($this->isClosed()) $this->setStaffId(0); $this->onAssign($team, $note, $alert); $this->logEvent('assigned'); return true; } //Assign ticket to staff or team - overloaded ID. function assign($assignId, $note, $alert=true) { global $thisstaff; $rv=0; $id=preg_replace("/[^0-9]/", "", $assignId); if($assignId[0]=='t') { $rv=$this->assignToTeam($id, $note, $alert); } elseif($assignId[0]=='s' || is_numeric($assignId)) { $alert=($alert && $thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!! //We don't care if a team is already assigned to the ticket - staff assignment takes precedence $rv=$this->assignToStaff($id, $note, $alert); } return $rv; } //unassign primary assignee function unassign() { if(!$this->isAssigned()) //We can't release what is not assigned buddy! return true; //We can only unassigned OPEN tickets. if($this->isClosed()) return false; //Unassign staff (if any) if($this->getStaffId() && !$this->setStaffId(0)) return false; //unassign team (if any) if($this->getTeamId() && !$this->setTeamId(0)) return false; $this->reload(); return true; } function release() { return $this->unassign(); } //Change ownership function changeOwner($user) { global $thisstaff; if (!$user || ($user->getId() == $this->getOwnerId()) || !$thisstaff->canEditTickets()) return false; $sql ='UPDATE '.TICKET_TABLE.' SET updated = NOW() ' .', user_id = '.db_input($user->getId()) .' WHERE ticket_id = '.db_input($this->getId()); if (!db_query($sql) || !db_affected_rows()) return false; $this->ht['user_id'] = $user->getId(); $this->user = null; $this->collaborators = null; $this->recipients = null; //Log an internal note $note = sprintf(_S('%s changed ticket ownership to %s'), $thisstaff->getName(), $user->getName()); //Remove the new owner from list of collaborators $c = Collaborator::lookup(array('userId' => $user->getId(), 'ticketId' => $this->getId())); if ($c && $c->remove()) $note.= ' '._S('(removed as collaborator)'); $this->logNote('Ticket ownership changed', $note); return true; } //Insert message from client function postMessage($vars, $origin='', $alerts=true) { global $cfg; $vars['origin'] = $origin; if(isset($vars['ip'])) $vars['ip_address'] = $vars['ip']; elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; $errors = array(); if(!($message = $this->getThread()->addMessage($vars, $errors))) return null; $this->setLastMessage($message); //Add email recipients as collaborators... if ($vars['recipients'] && (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail())) //Only add if we have a matched local address && $vars['to-email-id']) { //New collaborators added by other collaborators are disable -- // requires staff approval. $info = array( 'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0); $collabs = array(); foreach ($vars['recipients'] as $recipient) { // Skip virtual delivered-to addresses if (strcasecmp($recipient['source'], 'delivered-to') === 0) continue; if (($user=User::fromVars($recipient))) if ($c=$this->addCollaborator($user, $info, $errors)) $collabs[] = sprintf('%s%s', (string) $c, $recipient['source'] ? " ".sprintf(_S('via %s'), $recipient['source']) : '' ); } //TODO: Can collaborators add others? if ($collabs) { //TODO: Change EndUser to name of user. $this->logNote(_S('Collaborators added by end user'), implode("
", $collabs), _S('End User'), false); } } if(!$alerts) return $message; //Our work is done... // Do not auto-respond to bounces and other auto-replies $autorespond = isset($vars['flags']) ? !$vars['flags']['bounce'] && !$vars['flags']['auto-reply'] : true; if ($autorespond && $message->isAutoReply()) $autorespond = false; $this->onMessage($message, $autorespond); //must be called b4 sending alerts to staff. if ($autorespond && $cfg && $cfg->notifyCollabsONNewMessage()) $this->notifyCollaborators($message, array('signature' => '')); $dept = $this->getDept(); $variables = array( 'message' => $message, 'poster' => ($vars['poster'] ? $vars['poster'] : $this->getName()) ); $options = array( 'inreplyto' => $message->getEmailMessageId(), 'references' => $message->getEmailReferences(), 'thread'=>$message); //If enabled...send alert to staff (New Message Alert) if($cfg->alertONNewMessage() && ($email = $dept->getAlertEmail()) && ($tpl = $dept->getTemplate()) && ($msg = $tpl->getNewMessageAlertMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), $variables); //Build list of recipients and fire the alerts. $recipients=array(); //Last respondent. if($cfg->alertLastRespondentONNewMessage() || $cfg->alertAssignedONNewMessage()) $recipients[]=$this->getLastRespondent(); //Assigned staff if any...could be the last respondent if ($cfg->alertAssignedONNewMessage() && $this->isAssigned()) { if ($staff = $this->getStaff()) $recipients[] = $staff; elseif ($team = $this->getTeam()) $recipients = array_merge($recipients, $team->getMembers()); } //Dept manager if($cfg->alertDeptManagerONNewMessage() && $dept && ($manager=$dept->getManager())) $recipients[]=$manager; // Account manager if ($cfg->alertAcctManagerONNewMessage() && ($org = $this->getOwner()->getOrganization()) && ($acct_manager = $org->getAccountManager())) { if ($acct_manager instanceof Team) $recipients = array_merge($recipients, $acct_manager->getMembers()); else $recipients[] = $acct_manager; } $sentlist=array(); //I know it sucks...but..it works. foreach( $recipients as $k=>$staff) { if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } return $message; } function postCannedReply($canned, $msgId, $alert=true) { global $ost, $cfg; if((!is_object($canned) && !($canned=Canned::lookup($canned))) || !$canned->isEnabled()) return false; $files = array(); foreach ($canned->attachments->getAll() as $file) $files[] = $file['id']; if ($cfg->isHtmlThreadEnabled()) $response = new HtmlThreadBody( $this->replaceVars($canned->getHtml())); else $response = new TextThreadBody( $this->replaceVars($canned->getPlainText())); $info = array('msgId' => $msgId, 'poster' => __('SYSTEM (Canned Reply)'), 'response' => $response, 'cannedattachments' => $files); $errors = array(); if(!($response=$this->postReply($info, $errors, false))) return null; $this->markUnAnswered(); if(!$alert) return $response; $dept = $this->getDept(); if(($email=$dept->getEmail()) && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getAutoReplyMsgTemplate())) { if($dept && $dept->isPublic()) $signature=$dept->getSignature(); else $signature=''; $msg = $this->replaceVars($msg->asArray(), array( 'response' => $response, 'signature' => $signature, 'recipient' => $this->getOwner(), )); $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array(); $options = array( 'inreplyto'=>$response->getEmailMessageId(), 'references'=>$response->getEmailReferences(), 'thread'=>$response); $email->sendAutoReply($this, $msg['subj'], $msg['body'], $attachments, $options); } return $response; } /* public */ function postReply($vars, &$errors, $alert = true) { global $thisstaff, $cfg; if(!$vars['poster'] && $thisstaff) $vars['poster'] = $thisstaff; if(!$vars['staffId'] && $thisstaff) $vars['staffId'] = $thisstaff->getId(); if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; if(!($response = $this->getThread()->addResponse($vars, $errors))) return null; //Check for actions in the subject if($vars['subject'] && preg_match ("/#close/", $vars['subject'], $action)) { return $this->close(); } $assignee = $this->getStaff(); // Set status - if checked. if ($vars['reply_status_id'] && $vars['reply_status_id'] != $this->getStatusId()) $this->setStatus($vars['reply_status_id']); // Claim on response bypasses the department assignment restrictions if($thisstaff && $this->isOpen() && !$this->getStaffId() && $cfg->autoClaimTickets()) $this->setStaffId($thisstaff->getId()); //direct assignment; $this->onResponse($response, array('assignee' => $assignee)); //do house cleaning.. /* email the user?? - if disabled - then bail out */ if (!$alert) return $response; $dept = $this->getDept(); 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(), 'thread'=>$response); if(($email=$dept->getEmail()) && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), $variables + array('recipient' => $this->getOwner())); $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); $email->send($this->getOwner(), $msg['subj'], $msg['body'], $attachments, $options); } if($vars['emailcollab']) $this->notifyCollaborators($response, array('signature' => $signature)); return $response; } //Activity log - saved as internal notes WHEN enabled!! function logActivity($title, $note) { return $this->logNote($title, $note, 'SYSTEM', false); } // History log -- used for statistics generation (pretty reports) function logEvent($state, $annul=null, $staff=null) { global $thisstaff; if ($staff === null) { if ($thisstaff) $staff=$thisstaff->getUserName(); else $staff='SYSTEM'; # XXX: Security Violation ? } # Annul previous entries if requested (for instance, reopening a # ticket will annul an 'closed' entry). This will be useful to # easily prevent repeated statistics. if ($annul) { db_query('UPDATE '.TICKET_EVENT_TABLE.' SET annulled=1' .' WHERE ticket_id='.db_input($this->getId()) .' AND state='.db_input($annul)); } return db_query('INSERT INTO '.TICKET_EVENT_TABLE .' SET ticket_id='.db_input($this->getId()) .', staff_id='.db_input($this->getStaffId()) .', team_id='.db_input($this->getTeamId()) .', dept_id='.db_input($this->getDeptId()) .', topic_id='.db_input($this->getTopicId()) .', timestamp=NOW(), state='.db_input($state) .', staff='.db_input($staff)) && db_affected_rows() == 1; } //Insert Internal Notes function logNote($title, $note, $poster='SYSTEM', $alert=true) { $errors = array(); //Unless specified otherwise, assume HTML if ($note && is_string($note)) $note = new HtmlThreadBody($note); return $this->postNote( array( 'title' => $title, 'note' => $note, ), $errors, $poster, $alert ); } function postNote($vars, &$errors, $poster, $alert=true) { global $cfg, $thisstaff; //Who is posting the note - staff or system? $vars['staffId'] = 0; $vars['poster'] = 'SYSTEM'; if($poster && is_object($poster)) { $vars['staffId'] = $poster->getId(); $vars['poster'] = $poster->getName(); }elseif($poster) { //string $vars['poster'] = $poster; } if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; if(!($note=$this->getThread()->addNote($vars, $errors))) return null; $alert = $alert && ( isset($vars['flags']) // No alerts for bounce and auto-reply emails ? !$vars['flags']['bounce'] && !$vars['flags']['auto-reply'] : true ); // Get assigned staff just in case the ticket is closed. $assignee = $this->getStaff(); if ($vars['note_status_id'] && ($status=TicketStatus::lookup($vars['note_status_id']))) { if ($this->setStatus($status)) $this->reload(); } $activity = $vars['activity'] ?: _S('New Internal Note'); $this->onActivity(array( 'activity' => $activity, 'threadentry' => $note, 'assignee' => $assignee ), $alert); return $note; } //Print ticket... export the ticket thread as PDF. function pdfExport($psize='Letter', $notes=false) { global $thisstaff; require_once(INCLUDE_DIR.'class.pdf.php'); if (!is_string($psize)) { if ($_SESSION['PAPER_SIZE']) $psize = $_SESSION['PAPER_SIZE']; elseif (!$thisstaff || !($psize = $thisstaff->getDefaultPaperSize())) $psize = 'Letter'; } $pdf = new Ticket2PDF($this, $psize, $notes); $name='Ticket-'.$this->getNumber().'.pdf'; $pdf->Output($name, 'I'); //Remember what the user selected - for autoselect on the next print. $_SESSION['PAPER_SIZE'] = $psize; exit; } function delete($comments='') { global $ost, $thisstaff; //delete just orphaned ticket thread & associated attachments. // Fetch thread prior to removing ticket entry $t = $this->getThread(); $sql = 'DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1'; if(!db_query($sql) || !db_affected_rows()) return false; $t->delete(); foreach (DynamicFormEntry::forTicket($this->getId()) as $form) $form->delete(); $this->deleteDrafts(); $sql = 'DELETE FROM '.TICKET_TABLE.'__cdata WHERE `ticket_id`=' .db_input($this->getId()); // If the CDATA table doesn't exist, that's not an error db_query($sql, false); // Log delete $log = sprintf(__('Ticket #%1$s deleted by %2$s'), $this->getNumber(), $thisstaff ? $thisstaff->getName() : __('SYSTEM')); if ($comments) $log .= sprintf('
%s', $comments); $ost->logDebug( sprintf( __('Ticket #%s deleted'), $this->getNumber()), $log); return true; } function deleteDrafts() { Draft::deleteForNamespace('ticket.%.' . $this->getId()); } function update($vars, &$errors) { global $cfg, $thisstaff; if(!$cfg || !$thisstaff || !$thisstaff->canEditTickets()) return false; $fields=array(); $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Help topic selection is required')); $fields['slaId'] = array('type'=>'int', 'required'=>0, 'error'=>__('Select a valid SLA')); $fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY')); $fields['note'] = array('type'=>'text', 'required'=>1, 'error'=>__('A reason for the update is required')); $fields['user_id'] = array('type'=>'int', 'required'=>0, 'error'=>__('Invalid user-id')); if(!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] = __('Missing or invalid data - check the errors and try again'); if($vars['duedate']) { if($this->isClosed()) $errors['duedate']=__('Due date can NOT be set on a closed ticket'); elseif(!$vars['time'] || strpos($vars['time'],':')===false) $errors['time']=__('Select a time from the list'); elseif(strtotime($vars['duedate'].' '.$vars['time'])===false) $errors['duedate']=__('Invalid due date'); elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time()) $errors['duedate']=__('Due date must be in the future'); } // Validate dynamic meta-data $forms = DynamicFormEntry::forTicket($this->getId()); foreach ($forms as $form) { // Don't validate deleted forms if (!in_array($form->getId(), $vars['forms'])) continue; $form->setSource($_POST); if (!$form->isValid()) $errors = array_merge($errors, $form->errors()); } if($errors) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() ' .' ,topic_id='.db_input($vars['topicId']) .' ,sla_id='.db_input($vars['slaId']) .' ,source='.db_input($vars['source']) .' ,duedate='.($vars['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))):'NULL'); if($vars['user_id']) $sql.=', user_id='.db_input($vars['user_id']); if($vars['duedate']) { //We are setting new duedate... $sql.=' ,isoverdue=0'; } $sql.=' WHERE ticket_id='.db_input($this->getId()); if(!db_query($sql) || !db_affected_rows()) return false; if(!$vars['note']) $vars['note']=sprintf(_S('Ticket details updated by %s'), $thisstaff->getName()); $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff); // Decide if we need to keep the just selected SLA $keepSLA = ($this->getSLAId() != $vars['slaId']); // Update dynamic meta-data foreach ($forms as $f) { // Drop deleted forms $idx = array_search($f->getId(), $vars['forms']); if ($idx === false) { $f->delete(); } else { $f->set('sort', $idx); $f->save(); } } // Reload the ticket so we can do further checking $this->reload(); // Reselect SLA if transient if (!$keepSLA && (!$this->getSLA() || $this->getSLA()->isTransient())) $this->selectSLAId(); // Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue. if($this->isOverdue() && (!$this->getEstDueDate() //Duedate + SLA cleared || Misc::db2gmtime($this->getEstDueDate()) > Misc::gmtime() //New due date in the future. )) { $this->clearOverdue(); } Signal::send('model.updated', $this); return true; } /*============== Static functions. Use Ticket::function(params); =============nolint*/ function getIdByNumber($number, $email=null) { if(!$number) return 0; $sql ='SELECT ticket.ticket_id FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' .' WHERE ticket.`number`='.db_input($number); if($email) $sql .= ' AND email.address = '.db_input($email); if(($res=db_query($sql)) && db_num_rows($res)) list($id)=db_fetch_row($res); return $id; } function lookup($id) { //Assuming local ID is the only lookup used! return ($id && is_numeric($id) && ($ticket= new Ticket($id)) && $ticket->getId()==$id) ?$ticket:null; } function lookupByNumber($number, $email=null) { return self::lookup(self:: getIdByNumber($number, $email)); } static function isTicketNumberUnique($number) { return 0 == db_num_rows(db_query( 'SELECT ticket_id FROM '.TICKET_TABLE.' WHERE `number`='.db_input($number))); } function getIdByMessageId($mid, $email) { if(!$mid || !$email) return 0; $sql='SELECT ticket.ticket_id FROM '.TICKET_TABLE. ' ticket '. ' LEFT JOIN '.TICKET_THREAD_TABLE.' msg USING(ticket_id) '. ' INNER JOIN '.TICKET_EMAIL_INFO_TABLE.' emsg ON (msg.id = emsg.message_id) '. ' WHERE email_mid='.db_input($mid).' AND email='.db_input($email); $id=0; if(($res=db_query($sql)) && db_num_rows($res)) list($id)=db_fetch_row($res); return $id; } /* Quick staff's tickets stats */ function getStaffStats($staff) { global $cfg; /* Unknown or invalid staff */ if(!$staff || (!is_object($staff) && !($staff=Staff::lookup($staff))) || !$staff->isStaff()) return null; $where = array('(ticket.staff_id='.db_input($staff->getId()) .' AND status.state="open")'); $where2 = ''; if(($teams=$staff->getTeams())) $where[] = ' ( ticket.team_id IN('.implode(',', db_input(array_filter($teams))) .') AND status.state="open")'; if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tickets. $where[] = 'ticket.dept_id IN('.implode(',', db_input($depts)).') '; if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets())) $where2 =' AND ticket.staff_id=0 '; $where = implode(' OR ', $where); if ($where) $where = 'AND ( '.$where.' ) '; $sql = 'SELECT \'open\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status ON (ticket.status_id=status.id AND status.state=\'open\') ' .'WHERE ticket.isanswered = 0 ' . $where . $where2 .'UNION SELECT \'answered\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status ON (ticket.status_id=status.id AND status.state=\'open\') ' .'WHERE ticket.isanswered = 1 ' . $where .'UNION SELECT \'overdue\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status ON (ticket.status_id=status.id AND status.state=\'open\') ' .'WHERE ticket.isoverdue =1 ' . $where .'UNION SELECT \'assigned\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status ON (ticket.status_id=status.id AND status.state=\'open\') ' .'WHERE ticket.staff_id = ' . db_input($staff->getId()) . ' ' . $where .'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets ' .'FROM ' . TICKET_TABLE . ' ticket ' .'INNER JOIN '.TICKET_STATUS_TABLE. ' status ON (ticket.status_id=status.id AND status.state=\'closed\' ) ' .'WHERE 1 ' . $where; $res = db_query($sql); $stats = array(); while($row = db_fetch_row($res)) { $stats[$row[0]] = $row[1]; } return $stats; } /* Quick client's tickets stats @email - valid email. */ function getUserStats($user) { if(!$user || !($user instanceof EndUser)) return null; $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed ' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.TICKET_TABLE.' open ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') ' .' LEFT JOIN '.TICKET_TABLE.' closed ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')' .' WHERE ticket.user_id = '.db_input($user->getId()); return db_fetch_array(db_query($sql)); } protected function filterTicketData($origin, $vars, $forms, $user=false) { global $cfg; // Unset all the filter data field data in case things change // during recursive calls foreach ($vars as $k=>$v) if (strpos($k, 'field.') === 0) unset($vars[$k]); foreach ($forms as $F) { if ($F) { $vars += $F->getFilterData(); } } if (!$user) { $interesting = array('name', 'email'); $user_form = UserForm::getUserForm()->getForm($vars); // Add all the user-entered info for filtering foreach ($interesting as $F) { $field = $user_form->getField($F); $vars[$F] = $field->toString($field->getClean()); } // Attempt to lookup the user and associated data $user = User::lookupByEmail($vars['email']); } // Add in user and organization data for filtering if ($user) { $vars += $user->getFilterData(); $vars['email'] = $user->getEmail(); $vars['name'] = $user->getName()->getOriginal(); if ($org = $user->getOrganization()) { $vars += $org->getFilterData(); } } // Don't include org information based solely on email domain // for existing user instances else { // Unpack all known user info from the request foreach ($user_form->getFields() as $f) { $vars['field.'.$f->get('id')] = $f->toString($f->getClean()); } // Add in organization data if one exists for this email domain list($mailbox, $domain) = explode('@', $vars['email'], 2); if ($org = Organization::forDomain($domain)) { $vars += $org->getFilterData(); } } try { // Make sure the email address is not banned if (TicketFilter::isBanned($vars['email'])) { throw new RejectedException(Banlist::getFilter(), $vars); } // Init ticket filters... $ticket_filter = new TicketFilter($origin, $vars); $ticket_filter->apply($vars); } catch (FilterDataChanged $ex) { // Don't pass user recursively, assume the user has changed return self::filterTicketData($origin, $ex->getData(), $forms); } return $vars; } /* * The mother of all functions...You break it you fix it! * * $autorespond and $alertstaff overrides config settings... */ static function create($vars, &$errors, $origin, $autorespond=true, $alertstaff=true) { global $ost, $cfg, $thisclient, $_FILES; // Don't enforce form validation for email $field_filter = function($type) use ($origin) { return function($f) use ($origin, $type) { // Ultimately, only offer validation errors for web for // non-internal fields. For email, no validation can be // performed. For other origins, validate as usual switch (strtolower($origin)) { case 'email': return false; case 'staff': // Required 'Contact Information' fields aren't required // when staff open tickets return $type != 'user' || in_array($f->get('name'), array('name','email')); case 'web': return !$f->get('private'); default: return true; } }; }; $reject_ticket = function($message) use (&$errors) { global $ost; $errors = array( 'errno' => 403, 'err' => __('This help desk is for use by authorized users only')); $ost->logWarning(_S('Ticket Denied'), $message, false); return 0; }; Signal::send('ticket.create.before', null, $vars); // Create and verify the dynamic form entry for the new ticket $form = TicketForm::getNewInstance(); $form->setSource($vars); // If submitting via email or api, ensure we have a subject and such if (!in_array(strtolower($origin), array('web', 'staff'))) { foreach ($form->getFields() as $field) { $fname = $field->get('name'); if ($fname && isset($vars[$fname]) && !$field->value) $field->value = $field->parse($vars[$fname]); } } if (!$form->isValid($field_filter('ticket'))) $errors += $form->errors(); if ($vars['uid']) $user = User::lookup($vars['uid']); $id=0; $fields=array(); $fields['message'] = array('type'=>'*', 'required'=>1, 'error'=>__('Message content is required')); switch (strtolower($origin)) { case 'web': $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Select a help topic')); break; case 'staff': $fields['deptId'] = array('type'=>'int', 'required'=>0, 'error'=>__('Department selection is required')); $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Help topic selection is required')); $fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY')); case 'api': $fields['source'] = array('type'=>'string', 'required'=>1, 'error'=>__('Indicate ticket source')); break; case 'email': $fields['emailId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Unknown system email')); break; default: # TODO: Return error message $errors['err']=$errors['origin'] = __('Invalid ticket origin given'); } if(!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] =__('Missing or invalid data - check the errors and try again'); //Make sure the due date is valid if($vars['duedate']) { if(!$vars['time'] || strpos($vars['time'],':')===false) $errors['time']=__('Select a time from the list'); elseif(strtotime($vars['duedate'].' '.$vars['time'])===false) $errors['duedate']=__('Invalid due date'); elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time()) $errors['duedate']=__('Due date must be in the future'); } if (!$errors) { # Perform ticket filter actions on the new ticket arguments $__form = null; if ($vars['topicId']) { if (($__topic=Topic::lookup($vars['topicId'])) && ($__form = $__topic->getForm()) ) { $__form = $__form->instanciate(); $__form->setSource($vars); } } try { $vars = self::filterTicketData($origin, $vars, array($form, $__form), $user); } catch (RejectedException $ex) { return $reject_ticket( sprintf(_S('Ticket rejected (%s) by filter "%s"'), $ex->vars['email'], $ex->getRejectingFilter()->getName()) ); } //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL) if ($cfg->getMaxOpenTickets() > 0 && strcasecmp($origin, 'staff') && ($_user=TicketUser::lookupByEmail($vars['email'])) && ($openTickets=$_user->getNumOpenTickets()) && ($openTickets>=$cfg->getMaxOpenTickets()) ) { $errors = array('err' => __("You've reached the maximum open tickets allowed.")); $ost->logWarning(sprintf(_S('Ticket denied - %s'), $vars['email']), sprintf(_S('Max open tickets (%1$d) reached for %2$s'), $cfg->getMaxOpenTickets(), $vars['email']), false); return 0; } // Allow vars to be changed in ticket filter and applied to the user // account created or detected if (!$user && $vars['email']) $user = User::lookupByEmail($vars['email']); if (!$user) { // Reject emails if not from registered clients (if // configured) if (strcasecmp($origin, 'email') === 0 && !$cfg->acceptUnregisteredEmail()) { list($mailbox, $domain) = explode('@', $vars['email'], 2); // Users not yet created but linked to an organization // are still acceptable if (!Organization::forDomain($domain)) { return $reject_ticket( sprintf(_S('Ticket rejected (%s) (unregistered client)'), $vars['email'])); } } $user_form = UserForm::getUserForm()->getForm($vars); if (!$user_form->isValid($field_filter('user')) || !($user=User::fromVars($user_form->getClean()))) $errors['user'] = __('Incomplete client information'); } } if ($vars['topicId']) { if ($topic=Topic::lookup($vars['topicId'])) { if ($topic_form = $topic->getForm()) { $TF = $topic_form->getForm($vars); $topic_form = $topic_form->instanciate(); $topic_form->setSource($vars); if (!$TF->isValid($field_filter('topic'))) $errors = array_merge($errors, $TF->errors()); } } else { $errors['topicId'] = 'Invalid help topic selected'; } } // Any error above is fatal. if ($errors) return 0; Signal::send('ticket.create.validated', null, $vars); # Some things will need to be unpacked back into the scope of this # function if (isset($vars['autorespond'])) $autorespond = $vars['autorespond']; # Apply filter-specific priority if ($vars['priorityId']) $form->setAnswer('priority', null, $vars['priorityId']); // If the filter specifies a help topic which has a form associated, // and there was previously either no help topic set or the help // topic did not have a form, there's no need to add it now as (1) // validation is closed, (2) there may be a form already associated // and filled out from the original help topic, and (3) staff // members can always add more forms now // OK...just do it. $statusId = $vars['statusId']; $deptId = $vars['deptId']; //pre-selected Dept if any. $source = ucfirst($vars['source']); // Apply email settings for emailed tickets. Email settings should // trump help topic settins if the email has an associated help // topic if ($vars['emailId'] && ($email=Email::lookup($vars['emailId']))) { $deptId = $deptId ?: $email->getDeptId(); $priority = $form->getAnswer('priority'); if (!$priority || !$priority->getIdValue()) $form->setAnswer('priority', null, $email->getPriorityId()); if ($autorespond) $autorespond = $email->autoRespond(); if (!isset($topic) && ($T = $email->getTopic()) && ($T->isActive())) { $topic = $T; } $email = null; $source = 'Email'; } if (!isset($topic)) { // This may return NULL, no big deal $topic = $cfg->getDefaultTopic(); } // Intenal mapping magic...see if we need to override anything if (isset($topic)) { $deptId = $deptId ?: $topic->getDeptId(); $statusId = $statusId ?: $topic->getStatusId(); $priority = $form->getAnswer('priority'); if (!$priority || !$priority->getIdValue()) $form->setAnswer('priority', null, $topic->getPriorityId()); if ($autorespond) $autorespond = $topic->autoRespond(); //Auto assignment. if (!isset($vars['staffId']) && $topic->getStaffId()) $vars['staffId'] = $topic->getStaffId(); elseif (!isset($vars['teamId']) && $topic->getTeamId()) $vars['teamId'] = $topic->getTeamId(); //set default sla. if (isset($vars['slaId'])) $vars['slaId'] = $vars['slaId'] ?: $cfg->getDefaultSLAId(); elseif ($topic && $topic->getSLAId()) $vars['slaId'] = $topic->getSLAId(); } // Auto assignment to organization account manager if (($org = $user->getOrganization()) && $org->autoAssignAccountManager() && ($code = $org->getAccountManagerId())) { if (!isset($vars['staffId']) && $code[0] == 's') $vars['staffId'] = substr($code, 1); elseif (!isset($vars['teamId']) && $code[0] == 't') $vars['teamId'] = substr($code, 1); } // Last minute checks $priority = $form->getAnswer('priority'); if (!$priority || !$priority->getIdValue()) $form->setAnswer('priority', null, $cfg->getDefaultPriorityId()); $deptId = $deptId ?: $cfg->getDefaultDeptId(); $statusId = $statusId ?: $cfg->getDefaultTicketStatusId(); $topicId = isset($topic) ? $topic->getId() : 0; $ipaddress = $vars['ip'] ?: $_SERVER['REMOTE_ADDR']; $source = $source ?: 'Web'; //We are ready son...hold on to the rails. $number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber(); $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() ' .' ,lastmessage= NOW()' .' ,user_id='.db_input($user->getId()) .' ,`number`='.db_input($number) .' ,dept_id='.db_input($deptId) .' ,topic_id='.db_input($topicId) .' ,ip_address='.db_input($ipaddress) .' ,source='.db_input($source); if (isset($vars['emailId']) && $vars['emailId']) $sql.=', email_id='.db_input($vars['emailId']); //Make sure the origin is staff - avoid firebug hack! if($vars['duedate'] && !strcasecmp($origin,'staff')) $sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))); if(!db_query($sql) || !($id=db_insert_id()) || !($ticket =Ticket::lookup($id))) return null; /* -------------------- POST CREATE ------------------------ */ // Save the (common) dynamic form $form->setTicketId($id); $form->save(); // Save the form data from the help-topic form, if any if ($topic_form) { $topic_form->setTicketId($id); $topic_form->save(); } $ticket->loadDynamicData(); $dept = $ticket->getDept(); // Add organizational collaborators if ($org && $org->autoAddCollabs()) { $pris = $org->autoAddPrimaryContactsAsCollabs(); $members = $org->autoAddMembersAsCollabs(); $settings = array('isactive' => true); $collabs = array(); foreach ($org->allMembers() as $u) { if ($members || ($pris && $u->isPrimaryContact())) { if ($c = $ticket->addCollaborator($u, $settings, $errors)) { $collabs[] = (string) $c; } } } //TODO: Can collaborators add others? if ($collabs) { //TODO: Change EndUser to name of user. $ticket->logNote(sprintf(_S('Collaborators for %s organization added'), $org->getName()), implode("
", $collabs), $org->getName(), false); } } //post the message. $vars['title'] = $vars['subject']; //Use the initial subject as title of the post. $vars['userId'] = $ticket->getUserId(); $message = $ticket->postMessage($vars , $origin, false); // Configure service-level-agreement for this ticket $ticket->selectSLAId($vars['slaId']); // Assign ticket to staff or team (new ticket by staff) if($vars['assignId']) { $ticket->assign($vars['assignId'], $vars['note']); } else { // Auto assign staff or team - auto assignment based on filter // rules. Both team and staff can be assigned if ($vars['staffId']) $ticket->assignToStaff($vars['staffId'], _S('Auto Assignment')); if ($vars['teamId']) // No team alert if also assigned to an individual agent $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'), !$vars['staffId']); } // Apply requested status — this should be done AFTER assignment, // because if it is requested to be closed, it should not cause the // ticket to be reopened for assignment. if ($statusId) $ticket->setStatus($statusId, false, false); /********** double check auto-response ************/ //Override auto responder if the FROM email is one of the internal emails...loop control. if($autorespond && (Email::getIdByEmail($ticket->getEmail()))) $autorespond=false; # Messages that are clearly auto-responses from email systems should # not have a return 'ping' message if (isset($vars['flags']) && $vars['flags']['bounce']) $autorespond = false; if ($autorespond && $message->isAutoReply()) $autorespond = false; //post canned auto-response IF any (disables new ticket auto-response). if ($vars['cannedResponseId'] && $ticket->postCannedReply($vars['cannedResponseId'], $message->getId(), $autorespond)) { $ticket->markUnAnswered(); //Leave the ticket as unanswred. $autorespond = false; } //Check department's auto response settings // XXX: Dept. setting doesn't affect canned responses. if($autorespond && $dept && !$dept->autoRespONNewTicket()) $autorespond=false; //Don't send alerts to staff when the message is a bounce // this is necessary to avoid possible loop (especially on new ticket) if ($alertstaff && $message->isBounce()) $alertstaff = false; /***** See if we need to send some alerts ****/ $ticket->onNewTicket($message, $autorespond, $alertstaff); /************ check if the user JUST reached the max. open tickets limit **********/ if($cfg->getMaxOpenTickets()>0 && ($user=$ticket->getOwner()) && ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())) { $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff'))); } /* Start tracking ticket lifecycle events */ $ticket->logEvent('created'); // Fire post-create signal (for extra email sending, searching) Signal::send('model.created', $ticket); /* Phew! ... time for tea (KETEPA) */ return $ticket; } /* routine used by staff to open a new ticket */ static function open($vars, &$errors) { global $thisstaff, $cfg; if(!$thisstaff || !$thisstaff->canCreateTickets()) return false; if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other'))) $errors['source']=sprintf(__('Invalid source given - %s'),Format::htmlchars($vars['source'])); if (!$vars['uid']) { //Special validation required here if (!$vars['email'] || !Validator::is_email($vars['email'])) $errors['email'] = __('Valid email address is required'); if (!$vars['name']) $errors['name'] = __('Name is required'); } if (!$thisstaff->canAssignTickets()) unset($vars['assignId']); $create_vars = $vars; $tform = TicketForm::objects()->one()->getForm($create_vars); $create_vars['cannedattachments'] = $tform->getField('message')->getWidget()->getAttachments()->getClean(); if(!($ticket=Ticket::create($create_vars, $errors, 'staff', false))) return false; $vars['msgId']=$ticket->getLastMsgId(); // post response - if any $response = null; if($vars['response'] && $thisstaff->canPostReply()) { $vars['response'] = $ticket->replaceVars($vars['response']); // $vars['cannedatachments'] contains the attachments placed on // the response form. $response = $ticket->postReply($vars, $errors, false); } // Not assigned...save optional note if any if (!$vars['assignId'] && $vars['note']) { $ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false); } else { // Not assignment and no internal note - log activity $ticket->logActivity(_S('New Ticket by Agent'), sprintf(_S('Ticket created by agent - %s'), $thisstaff->getName())); } $ticket->reload(); if(!$cfg->notifyONNewStaffTicket() || !isset($vars['alertuser']) || !($dept=$ticket->getDept())) return $ticket; //No alerts. //Send Notice to user --- if requested AND enabled!! if(($tpl=$dept->getTemplate()) && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) && ($email=$dept->getEmail())) { $message = (string) $ticket->getLastMessage(); if($response) { $message .= ($cfg->isHtmlThreadEnabled()) ? "

" : "\n\n"; $message .= $response->getBody(); } if($vars['signature']=='mine') $signature=$thisstaff->getSignature(); elseif($vars['signature']=='dept' && $dept && $dept->isPublic()) $signature=$dept->getSignature(); else $signature=''; $attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array(); $msg = $ticket->replaceVars($msg->asArray(), array( 'message' => $message, 'signature' => $signature, 'response' => ($response) ? $response->getBody() : '', 'recipient' => $ticket->getOwner(), //End user 'staff' => $thisstaff, ) ); $references = $ticket->getLastMessage()->getEmailMessageId(); if (isset($response)) $references = array($response->getEmailMessageId(), $references); $options = array( 'references' => $references, 'thread' => $ticket->getLastMessage() ); $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments, $options); } return $ticket; } function checkOverdue() { $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 ' .' INNER JOIN '.TICKET_STATUS_TABLE.' status ON (status.id=T1.status_id AND status.state="open") ' .' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) ' .' WHERE isoverdue=0 ' .' AND ((reopened is NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),T1.created))>=T2.grace_period*3600) ' .' OR (reopened is NOT NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),reopened))>=T2.grace_period*3600) ' .' OR (duedate is NOT NULL AND duedatemarkOverdue()) $ticket->logActivity(_S('Ticket Marked Overdue'), _S('Ticket flagged as overdue by the system.')); } } else { //TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period. } } } ?>