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.export.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'); require_once(INCLUDE_DIR.'class.task.php'); require_once(INCLUDE_DIR.'class.faq.php'); class Ticket extends VerySimpleModel implements RestrictedAccess, Threadable, Searchable { static $meta = array( 'table' => TICKET_TABLE, 'pk' => array('ticket_id'), 'select_related' => array('topic', 'staff', 'user', 'team', 'dept', 'sla', 'thread', 'child_thread', 'user__default_email', 'status'), 'joins' => array( 'user' => array( 'constraint' => array('user_id' => 'User.id'), 'null' => true, ), 'status' => array( 'constraint' => array('status_id' => 'TicketStatus.id') ), 'lock' => array( 'constraint' => array('lock_id' => 'Lock.lock_id'), 'null' => true, ), 'dept' => array( 'constraint' => array('dept_id' => 'Dept.id'), 'null' => true, ), 'sla' => array( 'constraint' => array('sla_id' => 'Sla.id'), 'null' => true, ), 'staff' => array( 'constraint' => array('staff_id' => 'Staff.staff_id'), 'null' => true, ), 'tasks' => array( 'reverse' => 'Task.ticket', ), 'team' => array( 'constraint' => array('team_id' => 'Team.team_id'), 'null' => true, ), 'topic' => array( 'constraint' => array('topic_id' => 'Topic.topic_id'), 'null' => true, ), 'thread' => array( 'reverse' => 'TicketThread.ticket', 'list' => false, 'null' => true, ), 'child_thread' => array( 'constraint' => array( 'ticket_id' => 'TicketThread.object_id', "'C'" => 'TicketThread.object_type', ), 'null' => true, ), 'cdata' => array( 'reverse' => 'TicketCData.ticket', 'list' => false, ), 'entries' => array( 'constraint' => array( "'T'" => 'DynamicFormEntry.object_type', 'ticket_id' => 'DynamicFormEntry.object_id', ), 'list' => true, ), ) ); const PERM_CREATE = 'ticket.create'; const PERM_EDIT = 'ticket.edit'; const PERM_ASSIGN = 'ticket.assign'; const PERM_RELEASE = 'ticket.release'; const PERM_TRANSFER = 'ticket.transfer'; const PERM_REFER = 'ticket.refer'; const PERM_MERGE = 'ticket.merge'; const PERM_LINK = 'ticket.link'; const PERM_REPLY = 'ticket.reply'; const PERM_MARKANSWERED = 'ticket.markanswered'; const PERM_CLOSE = 'ticket.close'; const PERM_DELETE = 'ticket.delete'; const FLAG_COMBINE_THREADS = 0x0001; const FLAG_SEPARATE_THREADS = 0x0002; const FLAG_LINKED = 0x0008; const FLAG_PARENT = 0x0010; static protected $perms = array( self::PERM_CREATE => array( 'title' => /* @trans */ 'Create', 'desc' => /* @trans */ 'Ability to open tickets on behalf of users'), self::PERM_EDIT => array( 'title' => /* @trans */ 'Edit', 'desc' => /* @trans */ 'Ability to edit tickets'), self::PERM_ASSIGN => array( 'title' => /* @trans */ 'Assign', 'desc' => /* @trans */ 'Ability to assign tickets to agents or teams'), self::PERM_RELEASE => array( 'title' => /* @trans */ 'Release', 'desc' => /* @trans */ 'Ability to release ticket assignment'), self::PERM_TRANSFER => array( 'title' => /* @trans */ 'Transfer', 'desc' => /* @trans */ 'Ability to transfer tickets between departments'), self::PERM_REFER => array( 'title' => /* @trans */ 'Refer', 'desc' => /* @trans */ 'Ability to manage ticket referrals'), self::PERM_MERGE => array( 'title' => /* @trans */ 'Merge', 'desc' => /* @trans */ 'Ability to merge tickets'), self::PERM_LINK => array( 'title' => /* @trans */ 'Link', 'desc' => /* @trans */ 'Ability to link tickets'), self::PERM_REPLY => array( 'title' => /* @trans */ 'Post Reply', 'desc' => /* @trans */ 'Ability to post a ticket reply'), self::PERM_MARKANSWERED => array( 'title' => /* @trans */ 'Mark as Answered', 'desc' => /* @trans */ 'Ability to mark a ticket as Answered/Unanswered'), self::PERM_CLOSE => array( 'title' => /* @trans */ 'Close', 'desc' => /* @trans */ 'Ability to close tickets'), self::PERM_DELETE => array( 'title' => /* @trans */ 'Delete', 'desc' => /* @trans */ 'Ability to delete tickets'), ); // Ticket Sources static protected $sources = array( 'Phone' => /* @trans */ 'Phone', 'Email' => /* @trans */ 'Email', 'Web' => /* @trans */ 'Web', 'API' => /* @trans */ 'API', 'Other' => /* @trans */ 'Other', ); var $lastMsgId; var $last_message; var $owner; // TicketOwner var $_user; // EndUser var $_answers; var $collaborators; var $active_collaborators; var $recipients; var $lastrespondent; var $lastuserrespondent; var $_children; function loadDynamicData($force=false) { if (!isset($this->_answers) || $force) { $this->_answers = array(); foreach (DynamicFormEntryAnswer::objects() ->filter(array( 'entry__object_id' => $this->getId(), 'entry__object_type' => 'T' )) as $answer ) { $tag = mb_strtolower($answer->field->name) ?: 'field.' . $answer->field->id; $this->_answers[$tag] = $answer; } } return $this->_answers; } function getAnswer($field, $form=null) { // TODO: Prefer CDATA ORM relationship if already loaded $this->loadDynamicData(); return $this->_answers[$field]; } function getId() { return $this->ticket_id; } function getPid() { return $this->ticket_pid; } function getChildren() { if (!isset($this->_children) && $this->isParent()) $this->_children = self::getChildTickets($this->getId()); return $this->_children ?: array(); } function getMergeTypeByFlag($flag) { if (($flag & self::FLAG_COMBINE_THREADS) != 0) return 'combine'; if (($flag & self::FLAG_SEPARATE_THREADS) != 0) return 'separate'; else return 'visual'; return 'visual'; } function getMergeType() { if ($this->hasFlag(self::FLAG_COMBINE_THREADS)) return 'combine'; if ($this->hasFlag(self::FLAG_SEPARATE_THREADS)) return 'separate'; else return 'visual'; return 'visual'; } function isMerged() { if (!is_null($this->getPid()) || $this->isParent()) return true; return false; } function isParent($flag=false) { if (is_numeric($flag) && ($flag & self::FLAG_PARENT) != 0) return true; elseif (!is_numeric($flag) && $this->hasFlag(self::FLAG_PARENT)) return true; return false; } function hasFlag($flag) { return ($this->get('flags', 0) & $flag) != 0; } function isChild($pid=false) { return ($this->getPid() ? true : false); } function hasState($state) { return strcasecmp($this->getState(), $state) == 0; } function isOpen() { return $this->hasState('open'); } function isReopened() { return null !== $this->getReopenDate(); } function isReopenable() { return ($this->getStatus()->isReopenable() && $this->getDept()->allowsReopen() && ($this->getTopic() ? $this->getTopic()->allowsReopen() : true)); } function isClosed() { return $this->hasState('closed'); } function isCloseable() { global $cfg; if ($this->isClosed()) return true; $warning = null; if (self::getMissingRequiredFields($this)) { $warning = sprintf( __( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'), __('This ticket'), '', ''); } elseif (($num=$this->getNumOpenTasks())) { $warning = sprintf(__('%1$s has %2$d open tasks and cannot be closed'), __('This ticket'), $num); } elseif ($cfg->requireTopicToClose() && !$this->getTopicId()) { $warning = sprintf( __( '%1$s is missing a %2$s and cannot be closed'), __('This ticket'), __('Help Topic'), ''); } return $warning ?: true; } function isArchived() { return $this->hasState('archived'); } function isDeleted() { return $this->hasState('deleted'); } function isAssigned($to=null) { if (!$this->isOpen()) return false; if (is_null($to)) return ($this->getStaffId() || $this->getTeamId()); switch (true) { case $to instanceof Staff: return ($to->getId() == $this->getStaffId() || $to->isTeamMember($this->getTeamId())); break; case $to instanceof Team: return ($to->getId() == $this->getTeamId()); break; } return false; } function isOverdue() { return $this->ht['isoverdue']; } function isAnswered() { return $this->ht['isanswered']; } function isLocked() { return null !== $this->getLock(); } function getRole($staff) { if (!$staff instanceof Staff) return null; return $staff->getRole($this->getDept(), $this->isAssigned($staff)); } function checkStaffPerm($staff, $perm=null) { // Must be a valid staff if ((!$staff instanceof Staff) && !($staff=Staff::lookup($staff))) return false; // check department access first if (!$staff->canAccessDept($this->getDept()) // check assignment && !$this->isAssigned($staff) // check referral && !$this->getThread()->isReferred($staff)) return false; // At this point staff has view access unless a specific permission is // requested if ($perm === null) return true; // Permission check requested -- get role if any if (!($role=$this->getRole($staff))) return false; // Check permission based on the effective role return $role->hasPerm($perm); } function checkUserAccess($user) { if (!$user || !($user instanceof EndUser)) return false; // Ticket Owner if ($user->getId() == $this->getUserId()) return true; // Organization if ($user->canSeeOrgTickets() && ($U = $this->getUser()) && ($U->getOrgId() == $user->getOrgId()) ) { // The owner of this ticket is in the same organization as the // user in question, and the organization is configured to allow // the user in question to see other tickets in the // organization. return true; } // Collaborator? // 1) If the user was authorized via this ticket. if ($user->getTicketId() == $this->getId() && !strcasecmp($user->getUserType(), 'collaborator') ) { return true; } // 2) Query the database to check for expanded access... if (Collaborator::lookup(array( 'user_id' => $user->getId(), 'thread_id' => $this->getThreadId())) ) { return true; } // 3) If the ticket is a child of a merge if ($this->isParent() && $this->getMergeType() != 'visual') { $children = Ticket::objects() ->filter(array('ticket_pid'=>$this->getId())) ->order_by('sort'); foreach ($children as $child) if ($child->checkUserAccess($user)) return true; } return false; } // Getters function getNumber() { return $this->number; } function getOwnerId() { return $this->user_id; } function getOwner() { if (!isset($this->owner)) { $this->owner = new TicketOwner(new EndUser($this->user), $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(); } // Deprecated function getOldAuthToken() { # 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->getAnswer('subject'); } /* Help topic title - NOT object -> $topic */ function getHelpTopic() { if ($this->topic) return $this->topic->getFullName(); } function getCreateDate() { return $this->created; } function getOpenDate() { return $this->getCreateDate(); } function getReopenDate() { return $this->reopened; } function getUpdateDate() { return $this->updated; } function getEffectiveDate() { return $this->lastupdate; } function getDueDate() { return $this->duedate; } function getSLADueDate($recompute=false) { global $cfg; if (!$recompute && $this->est_duedate) return $this->est_duedate; if (($sla = $this->getSLA()) && $sla->isActive()) { $schedule = $this->getDept()->getSchedule(); $tz = new DateTimeZone($cfg->getDbTimezone()); $dt = new DateTime($this->getReopenDate() ?: $this->getCreateDate(), $tz); $dt = $sla->addGracePeriod($dt, $schedule); // Make sure time is in DB timezone $dt->setTimezone($tz); return $dt->format('Y-m-d H:i:s'); } } function updateEstDueDate($clearOverdue=true) { if ($this->isOverdue() && $clearOverdue) $this->clearOverdue(false); $this->est_duedate = $this->getSLADueDate(true) ?: null; return $this->save(); } function getEstDueDate() { // Real due date or sla due date (If ANY) return $this->getDueDate() ?: $this->getSLADueDate(); } function getCloseDate() { return $this->closed; } function getStatusId() { return $this->status_id; } /** * setStatusId * * Forceably set the ticket status ID to the received status ID. No * checks are made. Use ::setStatus() to change the ticket status */ // XXX: Use ::setStatus to change the status. This can be used as a // fallback if the logic in ::setStatus fails. function setStatusId($id) { $this->status_id = $id; return $this->save(); } function getStatus() { return $this->status; } function getState() { if (!$this->getStatus()) { return ''; } return $this->getStatus()->getState(); } function getDeptId() { return $this->dept_id; } function getDeptName() { if ($this->dept instanceof Dept) return $this->dept->getFullName(); } function getPriorityId() { global $cfg; if (($priority = $this->getPriority())) return $priority->getId(); return $cfg->getDefaultPriorityId(); } function getPriority() { if (($a = $this->getAnswer('priority'))) return $a->getValue(); return null; } function getPriorityField() { if (($a = $this->getAnswer('priority'))) return $a->getField(); return TicketForm::getInstance()->getField('priority'); } function getPhoneNumber() { return (string)$this->getOwner()->getPhoneNumber(); } function getSource() { $sources = $this->getSources(); return $sources[$this->source] ?: $this->source; } function getIP() { return $this->ip_address; } function getHashtable() { return $this->ht; } function getUpdateInfo() { global $cfg; return array( 'source' => $this->source, 'topicId' => $this->getTopicId(), 'slaId' => $this->getSLAId(), 'user_id' => $this->getOwnerId(), 'duedate' => Misc::db2gmtime($this->getDueDate()), ); } function getLock() { $lock = $this->lock; if ($lock && !$lock->isExpired()) return $lock; } function acquireLock($staffId, $lockTime=null) { global $cfg; if (!isset($lockTime)) $lockTime = $cfg->getLockTime(); 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->lock = Lock::acquire($staffId, $lockTime); //Create a new lock.. if ($this->lock) { $this->save(); } // load and return the newly created lock if any! return $this->lock; } function releaseLock($staffId=false) { if (!($lock = $this->getLock())) return false; if ($staffId && $lock->staff_id != $staffId) return false; if (!$lock->delete()) return false; $this->lock = null; return $this->save(); } function getDept() { global $cfg; return $this->dept ?: $cfg->getDefaultDept(); } function getUserId() { return $this->getOwnerId(); } function getUser() { if (!isset($this->_user) && $this->user) { $this->_user = new EndUser($this->user); } return $this->_user; } function getStaffId() { return $this->staff_id; } function getStaff() { return $this->staff; } function getTeamId() { return $this->team_id; } function getTeam() { return $this->team; } function getAssigneeId() { if (!($assignee=$this->getAssignee())) return null; $id = ''; if ($assignee instanceof Staff) $id = 's'.$assignee->getId(); elseif ($assignee instanceof Team) $id = 't'.$assignee->getId(); return $id; } function getAssignee() { if (!$this->isOpen() || !$this->isAssigned()) return false; if ($this->staff) return $this->staff; if ($this->team) return $this->team; return null; } 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->topic_id; } function getTopic() { return $this->topic; } function getSLAId() { return $this->sla_id; } function getSLA() { return $this->sla; } function getLastRespondent() { if (!isset($this->lastrespondent)) { if (!$this->getThread() || !$this->getThread()->entries) return $this->lastrespondent = false; $this->lastrespondent = Staff::objects() ->filter(array( 'staff_id' => $this->getThread()->entries ->filter(array( 'type' => 'R', 'staff_id__gt' => 0, )) ->values_flat('staff_id') ->order_by('-id') ->limit('1,1') )) ->first() ?: false; } return $this->lastrespondent; } function getLastUserRespondent() { if (!isset($this->$lastuserrespondent)) { if (!$this->getThread() || !$this->getThread()->entries) return $this->$lastuserrespondent = false; $this->$lastuserrespondent = User::objects() ->filter(array( 'id' => $this->getThread()->entries ->filter(array( 'user_id__gt' => 0, )) ->values_flat('user_id') ->order_by('-id') ->limit(1) )) ->first() ?: false; } return $this->$lastuserrespondent; } function getLastMessageDate() { return $this->getThread()->lastmessage; } function getLastMsgDate() { return $this->getLastMessageDate(); } function getLastResponseDate() { return $this->getThread()->lastresponse; } function getLastRespDate() { return $this->getLastResponseDate(); } function getLastMsgId() { return $this->lastMsgId; } function getLastMessage() { if (!isset($this->last_message)) { if ($this->getLastMsgId()) $this->last_message = MessageThreadEntry::lookup( $this->getLastMsgId(), $this->getThreadId()); if (!$this->last_message) $this->last_message = $this->getThread() ? $this->getThread()->getLastMessage() : ''; } return $this->last_message; } function getNumTasks() { // FIXME: Implement this after merging Tasks return count($this->tasks); } function getNumOpenTasks() { return count($this->tasks->filter(array( 'flags__hasbit' => TaskModel::ISOPEN))); } function getThreadId() { if ($this->getThread()) return $this->getThread()->getId(); } function getThread() { if (is_null($this->thread) && $this->child_thread) return $this->child_thread; return $this->thread; } function getThreadCount() { return $this->getClientThread()->count(); } function getNumMessages() { return $this->getThread()->getNumMessages(); } function getNumResponses() { return $this->getThread()->getNumResponses(); } function getNumNotes() { return $this->getThread()->getNumNotes(); } function getMessages() { return $this->getThreadEntries(array('M')); } function getResponses() { return $this->getThreadEntries(array('R')); } function getNotes() { return $this->getThreadEntries(array('N')); } function getClientThread() { return $this->getThreadEntries(array('M', 'R')); } function getThreadEntry($id) { return $this->getThread()->getEntry($id); } function getThreadEntries($type=false) { if ($this->getThread()) { $entries = $this->getThread()->getEntries(); if ($type && is_array($type)) $entries->filter(array('type__in' => $type)); } return $entries; } // MailingList of participants (owner + collaborators) function getRecipients($who='all', $whitelist=array(), $active=true) { $list = new MailingList(); switch (strtolower($who)) { case 'user': $list->addTo($this->getOwner()); break; case 'all': $list->addTo($this->getOwner()); // Fall-trough case 'collabs': if (($collabs = $active ? $this->getActiveCollaborators() : $this->getCollaborators())) { foreach ($collabs as $c) if (!$whitelist || in_array($c->getUserId(), $whitelist)) $list->addCc($c); } break; default: return null; } return $list; } function getCollaborators() { return $this->getThread() ? $this->getThread()->getCollaborators() : ''; } function getNumCollaborators() { return $this->getThread() ? $this->getThread()->getNumCollaborators() : ''; } function getActiveCollaborators() { return $this->getThread() ? $this->getThread()->getActiveCollaborators() : ''; } function getNumActiveCollaborators() { return $this->getThread() ? $this->getThread()->getNumActiveCollaborators() : ''; } function getAssignmentForm($source=null, $options=array()) { global $thisstaff; $prompt = $assignee = ''; // Possible assignees $assignees = null; $dept = $this->getDept(); switch (strtolower($options['target'])) { case 'agents': $assignees = array(); foreach ($thisstaff->getDeptAgents(array('available' => true)) as $member) $assignees['s'.$member->getId()] = $member; if (!$source && $this->isOpen() && $this->staff) $assignee = sprintf('s%d', $this->staff->getId()); $prompt = __('Select an Agent'); break; case 'teams': $assignees = array(); if (($teams = Team::getActiveTeams())) foreach ($teams as $id => $name) $assignees['t'.$id] = $name; if (!$source && $this->isOpen() && $this->team) $assignee = sprintf('t%d', $this->team->getId()); $prompt = __('Select a Team'); break; } // Default to current assignee if source is not set if (!$source) $source = array('assignee' => array($assignee)); $form = AssignmentForm::instantiate($source, $options); if (isset($assignees)) $form->setAssignees($assignees); if (($refer = $form->getField('refer'))) { if ($assignee) { $visibility = new VisibilityConstraint( new Q(array()), VisibilityConstraint::HIDDEN); $refer->set('visibility', $visibility); } else { $refer->configure('desc', sprintf(__('Maintain referral access to %s'), $this->getAssigned())); } } // Field configurations if ($f=$form->getField('assignee')) { if ($prompt) $f->configure('prompt', $prompt); $f->configure('dept', $dept); } return $form; } function getReferralForm($source=null, $options=array()) { $form = ReferralForm::instantiate($source, $options); $dept = $this->getDept(); // Agents $staff = Staff::objects()->filter(array( 'isactive' => 1, )) ->filter(Q::not(array('dept_id' => $dept->getId()))); $staff = Staff::nsort($staff); $agents = array(); foreach ($staff as $s) $agents[$s->getId()] = $s; $form->setChoices('agent', $agents); // Teams $form->setChoices('team', Team::getActiveTeams()); // Depts $form->setChoices('dept', Dept::getDepartments()); return $form; } function getClaimForm($source=null, $options=array()) { global $thisstaff; $id = sprintf('s%d', $thisstaff->getId()); if(!$source) $source = array('assignee' => array($id)); $form = ClaimForm::instantiate($source, $options); $form->setAssignees(array($id => $thisstaff->getName())); return $form; } function getTransferForm($source=null) { if (!$source) $source = array('dept' => array($this->getDeptId()), 'refer' => false); return TransferForm::instantiate($source); } function getField($fid) { if (is_numeric($fid)) return $this->getDynamicFieldById($fid); // Special fields switch ($fid) { case 'priority': return $this->getPriorityField(); break; case 'sla': return SLAField::init(array( 'id' => $fid, 'name' => "{$fid}_id", 'label' => __('SLA Plan'), 'default' => $this->getSLAId(), 'choices' => SLA::getSLAs() )); break; case 'topic': $current = array(); if ($topic = $this->getTopic()) $current = array($topic->getId()); $choices = Topic::getHelpTopics(false, $topic ? (Topic::DISPLAY_DISABLED) : false, true, $current); return TopicField::init(array( 'id' => $fid, 'name' => "{$fid}_id", 'label' => __('Help Topic'), 'default' => $this->getTopicId(), 'choices' => $choices )); break; case 'source': return ChoiceField::init(array( 'id' => $fid, 'name' => 'source', 'label' => __('Ticket Source'), 'default' => $this->source, 'choices' => Ticket::getSources() )); break; case 'duedate': $hint = sprintf(__('Setting a %s will override %s'), __('Due Date'), __('SLA Plan')); return DateTimeField::init(array( 'id' => $fid, 'name' => $fid, 'default' => Misc::db2gmtime($this->getDueDate()), 'label' => __('Due Date'), 'hint' => $hint, 'configuration' => array( 'min' => Misc::gmtime(), 'time' => true, 'gmt' => false, 'future' => true, ) )); } } function getDynamicFieldById($fid) { foreach (DynamicFormEntry::forTicket($this->getId()) as $form) { foreach ($form->getFields() as $field) if ($field->getId() == $fid) { // This is to prevent SimpleForm using index name as // field name when one is not set. if (!$field->get('name')) $field->set('name', "field_$fid"); return $field; } } } function getDynamicFields($criteria=array()) { $fields = DynamicFormField::objects()->filter(array( 'id__in' => $this->entries ->filter($criteria) ->values_flat('answers__field_id'))); return ($fields && count($fields)) ? $fields : array(); } function hasClientEditableFields() { $forms = DynamicFormEntry::forTicket($this->getId()); foreach ($forms as $form) { foreach ($form->getFields() as $field) { if ($field->isEditableToUsers()) return true; } } } //if ids passed, function returns only the ids of fields disabled by help topic static function getMissingRequiredFields($ticket, $ids=false) { // Check for fields disabled by Help Topic $disabled = array(); foreach (($ticket->getTopic() ? $ticket->getTopic()->forms : $ticket->entries) as $f) { $extra = JsonDataParser::decode($f->extra); if (!empty($extra['disable'])) $disabled[] = $extra['disable']; } $disabled = !empty($disabled) ? call_user_func_array('array_merge', $disabled) : NULL; if ($ids) return $disabled; $criteria = array( 'answers__field__flags__hasbit' => DynamicFormField::FLAG_ENABLED, 'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED, 'answers__value__isnull' => true, ); // If there are disabled fields then exclude them if ($disabled) array_push($criteria, Q::not(array('answers__field__id__in' => $disabled))); return $ticket->getDynamicFields($criteria); } function getMissingRequiredField() { $fields = self::getMissingRequiredFields($this); return $fields ? $fields[0] : null; } function addCollaborator($user, $vars, &$errors, $event=true) { if ($user && $user->getId() == $this->getOwnerId()) $errors['err'] = __('Ticket Owner cannot be a Collaborator'); if ($user && !$errors && ($c = $this->getThread()->addCollaborator($user, $vars, $errors, $event))) { $c->setCc($c->active); $this->collaborators = null; $this->recipients = null; return $c; } return null; } function addCollaborators($users, $vars, &$errors, $event=true) { if (!$users || !is_array($users)) return null; $collabs = $this->getCollaborators(); $new = array(); foreach ($users as $user) { if (!($user instanceof User) && !($user = User::lookup($user))) continue; if ($collabs->findFirst(array('user_id' => $user->getId()))) continue; if ($user->getId() == $this->getOwnerId()) continue; if ($c=$this->addCollaborator($user, $vars, $errors, $event)) $new[] = $c; } return $new; } //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->delete()) $collabs[] = (string) $c; } $this->logEvent('collab', array('del' => $collabs)); } //statuses $cids = null; if($vars['cid'] && ($cids=array_filter($vars['cid']))) { $this->getThread()->collaborators->filter(array( 'thread_id' => $this->getThreadId(), 'id__in' => $cids ))->update(array( 'updated' => SqlFunction::NOW(), )); } if ($cids) { $this->getThread()->collaborators->filter(array( 'thread_id' => $this->getThreadId(), Q::not(array('id__in' => $cids)) ))->update(array( 'updated' => SqlFunction::NOW(), )); } unset($this->active_collaborators); $this->collaborators = null; return true; } function getAuthToken($user, $algo=1) { //Format: // x $authtoken = sprintf('%s%dx%s', ($user->getId() == $this->getOwnerId() ? 'o' : 'c'), $algo, Base32::encode(pack('VV',$user->getId(), $this->getId()))); switch($algo) { case 1: $authtoken .= substr(base64_encode( md5($user->getId().$this->getCreateDate().$this->getId().SECRET_SALT, true)), 8); break; default: return null; } return $authtoken; } function sendAccessLink($user) { global $ost; if (!($email = $ost->getConfig()->getDefaultEmail()) || !($content = Page::lookupByType('access-link'))) return; $vars = array( 'url' => $ost->getConfig()->getBaseUrl(), 'ticket' => $this, 'user' => $user, 'recipient' => $user, // Get ticket link, with authcode, directly to bypass collabs // check 'recipient.ticket_link' => $user->getTicketLink(), ); $lang = $user->getLanguage(UserAccount::LANG_MAILOUTS); $msg = $ost->replaceTemplateVariables(array( 'subj' => $content->getLocalName($lang), 'body' => $content->getLocalBody($lang), ), $vars); $email->send($user, Format::striptags($msg['subj']), $msg['body']); } /* -------------------- Setters --------------------- */ public function setFlag($flag, $val) { if ($val) $this->flags |= $flag; else $this->flags &= ~$flag; } function setMergeType($combine=false, $parent=false) { //for $combine, 0 = separate, 1 = combine, 2 = link, 3 = regular ticket $flags = array(Ticket::FLAG_SEPARATE_THREADS, Ticket::FLAG_COMBINE_THREADS, Ticket::FLAG_LINKED); foreach ($flags as $key => $flag) { if ($combine == $key) $this->setFlag($flag, true); else $this->setFlag($flag, false); } if ($parent) $this->setFlag(Ticket::FLAG_PARENT, true); else $this->setFlag(Ticket::FLAG_PARENT, false); $this->save(); } function setPid($pid) { return $this->ticket_pid = $this->getId() != $pid ? $pid : NULL; } function setSort($sort) { return $this->sort=$sort; } 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 ($deptId == $this->getDeptId() || !($dept=Dept::lookup($deptId))) { return false; } $this->dept = $dept; return $this->save(); } // Set staff ID...assign/unassign/release (id can be 0) function setStaffId($staffId) { if (!is_numeric($staffId)) return false; $this->staff = Staff::lookup($staffId); return $this->save(); } function setSLAId($slaId) { if ($slaId == $this->getSLAId()) return true; $sla = null; if ($slaId && !($sla = Sla::lookup($slaId))) return false; $this->sla = $sla; return $this->save(); } /** * 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; $this->team = Team::lookup($teamId); return $this->save(); } // Ticket Status helper. function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true, $force_close=false) { global $cfg, $thisstaff; if ($thisstaff && !($role=$this->getRole($thisstaff))) return false; if ((!$status instanceof TicketStatus) && !($status = TicketStatus::lookup($status))) return false; // Double check permissions (when changing status) if ($role && $this->getStatusId()) { switch ($status->getState()) { case 'closed': if (!($role->hasPerm(Ticket::PERM_CLOSE))) return false; break; case 'deleted': // XXX: intercept deleted status and do hard delete TODO: soft deletes if ($role->hasPerm(Ticket::PERM_DELETE)) return $this->delete($comments); // Agent doesn't have permission to delete tickets return false; break; } } $hadStatus = $this->getStatusId(); if ($this->getStatusId() == $status->getId()) return true; // Perform checks on the *new* status, _before_ the status changes $ecb = $refer = null; switch ($status->getState()) { case 'closed': // Check if ticket is closeable $closeable = $force_close ? true : $this->isCloseable(); if ($closeable !== true) $errors['err'] = $closeable ?: sprintf(__('%s cannot be closed'), __('This ticket')); if ($errors) return false; $refer = $this->staff ?: $thisstaff; $this->closed = $this->lastupdate = SqlFunction::NOW(); if ($thisstaff && $set_closing_agent) $this->staff = $thisstaff; // Clear overdue flags & due dates $this->clearOverdue(false); $ecb = function($t) use ($status) { $t->logEvent('closed', array('status' => array($status->getId(), $status->getName())), null, 'closed'); $type = array('type' => 'closed'); Signal::send('object.edited', $t, $type); $t->deleteDrafts(); }; break; case 'open': if ($this->isClosed() && $this->isReopenable()) { // Auto-assign to closing staff or the last respondent if the // agent is available and has access. Otherwise, put the ticket back // to unassigned pool. $dept = $this->getDept(); $staff = $this->getStaff() ?: $this->getLastRespondent(); $autoassign = (!$dept->disableReopenAutoAssign()); if ($autoassign && $staff // Is agent on vacation ? && $staff->isAvailable() // Does the agent have access to dept? && $staff->canAccessDept($dept)) $this->setStaffId($staff->getId()); else $this->setStaffId(0); // Clear assignment } if ($this->isClosed()) { $this->closed = null; $this->lastupdate = $this->reopened = SqlFunction::NOW(); $ecb = function ($t) { $t->logEvent('reopened', false, null, 'closed'); // Set new sla duedate if any $t->updateEstDueDate(); }; } // If the ticket is not open then clear answered flag if (!$this->isOpen()) $this->isanswered = 0; break; default: return false; } $this->status = $status; if (!$this->save(true)) return false; // Refer thread to previously assigned or closing agent if ($refer && $cfg->autoReferTicketsOnClose()) $this->getThread()->refer($refer); // 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 ($hadStatus) { $alert = false; if ($comments = ThreadEntryBody::clean($comments)) { // Send out alerts if comments are included $alert = true; $this->logNote(__('Status Changed'), $comments, $thisstaff, $alert); } } // Log events via callback if ($ecb) $ecb($this); elseif ($hadStatus) // Don't log the initial status change $this->logEvent('edited', array('status' => $status->getId())); return true; } function setState($state, $alerts=false) { switch (strtolower($state)) { case 'open': return $this->setStatus('open'); case 'closed': return $this->setStatus('closed'); case 'answered': return $this->setAnsweredState(1); case 'unanswered': return $this->setAnsweredState(0); case 'overdue': return $this->markOverdue(); case 'notdue': return $this->clearOverdue(); case 'unassined': return $this->unassign(); } // FIXME: Throw and excception and add test cases return false; } function setAnsweredState($isanswered) { $this->isanswered = $isanswered; return $this->save(); } 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) : 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 ----------*/ if(!$cfg || !($dept=$this->getDept()) || !($tpl = $dept->getTemplate()) || !($email=$dept->getAutoRespEmail()) ) { return false; //bail out...missing stuff. } $options = array(); if (($message instanceof ThreadEntry) && $message->getEmailMessageId()) { $options += array( 'inreplyto'=>$message->getEmailMessageId(), 'references'=>$message->getEmailReferences(), 'thread'=>$message ); } else { $options += array( 'thread' => $this->getThread(), ); } //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 instanceof ThreadEntry && $message->isAutoReply()) $sentlist[] = $this->getEmail(); if ($dept->getNumMembersForAlerts()) { // Only alerts dept members if the ticket is NOT assigned. $manager = $dept->getManager(); if ($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned() && ($members = $dept->getMembersForAlerts()) ) { foreach ($members as $M) if ($M != $manager) $recipients[] = $M; } if ($cfg->alertDeptManagerONNewTicket() && $manager) { $recipients[] = $manager; } // Account manager if ($cfg->alertAcctManagerONNewTicket() && ($org = $this->getOwner()->getOrganization()) && ($acct_manager = $org->getAccountManager()) ) { if ($acct_manager instanceof Team) $recipients = array_merge($recipients, $acct_manager->getMembersForAlerts()); 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(); } } // Alert admin ONLY if not already a staff?? if ($cfg->alertAdminONNewTicket() && !in_array($cfg->getAdminEmail(), $sentlist) && ($dept->isGroupMembershipEnabled() != Dept::ALERTS_DISABLED)) { $options += array('utype'=>'A'); $alert = $this->replaceVars($msg, array('recipient' => 'Admin')); $email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options); } } 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()) { $this->isanswered = 1; $this->save(); $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; } $poster = User::lookup($entry->user_id); $posterEmail = $poster->getEmail()->address; $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('thread' => $entry); if ($vars['from_name']) $options += array('from_name' => $vars['from_name']); $skip = array(); if ($entry instanceof MessageThreadEntry) { foreach ($entry->getAllEmailRecipients() as $R) { $skip[] = $R->mailbox.'@'.$R->host; } } foreach ($recipients as $key => $recipient) { $recipient = $recipient->getContact(); if(get_class($recipient) == 'TicketOwner') $owner = $recipient; if ((get_class($recipient) == 'Collaborator' ? $recipient->getUserId() : $recipient->getId()) == $entry->user_id) unset($recipients[$key]); } if (!count($recipients)) return true; //see if the ticket user is a recipient if ($owner->getEmail()->address != $poster->getEmail()->address && !in_array($owner->getEmail()->address, $skip)) $owner_recip = $owner->getEmail()->address; //say dear collaborator if the ticket user is not a recipient if (!$owner_recip) { $nameFormats = array_keys(PersonsName::allFormats()); $names = array(); foreach ($nameFormats as $key => $value) { $names['recipient.name.' . $value] = __('Collaborator'); } $names = array_merge($names, array('recipient' => $recipient)); $cnotice = $this->replaceVars($msg, $names); } //otherwise address email to ticket user else $cnotice = $this->replaceVars($msg, array('recipient' => $owner)); $email->send($recipients, $cnotice['subj'], $cnotice['body'], $attachments, $options); } function onMessage($message, $autorespond=true, $reopen=true) { global $cfg; $this->isanswered = 0; $this->lastupdate = SqlFunction::NOW(); $this->save(); // 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 ($reopen && $this->isClosed() && $this->isReopenable()) $this->reopen(); if (!$autorespond) return; // Figure out the user if ($this->getOwnerId() == $message->getUserId()) $user = new TicketOwner( User::lookup($message->getUserId()), $this); else $user = Collaborator::lookup(array( 'user_id' => $message->getUserId(), 'thread_id' => $this->getThreadId())); /********** double check auto-response ************/ if (!$user) $autorespond = false; elseif ((Email::getIdByEmail($user->getEmail()))) $autorespond = false; elseif (($dept=$this->getDept())) $autorespond = $dept->autoRespONNewMessage(); if (!$autorespond || !$cfg->autoRespONNewMessage() || !$message ) { return; //no autoresp or alerts. } $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('thread' => $message); if ($message->getEmailMessageId()) { $options += array( 'inreplyto' => $message->getEmailMessageId(), 'references' => $message->getEmailReferences() ); } $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()) || !$dept->getNumMembersForAlerts() || !($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->getMembersForAlerts()); } // 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('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->checkStaffPerm($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; $user_comments = (bool) $comments; $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)'); //Log an internal note - no alerts on the internal note. if ($user_comments) { if ($assignee instanceof Staff && $thisstaff // self assignment && $assignee->getId() == $thisstaff->getId()) $title = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName()); else $title = sprintf(_S('Ticket Assigned to %s'), $assignee->getName()); $note = $this->logNote($title, $comments, $assigner, false); } $dept = $this->getDept(); // See if we need to send alerts if (!$alert || !$cfg->alertONAssignment() || !$dept->getNumMembersForAlerts()) return true; //No alerts! 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->getMembersForAlerts())) $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 = $note instanceof ThreadEntry ? array('thread'=>$note) : 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, $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()) || !$dept->getNumMembersForAlerts() ) { 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->getMembersForAlerts()) ) { $recipients=array_merge($recipients, $members); } } elseif ($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) { // Only alerts dept members if the ticket is NOT assigned. foreach ($dept->getMembersForAlerts() as $M) $recipients[] = $M; } // 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; } // TemplateVariable interface function asVar() { return $this->getNumber(); } function getVar($tag) { global $cfg; switch(mb_strtolower($tag)) { case 'phone': case 'phone_number': return $this->getPhoneNumber(); break; case 'auth_token': return $this->getOldAuthToken(); 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 new FormattedDate($this->getCreateDate()); break; case 'due_date': if ($due = $this->getEstDueDate()) return new FormattedDate($due); break; case 'close_date': if ($this->isClosed()) return new FormattedDate($this->getCloseDate()); break; case 'last_update': return new FormattedDate($this->lastupdate); case 'user': return $this->getOwner(); default: if ($a = $this->getAnswer($tag)) // The answer object is retrieved here which will // automatically invoke the toString() method when the // answer is coerced into text return $a; } } static function getVarScope() { $base = array( 'assigned' => __('Assigned Agent / Team'), 'close_date' => array( 'class' => 'FormattedDate', 'desc' => __('Date Closed'), ), 'create_date' => array( 'class' => 'FormattedDate', 'desc' => __('Date Created'), ), 'dept' => array( 'class' => 'Dept', 'desc' => __('Department'), ), 'due_date' => array( 'class' => 'FormattedDate', 'desc' => __('Due Date'), ), 'email' => __('Default email address of ticket owner'), 'id' => __('Ticket ID (internal ID)'), 'name' => array( 'class' => 'PersonsName', 'desc' => __('Name of ticket owner'), ), 'number' => __('Ticket Number'), 'phone' => __('Phone number of ticket owner'), 'priority' => array( 'class' => 'Priority', 'desc' => __('Priority'), ), 'recipients' => array( 'class' => 'UserList', 'desc' => __('List of all recipient names'), ), 'source' => __('Source'), 'status' => array( 'class' => 'TicketStatus', 'desc' => __('Status'), ), 'staff' => array( 'class' => 'Staff', 'desc' => __('Assigned/closing agent'), ), 'subject' => 'Subject', 'team' => array( 'class' => 'Team', 'desc' => __('Assigned/closing team'), ), 'thread' => array( 'class' => 'TicketThread', 'desc' => __('Ticket Thread'), ), 'topic' => array( 'class' => 'Topic', 'desc' => __('Help Topic'), ), // XXX: Isn't lastreponse and lastmessage more useful 'last_update' => array( 'class' => 'FormattedDate', 'desc' => __('Time of last update'), ), 'user' => array( 'class' => 'User', 'desc' => __('Ticket Owner'), ), ); $extra = VariableReplacer::compileFormScope(TicketForm::getInstance()); return $base + $extra; } // Searchable interface static function getSearchableFields() { $base = array( 'number' => new TextboxField(array( 'label' => __('Ticket Number') )), 'created' => new DatetimeField(array( 'label' => __('Create Date'), 'configuration' => array( 'fromdb' => true, 'time' => true, 'format' => 'y-MM-dd HH:mm:ss'), )), 'duedate' => new DatetimeField(array( 'label' => __('Due Date'), 'configuration' => array( 'fromdb' => true, 'time' => true, 'format' => 'y-MM-dd HH:mm:ss'), )), 'est_duedate' => new DatetimeField(array( 'label' => __('SLA Due Date'), 'configuration' => array( 'fromdb' => true, 'time' => true, 'format' => 'y-MM-dd HH:mm:ss'), )), 'reopened' => new DatetimeField(array( 'label' => __('Reopen Date'), 'configuration' => array( 'fromdb' => true, 'time' => true, 'format' => 'y-MM-dd HH:mm:ss'), )), 'closed' => new DatetimeField(array( 'label' => __('Close Date'), 'configuration' => array( 'fromdb' => true, 'time' => true, 'format' => 'y-MM-dd HH:mm:ss'), )), 'lastupdate' => new DatetimeField(array( 'label' => __('Last Update'), 'configuration' => array( 'fromdb' => true, 'time' => true, 'format' => 'y-MM-dd HH:mm:ss'), )), 'assignee' => new AssigneeChoiceField(array( 'label' => __('Assignee'), )), 'staff_id' => new AgentSelectionField(array( 'label' => __('Assigned Staff'), )), 'team_id' => new TeamSelectionField(array( 'label' => __('Assigned Team'), )), 'dept_id' => new DepartmentChoiceField(array( 'label' => __('Department'), )), 'sla_id' => new SLAChoiceField(array( 'label' => __('SLA Plan'), )), 'topic_id' => new HelpTopicChoiceField(array( 'label' => __('Help Topic'), )), 'source' => new TicketSourceChoiceField(array( 'label' => __('Ticket Source'), )), 'isoverdue' => new BooleanField(array( 'label' => __('Overdue'), 'descsearchmethods' => array( 'set' => '%s', 'nset' => 'Not %s' ), )), 'isanswered' => new BooleanField(array( 'label' => __('Answered'), 'descsearchmethods' => array( 'set' => '%s', 'nset' => 'Not %s' ), )), 'isassigned' => new AssignedField(array( 'label' => __('Assigned'), )), 'merged' => new MergedField(array( 'label' => __('Merged'), )), 'linked' => new LinkedField(array( 'label' => __('Linked'), )), 'thread_count' => new TicketThreadCountField(array( 'label' => __('Thread Count'), )), 'attachment_count' => new ThreadAttachmentCountField(array( 'label' => __('Attachment Count'), )), 'collaborator_count' => new ThreadCollaboratorCountField(array( 'label' => __('Collaborator Count'), )), 'task_count' => new TicketTasksCountField(array( 'label' => __('Task Count'), )), 'reopen_count' => new TicketReopenCountField(array( 'label' => __('Reopen Count'), )), 'ip_address' => new TextboxField(array( 'label' => __('IP Address'), 'configuration' => array('validator' => 'ip'), )), ); $tform = TicketForm::getInstance(); foreach ($tform->getFields() as $F) { $fname = $F->get('name') ?: ('field_'.$F->get('id')); if (!$F->hasData() || $F->isPresentationOnly() || !$F->isEnabled()) continue; if (!$F->isStorable()) $base[$fname] = $F; else $base["cdata__{$fname}"] = $F; } return $base; } static function supportsCustomData() { return true; } //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; // Only open tickets can be marked overdue if (!$this->isOpen()) return false; if ($this->isOverdue()) return true; $this->isoverdue = 1; if (!$this->save()) return false; $this->logEvent('overdue'); $this->onOverdue($whine); return true; } function clearOverdue($save=true) { //NOTE: Previously logged overdue event is NOT annuled. if ($this->isOverdue()) $this->isoverdue = 0; // clear due date if it's in the past if ($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime()) $this->duedate = null; // Clear SLA if est. due date is in the past if ($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime()) $this->est_duedate = null; return $save ? $this->save() : true; } function unlinkChild($parent) { $this->setPid(NULL); $this->setSort(1); $this->setFlag(Ticket::FLAG_LINKED, false); $this->save(); $this->logEvent('unlinked', array('ticket' => sprintf('Ticket #%s', $parent->getNumber()), 'id' => $parent->getId())); $parent->logEvent('unlinked', array('ticket' => sprintf('Ticket #%s', $this->getNumber()), 'id' => $this->getId())); } function unlink() { $pid = $this->isChild() ? $this->getPid() : $this->getId(); $parent = $this->isParent() ? $this : (Ticket::lookup($pid)); $child = $this->isChild() ? $this : ''; $children = $this->getChildren(); if ($children) { foreach ($children as $child) { $child = Ticket::lookup($child[0]); $child->unlinkChild($parent); } } elseif ($child) $child->unlinkChild($parent); if (count($children) == 0) { $parent->setFlag(Ticket::FLAG_LINKED, false); $parent->setFlag(Ticket::FLAG_PARENT, false); $parent->save(); } return true; } function manageMerge($tickets) { global $thisstaff; $permission = ($tickets['title'] && $tickets['title'] == 'link') ? (Ticket::PERM_LINK) : (Ticket::PERM_MERGE); $eventName = ($tickets['title'] && $tickets['title'] == 'link') ? 'linked' : 'merged'; //see if any tickets should be unlinked if ($tickets['dtids']) { foreach($tickets['dtids'] as $key => $value) { if (is_numeric($key) && $ticket = Ticket::lookup($value)) $ticket->unlink(); } return true; } elseif ($tickets['tids']) { //see if any tickets should be merged $ticketObjects = array(); foreach($tickets['tids'] as $key => $value) { if ($ticket = Ticket::lookupByNumber($value)) { $ticketObjects[] = $ticket; if (!$ticket->checkStaffPerm($thisstaff, $permission) && !$ticket->getThread()->isReferred()) return false; if ($key == 0) $parent = $ticket; //changing from link to merge if (($ticket->isParent() || $ticket->isChild()) && $ticket->getMergeType() == 'visual' && $tickets['combine'] != 2 || ($tickets['combine'] == 2 && !$parent->isParent() && $parent->isChild())) { //changing link parent $ticket->unlink(); $changeParent = true; } if ($ticket->getMergeType() == 'visual') { $ticket->setSort($key); $ticket->save(); } if ($parent && $parent->getId() != $ticket->getId()) { if (($changeParent) || ($parent->isParent() && $ticket->getMergeType() == 'visual' && !$ticket->isChild()) || //adding to link/merge (!$parent->isParent() && !$ticket->isChild())) { //creating fresh link/merge $parent->logEvent($eventName, array('ticket' => sprintf('Ticket #%s', $ticket->getNumber()), 'id' => $ticket->getId())); $ticket->logEvent($eventName, array('ticket' => sprintf('Ticket #%s', $parent->getNumber()), 'id' => $parent->getId())); if ($ticket->getPid() != $parent->getId()) $ticket->setPid($parent->getId()); $parent->setMergeType($tickets['combine'], true); $ticket->setMergeType($tickets['combine']); //referrals for merged tickets if ($parent->getDeptId() != ($ticketDeptId = $ticket->getDeptId()) && $tickets['combine'] != 2) { $refDept = $ticket->getDept(); $parent->getThread()->refer($refDept); $evd = array('dept' => $ticketDeptId); $parent->logEvent('referred', $evd); } } //switch between combine and separate } elseif ($parent->isParent() && $ticket->getMergeType() != 'visual' && $parent->getId() != $ticket->getId()) { $ticket->setMergeType($tickets['combine']); } elseif ($parent->isParent() && $ticket->getMergeType() != 'visual' && $parent->getId() == $ticket->getId()) $parent->setMergeType($tickets['combine'], true); } } } return $ticketObjects; } function merge($tickets) { $options = $tickets; if (!$tickets = self::manageMerge($tickets)) return false; if (is_bool($tickets)) return true; $children = array(); foreach ($tickets as $ticket) { if ($ticket->isParent()) $parent = $ticket; else $children[] = $ticket; } if ($parent && $parent->getMergeType() != 'visual') { $errors = array(); foreach ($children as $child) { if ($options['participants'] == 'all' && $collabs = $child->getCollaborators()) { foreach ($collabs as $collab) { $collab = $collab->getUser(); if ($collab->getId() != $parent->getOwnerId()) $parent->addCollaborator($collab, array(), $errors); } } $cUser = $child->getUser(); if ($cUser->getId() != $parent->getOwnerId()) $parent->addCollaborator($cUser, array(), $errors); $parentThread = $parent->getThread(); $deletedChild = Thread::objects() ->filter(array('extra__contains'=>'"ticket_id":'.$child->getId())) ->values_flat('id', 'extra') ->first(); if ($deletedChild) { $extraThread = Thread::lookup($deletedChild[0]); $extraThread->setExtra($parentThread, array('extra' => $deletedChild[1], 'threadId' => $extraThread->getId())); } if ($child->getThread()) $child->getThread()->setExtra($parentThread); $child->setMergeType($options['combine']); $child->setStatus(intval($options['childStatusId']), false, $errors, true, true); //force close status for children if ($options['parentStatusId']) $parent->setStatus(intval($options['parentStatusId'])); if ($options['delete-child'] || $options['move-tasks']) { if ($tasks = Task::objects() ->filter(array('object_id' => $child->getId())) ->values_flat('id')) { foreach ($tasks as $key => $tid) { $task = Task::lookup($tid[0]); $task->object_id = $parent->getId(); $task->save(); } } } if ($options['delete-child']) $child->delete(); } return $parent; } return false; } function getRelatedTickets() { return sprintf('   %s %s %s %s %s ', strtolower($this->getSource()), $this->getId(), $this->getId(), $this->getNumber(), $this->getSubject(), $this->getDeptName(), $this->getAssignee(), Format::datetime($this->getCreateDate())); } function hasReferral($object, $type) { if (($referral=$this->thread->getReferral($object->getId(), $type))) return $referral; return false; } //Dept Transfer...with alert.. done by staff function transfer(TransferForm $form, &$errors, $alert=true) { global $thisstaff, $cfg; // Check if staff can do the transfer if (!$this->checkStaffPerm($thisstaff, Ticket::PERM_TRANSFER)) return false; $cdept = $this->getDept(); // Current department $dept = $form->getDept(); // Target department if (!$dept || !($dept instanceof Dept)) $errors['dept'] = __('Department selection required'); elseif ($dept->getid() == $this->getDeptId()) $errors['dept'] = sprintf( __('%s already in the department'), __('Ticket')); else { $this->dept_id = $dept->getId(); // 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->staff_id = 0; } } if ($errors || !$this->save(true)) return false; // Reopen ticket if closed if ($this->isClosed()) $this->reopen(); // Set SLA of the new department if (!$this->getSLAId() || $this->getSLA()->isTransient()) if (($slaId=$this->getDept()->getSLAId())) $this->selectSLAId($slaId); // Log transfer event $this->logEvent('transferred', array('dept' => $dept->getName())); if (($referral=$this->hasReferral($dept,ObjectModel::OBJECT_TYPE_DEPT))) $referral->delete(); // Post internal note if any $note = null; $comments = $form->getField('comments')->getClean(); if ($comments) { $title = sprintf(__('%1$s transferred from %2$s to %3$s'), __('Ticket'), $cdept->getName(), $dept->getName()); $_errors = array(); $note = $this->postNote( array('note' => $comments, 'title' => $title), $_errors, $thisstaff, false); } if ($form->refer() && $cdept) $this->getThread()->refer($cdept); //Send out alerts if enabled AND requested if (!$alert || !$cfg->alertONTransfer() || !$dept->getNumMembersForAlerts()) return true; //no alerts!! if (($email = $dept->getAlertEmail()) && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getTransferAlertMsgTemplate()) ) { $msg = $this->replaceVars($msg->asArray(), array('comments' => $note, '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->getMembersForAlerts()) ) { $recipients = array_merge($recipients, $members); } } elseif ($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) { // Only alerts dept members if the ticket is NOT assigned. foreach ($dept->getMembersForAlerts() as $M) $recipients[] = $M; } // Always alert dept manager?? if ($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()) ) { $recipients[] = $manager; } $sentlist = $options = array(); if ($note) { $options += array('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(ClaimForm $form, &$errors) { global $thisstaff; $dept = $this->getDept(); $assignee = $form->getAssignee(); if (!($assignee instanceof Staff) || !$thisstaff || $thisstaff->getId() != $assignee->getId()) { $errors['err'] = __('Unknown assignee'); } elseif (!$assignee->isAvailable()) { $errors['err'] = __('Agent is unavailable for assignment'); } elseif (!$dept->canAssign($assignee)) { $errors['err'] = __('Permission denied'); } if ($errors) return false; return $this->assignToStaff($assignee, $form->getComments(), false); } function assignToStaff($staff, $note, $alert=true, $user=null) { if(!is_object($staff) && !($staff = Staff::lookup($staff))) return false; if (!$staff->isAvailable() || !$this->setStaffId($staff->getId())) return false; $this->onAssign($staff, $note, $alert); global $thisstaff; $data = array(); if ($thisstaff && $staff->getId() == $thisstaff->getId()) $data['claim'] = true; else $data['staff'] = $staff->getId(); $this->logEvent('assigned', $data, $user); $key = $data['claim'] ? 'claim' : 'auto'; $type = array('type' => 'assigned', $key => true); Signal::send('object.edited', $this, $type); if (($referral=$this->hasReferral($staff,ObjectModel::OBJECT_TYPE_STAFF))) $referral->delete(); return true; } function assignToTeam($team, $note, $alert=true, $user=null) { 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', array('team' => $team->getId()), $user); if (($referral=$this->hasReferral($team,ObjectModel::OBJECT_TYPE_TEAM))) $referral->delete(); return true; } function assign(AssignmentForm $form, &$errors, $alert=true) { global $thisstaff; $evd = array(); $audit = array(); $refer = null; $dept = $this->getDept(); $assignee = $form->getAssignee(); if ($assignee instanceof Staff) { if ($this->getStaffId() == $assignee->getId()) { $errors['assignee'] = sprintf(__('%s already assigned to %s'), __('Ticket'), __('the agent') ); } elseif (!$assignee->isAvailable()) { $errors['assignee'] = __('Agent is unavailable for assignment'); } elseif (!$dept->canAssign($assignee)) { $errors['err'] = __('Permission denied'); } else { $refer = $this->staff ?: null; $this->staff_id = $assignee->getId(); if ($thisstaff && $thisstaff->getId() == $assignee->getId()) { $alert = false; $evd['claim'] = true; $audit = array('staff' => $assignee->getName()->name,'claim' => true); } else { $evd['staff'] = array($assignee->getId(), (string) $assignee->getName()->getOriginal()); $audit = array('staff' => $assignee->getName()->name); } if (($referral=$this->hasReferral($assignee,ObjectModel::OBJECT_TYPE_STAFF))) $referral->delete(); } } elseif ($assignee instanceof Team) { if ($this->getTeamId() == $assignee->getId()) { $errors['assignee'] = sprintf(__('%s already assigned to %s'), __('Ticket'), __('the team') ); } elseif (!$dept->canAssign($assignee)) { $errors['err'] = __('Permission denied'); } else { $refer = $this->team ?: null; $this->team_id = $assignee->getId(); $evd = array('team' => $assignee->getId()); $audit = array('team' => $assignee->getName()); if (($referral=$this->hasReferral($assignee,ObjectModel::OBJECT_TYPE_TEAM))) $referral->delete(); } } else { $errors['assignee'] = __('Unknown assignee'); } if ($errors || !$this->save(true)) return false; $this->logEvent('assigned', $evd); $type = array('type' => 'assigned'); $type += $audit; Signal::send('object.edited', $this, $type); $this->onAssign($assignee, $form->getComments(), $alert); if ($refer && $form->refer()) $this->getThread()->refer($refer); return true; } // Unassign primary assignee function unassign() { // We can't release what is not assigned buddy! if (!$this->isAssigned()) 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; return true; } function release($info=array(), &$errors) { if ($info['sid'] && $info['tid']) return $this->unassign(); elseif ($info['sid'] && $this->setStaffId(0)) return true; elseif ($info['tid'] && $this->setTeamId(0)) return true; return false; } function refer(ReferralForm $form, &$errors, $alert=true) { global $thisstaff; $evd = array(); $audit = array(); $referee = $form->getReferee(); switch (true) { case $referee instanceof Staff: $dept = $this->getDept(); if ($this->getStaffId() == $referee->getId()) { $errors['agent'] = sprintf(__('%s is assigned to %s'), __('Ticket'), __('the agent') ); } elseif(!$referee->isAvailable()) { $errors['agent'] = sprintf(__('Agent is unavailable for %s'), __('referral')); } else { $evd['staff'] = array($referee->getId(), (string) $referee->getName()->getOriginal()); $audit = array('staff' => $referee->getName()->name); } break; case $referee instanceof Team: if ($this->getTeamId() == $referee->getId()) { $errors['team'] = sprintf(__('%s is assigned to %s'), __('Ticket'), __('the team') ); } else { //TODO:: $evd = array('team' => $referee->getId()); $audit = array('team' => $referee->getName()); } break; case $referee instanceof Dept: if ($this->getDeptId() == $referee->getId()) { $errors['dept'] = sprintf(__('%s is already in %s'), __('Ticket'), __('the department') ); } else { //TODO:: $evd = array('dept' => $referee->getId()); $audit = array('dept' => $referee->getName()); } break; default: $errors['target'] = __('Unknown referral'); } if (!$errors && !$this->getThread()->refer($referee)) $errors['err'] = __('Unable to refer ticket'); if ($errors) return false; $this->logEvent('referred', $evd); $type = array('type' => 'referred'); $type += $audit; Signal::send('object.edited', $this, $type); return true; } function systemReferral($emails) { global $cfg; if (!$thread = $this->getThread()) return; $eventEmails = array(); $events = ThreadEvent::objects() ->filter(array('thread_id' => $thread->getId(), 'event__name' => 'transferred')); if ($events) { foreach ($events as $e) { $emailId = Dept::getEmailIdById($e->dept_id) ?: $cfg->getDefaultEmailId(); if (!in_array($emailId, $eventEmails)) $eventEmails[] = $emailId; } } foreach ($emails as $id) { $refer = $eventEmails ? !in_array($id, $eventEmails) : true; if ($id != $this->email_id && $refer && ($email=Email::lookup($id)) && $this->getDeptId() != $email->getDeptId() && ($dept=Dept::lookup($email->getDeptId())) && $this->getThread()->refer($dept) ) $this->logEvent('referred', array('dept' => $dept->getId())); } } //Change ownership function changeOwner($user) { global $thisstaff; if (!$user || ($user->getId() == $this->getOwnerId()) || !($this->checkStaffPerm($thisstaff, Ticket::PERM_EDIT)) ) { return false; } $this->user_id = $user->getId(); if (!$this->save()) return false; unset($this->user); $this->collaborators = null; $this->recipients = null; // Remove the new owner from list of collaborators $c = Collaborator::lookup(array( 'user_id' => $user->getId(), 'thread_id' => $this->getThreadId() )); if ($c) $c->delete(); $this->logEvent('edited', array('owner' => $user->getId(), 'fields' => array('Ticket Owner' => $user->getName()->name))); return true; } // Insert message from client function postMessage($vars, $origin='', $alerts=true) { global $cfg; if ($origin) $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']; //see if message should go to a parent ticket if ($this->isChild() && $this->getMergeType() != 'visual') $parent = self::lookup($this->getPid()); $ticket = $parent ?: $this; $errors = array(); if ($vars['userId'] != $ticket->user_id) { if ($vars['userId']) { $user = User::lookup($vars['userId']); } elseif ($vars['header'] && ($hdr= Mail_parse::splitHeaders($vars['header'], true)) && $hdr['From'] && ($addr= Mail_Parse::parseAddressList($hdr['From']))) { $info = array( 'name' => $addr[0]->personal, 'email' => $addr[0]->mailbox.'@'.$addr[0]->host); if ($user=User::fromVars($info)) $vars['userId'] = $user->getId(); } if ($user) { $c = $ticket->getThread()->addCollaborator($user,array(), $errors); } } // Get active recipients of the response // Initial Message from Tickets created by Agent if ($vars['reply-to']) $recipients = $ticket->getRecipients($vars['reply-to'], $vars['ccs']); // Messages from Web Portal elseif (strcasecmp($origin, 'email')) { $recipients = $ticket->getRecipients('all'); foreach ($recipients as $key => $recipient) { if (!$recipientContact = $recipient->getContact()) continue; $userId = $recipientContact->getUserId() ?: $recipientContact->getId(); // Do not list the poster as a recipient if ($userId == $vars['userId']) unset($recipients[$key]); } } if ($recipients && $recipients instanceof MailingList) $vars['thread_entry_recipients'] = $recipients->getEmailAddresses(); if (!($message = $ticket->getThread()->addMessage($vars, $errors))) return null; $ticket->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() == $ticket->getUserId())? 1: 0); $collabs = array(); foreach ($vars['recipients'] as $recipient) { // Skip virtual delivered-to addresses if (strcasecmp($recipient['source'], 'delivered-to') === 0) continue; if (($cuser=User::fromVars($recipient))) { if (!$existing = Collaborator::getIdByUserId($cuser->getId(), $ticket->getThreadId())) { $_errors = array(); if ($c=$ticket->addCollaborator($cuser, $info, $_errors, false)) { $c->setCc($c->active); // FIXME: This feels very unwise — should be a // string indexed array for future $collabs[$c->user_id] = array( 'name' => $c->getName()->getOriginal(), 'src' => $recipient['source'], ); } } } } // TODO: Can collaborators add others? if ($collabs) { $ticket->logEvent('collab', array('add' => $collabs), $message->user); $type = array('type' => 'collab', 'add' => $collabs); Signal::send('object.created', $ticket, $type); } } // Do not auto-respond to bounces and other auto-replies $autorespond = isset($vars['mailflags']) ? !$vars['mailflags']['bounce'] && !$vars['mailflags']['auto-reply'] : true; $reopen = $autorespond; // Do not reopen bounces if ($autorespond && $message->isBounceOrAutoReply()) $autorespond = $reopen= false; elseif ($autorespond && isset($vars['autorespond'])) $autorespond = $vars['autorespond']; $ticket->onMessage($message, ($autorespond && $alerts), $reopen); //must be called b4 sending alerts to staff. if ($autorespond && $alerts && $cfg && $cfg->notifyCollabsONNewMessage() && strcasecmp($origin, 'email')) { //when user replies, this is where collabs notified $ticket->notifyCollaborators($message, array('signature' => '')); } if (!($alerts && $autorespond)) return $message; //Our work is done... $dept = $ticket->getDept(); $variables = array( 'message' => $message, 'poster' => ($vars['poster'] ? $vars['poster'] : $ticket->getName()) ); $options = array('thread'=>$message); // If enabled...send alert to staff (New Message Alert) if ($cfg->alertONNewMessage() && ($email = $dept->getAlertEmail()) && ($tpl = $dept->getTemplate()) && ($msg = $tpl->getNewMessageAlertMsgTemplate()) ) { $msg = $ticket->replaceVars($msg->asArray(), $variables); // Build list of recipients and fire the alerts. $recipients = array(); //Last respondent. if ($cfg->alertLastRespondentONNewMessage() && ($lr = $ticket->getLastRespondent())) $recipients[] = $lr; //Assigned staff if any...could be the last respondent if ($cfg->alertAssignedONNewMessage() && $ticket->isAssigned()) { if ($staff = $ticket->getStaff()) $recipients[] = $staff; elseif ($team = $ticket->getTeam()) $recipients = array_merge($recipients, $team->getMembersForAlerts()); } // 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->getMembersForAlerts()); 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(); } } $type = array('type' => 'message', 'uid' => $vars['userId']); Signal::send('object.created', $this, $type); return $message; } function postCannedReply($canned, $message, $alert=true) { global $ost, $cfg; if ((!is_object($canned) && !($canned=Canned::lookup($canned))) || !$canned->isEnabled() ) { return false; } $files = array(); foreach ($canned->attachments->getAll() as $att) { $files[] = array('id' => $att->file_id, 'name' => $att->getName()); $_SESSION[':cannedFiles'][$att->file_id] = $att->getName(); } if ($cfg->isRichTextEnabled()) $response = new HtmlThreadEntryBody( $this->replaceVars($canned->getHtml())); else $response = new TextThreadEntryBody( $this->replaceVars($canned->getPlainText())); $info = array('msgId' => $message instanceof ThreadEntry ? $message->getId() : 0, 'poster' => __('SYSTEM (Canned Reply)'), 'response' => $response, 'files' => $files ); $errors = array(); if (!($response=$this->postReply($info, $errors, false, 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('thread' => $response); if (($message instanceof ThreadEntry) && $message->getUserId() == $this->getUserId() && ($mid=$message->getEmailMessageId())) { $options += array( 'inreplyto' => $mid, 'references' => $message->getEmailReferences() ); } $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'], $attachments, $options); } return $response; } /* public */ function postReply($vars, &$errors, $alert=true, $claim=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']; // clear db cache $this->getThread()->_collaborators = null; // Get active recipients of the response $recipients = $this->getRecipients($vars['reply-to'], $vars['ccs']); if ($recipients instanceof MailingList) $vars['thread_entry_recipients'] = $recipients->getEmailAddresses(); if (!($response = $this->getThread()->addResponse($vars, $errors))) return null; $dept = $this->getDept(); $assignee = $this->getStaff(); // Set status if new is selected if ($vars['reply_status_id'] && ($status = TicketStatus::lookup($vars['reply_status_id'])) && $status->getId() != $this->getStatusId()) $this->setStatus($status); // Claim on response bypasses the department assignment restrictions $claim = ($claim && $cfg->autoClaimTickets() && !$dept->disableAutoClaim()); if ($claim && $thisstaff && $this->isOpen() && !$this->getStaffId()) { $this->setStaffId($thisstaff->getId()); //direct assignment; } $this->onResponse($response, array('assignee' => $assignee)); //do house cleaning.. $this->lastrespondent = $response->staff; $type = array('type' => 'message'); Signal::send('object.created', $this, $type); /* email the user?? - if disabled - then bail out */ if (!$alert) return $response; //allow agent to send from different dept email if (!$vars['from_email_id'] || !($email = Email::lookup($vars['from_email_id']))) $email = $dept->getEmail(); $options = array('thread'=>$response); $signature = $from_name = ''; if ($thisstaff && $vars['signature']=='mine') $signature=$thisstaff->getSignature(); elseif ($vars['signature']=='dept' && $dept->isPublic()) $signature=$dept->getSignature(); if ($thisstaff && ($type=$thisstaff->getReplyFromNameType())) { switch ($type) { case 'mine': if (!$cfg->hideStaffName()) $from_name = (string) $thisstaff->getName(); break; case 'dept': if ($dept->isPublic()) $from_name = $dept->getName(); break; case 'email': default: $from_name = $email->getName(); } if ($from_name) $options += array('from_name' => $from_name); } $variables = array( 'response' => $response, 'signature' => $signature, 'staff' => $thisstaff, 'poster' => $thisstaff ); if ($email && $recipients && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate())) { // Add ticket link (possibly with authtoken) if the ticket owner // is the only recipient on a ticket with collabs if (count($recipients) == 1 && $this->getNumCollaborators() && ($contact = $recipients->offsetGet(0)->getContact()) && ($contact instanceof TicketOwner)) $variables['recipient.ticket_link'] = $contact->getTicketLink(); $msg = $this->replaceVars($msg->asArray(), $variables + array('recipient' => $this->getOwner()) ); // Attachments $attachments = $cfg->emailAttachments() ? $response->getAttachments() : array(); //Send email to recepients $email->send($recipients, $msg['subj'], $msg['body'], $attachments, $options); } return $response; } function postReplyCustom($vars, &$errors, $alert=true, $claim=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']; // Add new collaborators (if any). //if (isset($vars['ccs']) && count($vars['ccs'])) // $this->addCollaborators($vars['ccs'], array(), $errors); // //if ($collabs = $this->getCollaborators()) { // foreach ($collabs as $collaborator) { // $cid = $collaborator->getUserId(); // // Enable collaborators if they were reselected // if (!$collaborator->isActive() && ($vars['ccs'] && in_array($cid, $vars['ccs']))) // $collaborator->setFlag(Collaborator::FLAG_ACTIVE, true); // // Disable collaborators if they were unchecked // elseif ($collaborator->isActive() && (!$vars['ccs'] || !in_array($cid, $vars['ccs']))) // $collaborator->setFlag(Collaborator::FLAG_ACTIVE, false); // // $collaborator->save(); // } //} // clear db cache //$this->getThread()->_collaborators = null; // Get active recipients of the response $recipients = $this->getRecipients($vars['reply-to'], $vars['ccs']); if ($recipients instanceof MailingList) $vars['thread_entry_recipients'] = $recipients->getEmailAddresses(); if (!($response = $this->getThread()->addResponse($vars, $errors))) return null; $dept = $this->getDept(); $assignee = $this->getStaff(); // Set status if new is selected if ($vars['reply_status_id'] && ($status = TicketStatus::lookup($vars['reply_status_id'])) && $status->getId() != $this->getStatusId()) $this->setStatus($status); // Claim on response bypasses the department assignment restrictions $claim = ($claim && $cfg->autoClaimTickets() && !$dept->disableAutoClaim()); if ($claim && $thisstaff && $this->isOpen() && !$this->getStaffId()) { $this->setStaffId($thisstaff->getId()); //direct assignment; } if(!$this->isAssigned()) { $this->setStaffId($vars['staffId']); } $this->lastrespondent = $response->staff; $this->onResponse($response, array('assignee' => $assignee)); //do house cleaning.. /* email the user?? - if disabled - then bail out */ if (!$alert) return $response; //allow agent to send from different dept email if (!$vars['from_email_id'] || !($email = Email::lookup($vars['from_email_id']))) $email = $dept->getEmail(); $options = array('thread'=>$response); $signature = $from_name = ''; if ($thisstaff && $vars['signature']=='mine') $signature=$thisstaff->getSignature(); elseif ($vars['signature']=='dept' && $dept->isPublic()) $signature=$dept->getSignature(); if ($thisstaff && ($type=$thisstaff->getReplyFromNameType())) { switch ($type) { case 'mine': if (!$cfg->hideStaffName()) $from_name = (string) $thisstaff->getName(); break; case 'dept': if ($dept->isPublic()) $from_name = $dept->getName(); break; case 'email': default: $from_name = $email->getName(); } if ($from_name) $options += array('from_name' => $from_name); } $variables = array( 'response' => $response, 'signature' => $signature, 'staff' => $thisstaff, 'poster' => $thisstaff ); if ($email && $recipients && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), $variables + array('recipient' => $this->getOwner()) ); // Attachments $attachments = $cfg->emailAttachments() ? $response->getAttachments() : array(); //Send email to recepients $email->send($recipients, $msg['subj'], $msg['body'], $attachments, $options); } 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, $data=null, $user=null, $annul=null) { switch ($state) { case 'collab': case 'transferred': $type = $data; $type['type'] = $state; break; case 'edited': $type = array('type' => $state, 'fields' => $data['fields'] ? $data['fields'] : $data); break; case 'assigned': case 'referred': break; default: $type = array('type' => $state); break; } if ($type) Signal::send('object.created', $this, $type); if ($this->getThread()) $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul); } //Insert Internal Notes function logNote($title, $note, $poster='SYSTEM', $alert=true) { // Unless specified otherwise, assume HTML if ($note && is_string($note)) $note = new HtmlThreadEntryBody($note); $errors = array(); return $this->postNote( array( 'title' => $title, 'note' => $note, ), $errors, $poster, $alert ); } function postNote($vars, &$errors, $poster=false, $alert=true) { global $cfg, $thisstaff; //Who is posting the note - staff or system? or user? if ($vars['staffId'] && !$poster) $poster = Staff::lookup($vars['staffId']); $vars['staffId'] = $vars['staffId'] ?: 0; if ($poster && is_object($poster) && !$vars['userId']) { $vars['staffId'] = $poster->getId(); $vars['poster'] = $poster->getName(); } elseif ($poster) { //string $vars['poster'] = $poster; } elseif (!isset($vars['poster'])) { $vars['poster'] = 'SYSTEM'; } 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['mailflags']) // No alerts for bounce and auto-reply emails ? !$vars['mailflags']['bounce'] && !$vars['mailflags']['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'])) ) { $this->setStatus($status); } $activity = $vars['activity'] ?: _S('New Internal Note'); $this->onActivity(array( 'activity' => $activity, 'threadentry' => $note, 'assignee' => $assignee ), $alert); $type = array('type' => 'note'); Signal::send('object.created', $this, $type); return $note; } // Threadable interface function postThreadEntry($type, $vars, $options=array()) { $errors = array(); switch ($type) { case 'M': return $this->postMessage($vars, $vars['origin']); case 'N': return $this->postNote($vars, $errors); case 'R': return $this->postReply($vars, $errors); } } // Print ticket... export the ticket thread as PDF. function pdfExport($psize='Letter', $notes=false, $events=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, $events); $name = 'Ticket-'.$this->getNumber().'.pdf'; Http::download($name, 'application/pdf', $pdf->output($name, 'S')); //Remember what the user selected - for autoselect on the next print. $_SESSION['PAPER_SIZE'] = $psize; exit; } function zipExport($notes=true, $tasks=false) { $exporter = new TicketZipExporter($this); $exporter->download(['notes'=>$notes, 'tasks'=>$tasks]); exit; } function delete($comments='') { global $ost, $thisstaff; //delete just orphaned ticket thread & associated attachments. // Fetch thread prior to removing ticket entry $t = $this->getThread(); if (!parent::delete()) return false; //deleting parent ticket if ($children = $this->getChildren()) { foreach ($children as $childId) { if (!($child = Ticket::lookup($childId[0]))) continue; $child->setPid(NULL); $child->setMergeType(3); $child->save(); $childThread = $child->getThread(); $childThread->object_type = 'T'; $childThread->save(); } } //deleting child ticket if ($this->isChild()) { $parent = Ticket::lookup($this->ticket_pid); if ($parent->isParent() && count($parent->getChildren()) == 0) { $parent->setMergeType(3); $parent->save(); } } else $t->delete(); $this->logEvent('deleted'); foreach (DynamicFormEntry::forTicket($this->getId()) as $form) $form->delete(); $this->deleteDrafts(); if ($this->cdata) $this->cdata->delete(); // 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 save($refetch=false) { if ($this->dirty) { $this->updated = SqlFunction::NOW(); if (isset($this->dirty['status_id']) && PHP_SAPI !== 'cli') // Refetch the queue counts SavedQueue::clearCounts(); } return parent::save($this->dirty || $refetch); } function update($vars, &$errors) { global $cfg, $thisstaff; if (!$cfg || !($this->checkStaffPerm($thisstaff, Ticket::PERM_EDIT)) ) { 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['user_id'] = array('type'=>'int', 'required'=>0, 'error'=>__('Invalid user-id')); if (!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] = sprintf('%s — %s', __('Missing or invalid data'), __('Correct any errors below and try again')); $vars['note'] = ThreadEntryBody::clean($vars['note']); if ($vars['duedate']) { if ($this->isClosed()) $errors['duedate']=__('Due date can NOT be set on a closed ticket'); elseif (strtotime($vars['duedate']) === false) $errors['duedate']=__('Invalid due date'); elseif (Misc::user2gmtime($vars['duedate']) <= Misc::user2gmtime()) $errors['duedate']=__('Due date must be in the future'); } if (isset($vars['source']) // Check ticket source if provided && !array_key_exists($vars['source'], Ticket::getSources())) $errors['source'] = sprintf( __('Invalid source given - %s'), Format::htmlchars($vars['source'])); $topic = Topic::lookup($vars['topicId']); if($topic && !$topic->isActive()) $errors['topicId']= sprintf(__('%s selected must be active'), __('Help Topic')); // 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->filterFields(function($f) { return !$f->isStorable(); }); $form->setSource($_POST); if (!$form->isValid(function($f) { return $f->isVisibleToStaff() && $f->isEditableToStaff(); })) { $errors = array_merge($errors, $form->errors()); } } if ($errors) return false; // Decide if we need to keep the just selected SLA $keepSLA = ($this->getSLAId() != $vars['slaId']); $this->topic_id = $vars['topicId']; $this->sla_id = $vars['slaId']; $this->source = $vars['source']; $this->duedate = $vars['duedate'] ? date('Y-m-d H:i:s',Misc::dbtime($vars['duedate'])) : null; if ($vars['user_id']) $this->user_id = $vars['user_id']; if ($vars['duedate']) // We are setting new duedate... $this->isoverdue = 0; $changes = array(); foreach ($this->dirty as $F=>$old) { switch ($F) { case 'topic_id': case 'user_id': case 'source': case 'duedate': case 'sla_id': $changes[$F] = array($old, $this->{$F}); } } if (!$this->save()) return false; $vars['note'] = ThreadEntryBody::clean($vars['note']); if ($vars['note']) $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff); // Update dynamic meta-data foreach ($forms as $form) { if ($C = $form->getChanges()) $changes['fields'] = ($changes['fields'] ?: array()) + $C; // Drop deleted forms $idx = array_search($form->getId(), $vars['forms']); if ($idx === false) { $form->delete(); } else { $form->set('sort', $idx); $form->saveAnswers(function($f) { return $f->isVisibleToStaff() && $f->isEditableToStaff(); } ); } } if ($changes) { $this->logEvent('edited', $changes); } // Reselect SLA if transient if (!$keepSLA && (!$this->getSLA() || $this->getSLA()->isTransient()) ) { $this->selectSLAId(); } if (!$this->save()) return false; $this->updateEstDueDate(); Signal::send('model.updated', $this); return true; } function updateField($form, &$errors) { global $thisstaff, $cfg; if (!($field = $form->getField('field'))) return null; $updateDuedate = false; if (!($changes = $field->getChanges())) $errors['field'] = sprintf(__('%s is already assigned this value'), __($field->getLabel())); else { if ($field->answer) { if (!$field->isEditableToStaff()) $errors['field'] = sprintf(__('%s can not be edited'), __($field->getLabel())); elseif (!$field->save(true)) $errors['field'] = __('Unable to update field'); // Strip tags from TextareaFields to ensure event data is not // truncated if ($field instanceof TextareaField) foreach ($changes as $k=>$v) $changes[$k] = Format::truncate(Format::striptags($v), 200); $changes['fields'] = array($field->getId() => $changes); } else { $val = $field->getClean(); $fid = $field->get('name'); // Convert duedate to DB timezone. if ($fid == 'duedate') { if (empty($val)) $val = null; elseif ($dt = Format::parseDateTime($val)) { // Make sure the due date is valid if (Misc::user2gmtime($val) <= Misc::user2gmtime()) $errors['field']=__('Due date must be in the future'); else { $dt->setTimezone(new DateTimeZone($cfg->getDbTimezone())); $val = $dt->format('Y-m-d H:i:s'); } } } elseif (is_object($val)) $val = $val->getId(); $changes = array(); $this->{$fid} = $val; foreach ($this->dirty as $F=>$old) { switch ($F) { case 'sla_id': case 'duedate': $updateDuedate = true; case 'topic_id': case 'user_id': case 'source': $changes[$F] = array($old, $this->{$F}); } } if (!$errors && !$this->save()) $errors['field'] = __('Unable to update field'); } } if ($errors) return false; // Record the changes $this->logEvent('edited', $changes); // Log comments (if any) if (($comments = $form->getField('comments')->getClean())) { $title = sprintf(__('%s updated'), __($field->getLabel())); $_errors = array(); $this->postNote( array('note' => $comments, 'title' => $title), $_errors, $thisstaff, false); } $this->lastupdate = SqlFunction::NOW(); if ($updateDuedate) $this->updateEstDueDate(); $this->save(); Signal::send('model.updated', $this); return true; } /*============== Static functions. Use Ticket::function(params); =============nolint*/ static function getIdByNumber($number, $email=null, $ticket=false) { if (!$number) return 0; $query = static::objects() ->filter(array('number' => $number)); if ($email) $query->filter(Q::any(array( 'user__emails__address' => $email, 'thread__collaborators__user__emails__address' => $email ))); if (!$ticket) { $query = $query->values_flat('ticket_id'); if ($row = $query->first()) return $row[0]; } else { return $query->first(); } } static function lookupByNumber($number, $email=null) { return static::getIdByNumber($number, $email, true); } static function isTicketNumberUnique($number) { $num = static::objects() ->filter(array('number' => $number)) ->count(); return ($num === 0); } static function getChildTickets($pid) { return Ticket::objects() ->filter(array('ticket_pid'=>$pid)) ->values_flat('ticket_id', 'number', 'ticket_pid', 'sort', 'thread__id', 'user_id', 'cdata__subject', 'user__name', 'flags') ->annotate(array('tasks' => SqlAggregate::COUNT('tasks__id', true), 'collaborators' => SqlAggregate::COUNT('thread__collaborators__id'), 'entries' => SqlAggregate::COUNT('thread__entries__id'),)) ->order_by('sort'); } /* 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, $postCreate=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) { if ($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 (($filter=Banlist::isBanned($vars['email']))) { throw new RejectedException($filter, $vars); } // Init ticket filters... $ticket_filter = new TicketFilter($origin, $vars); $ticket_filter->apply($vars, $postCreate); } 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, $thisstaff; // 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 $f->isVisibleToStaff(); case 'web': return $f->isVisibleToUsers(); 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 ($vars['uid']) $user = User::lookup($vars['uid']); $id=0; $fields=array(); 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'] = sprintf('%s — %s', __('Missing or invalid data'), __('Correct any errors below and try again')); // Make sure the due date is valid if ($vars['duedate']) { if (strtotime($vars['duedate']) === false) $errors['duedate']=__('Invalid due date'); elseif (Misc::user2gmtime($vars['duedate']) <= Misc::user2gmtime()) $errors['duedate']=__('Due date must be in the future'); } $topic_forms = array(); if (!$errors) { // Handle the forms associate with the help topics. Instanciate the // entries, disable and track the requested disabled fields. if ($vars['topicId']) { if ($__topic=Topic::lookup($vars['topicId'])) { foreach ($__topic->getForms() as $idx=>$__F) { $disabled = array(); foreach ($__F->getFields() as $field) { if (!$field->isEnabled() && $field->hasFlag(DynamicFormField::FLAG_ENABLED)) $disabled[] = $field->get('id'); } // Special handling for the ticket form — disable fields // requested to be disabled as per the help topic. if ($__F->get('type') == 'T') { foreach ($form->getFields() as $field) { if (false !== array_search($field->get('id'), $disabled)) $field->disable(); } $form->sort = $idx; $__F = $form; } else { $__F = $__F->instanciate($idx); $__F->setSource($vars); $topic_forms[] = $__F; } // Track fields currently disabled $__F->extra = JsonDataEncoder::encode(array( 'disable' => $disabled )); } } } try { $vars = self::filterTicketData($origin, $vars, array_merge(array($form), $topic_forms), $user, false); } 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); $can_create = !$thisstaff || $thisstaff->hasPerm(User::PERM_CREATE); if (!$user_form->isValid($field_filter('user')) || !($user=User::fromVars($user_form->getClean(), $can_create)) ) { $errors['user'] = $can_create ? __('Incomplete client information') : __('You do not have permission to create users.'); } } } if (!$form->isValid($field_filter('ticket'))) $errors += $form->errors(); if ($vars['topicId']) { if (($topic=Topic::lookup($vars['topicId'])) && $topic->isActive()) { foreach ($topic_forms as $topic_form) { $TF = $topic_form->getForm($vars); if (!$TF->isValid($field_filter('topic'))) $errors = array_merge($errors, $TF->errors()); } } else { $vars['topicId'] = 0; } } // Any errors above are 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(); // Unset slaId if 0 to use the Help Topic SLA or Default SLA if ($vars['slaId'] == 0) unset($vars['slaId']); //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(); $ticket = new static(array( 'created' => SqlFunction::NOW(), 'lastupdate' => SqlFunction::NOW(), 'number' => $number, 'user' => $user, 'dept_id' => $deptId, 'topic_id' => $topicId, 'ip_address' => $ipaddress, 'source' => $source, )); if (isset($vars['emailId']) && $vars['emailId']) $ticket->email_id = $vars['emailId']; //Make sure the origin is staff - avoid firebug hack! if ($vars['duedate'] && !strcasecmp($origin,'staff')) $ticket->duedate = date('Y-m-d G:i', Misc::dbtime($vars['duedate'])); if (!$ticket->save()) return null; if (!($thread = TicketThread::create($ticket->getId()))) return null; /* -------------------- POST CREATE ------------------------ */ $vars['ticket'] = $ticket; self::filterTicketData($origin, $vars, array_merge(array($form), $topic_forms), $user, true); // Save the (common) dynamic form // Ensure we have a subject $subject = $form->getAnswer('subject'); if ($subject && !$subject->getValue() && $topic) $subject->setValue($topic->getFullName()); $form->setTicketId($ticket->getId()); $form->save(); // Save the form data from the help-topic form, if any foreach ($topic_forms as $topic_form) { $topic_form->setTicketId($ticket->getId()); $topic_form->save(); } $ticket->loadDynamicData(true); $dept = $ticket->getDept(); // Start tracking ticket lifecycle events (created should come first!) $ticket->logEvent('created', null, $thisstaff ?: $user); // Set default ticket status (if none) for Thread::getObject() // in addCollaborators() if ($ticket->getStatusId() <= 0) $ticket->setStatusId($cfg->getDefaultTicketStatusId()); // Add collaborators (if any) if (isset($vars['ccs']) && count($vars['ccs'])) $ticket->addCollaborators($vars['ccs'], array(), $errors); // Add organizational collaborators if ($org && $org->autoAddCollabs()) { $pris = $org->autoAddPrimaryContactsAsCollabs(); $members = $org->autoAddMembersAsCollabs(); $settings = array('isactive' => true); $collabs = array(); foreach ($org->allMembers() as $u) { $_errors = array(); if ($members || ($pris && $u->isPrimaryContact())) { if ($c = $ticket->addCollaborator($u, $settings, $_errors)) { $collabs[] = (string) $c; } } } //TODO: Can collaborators add others? if ($collabs) { $ticket->logEvent('collab', array('org' => $org->getId())); } } //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); // If a message was posted, flag it as the orignal message. This // needs to be done on new ticket, so as to otherwise separate the // concept from the first message entry in a thread. if ($message instanceof ThreadEntry) { $message->setFlag(ThreadEntry::FLAG_ORIGINAL_MESSAGE); $message->save(); } //check to see if ticket was created from a thread if ($_SESSION[':form-data']['ticketId'] || $_SESSION[':form-data']['taskId']) { $oldTicket = Ticket::lookup($_SESSION[':form-data']['ticketId']); $oldTask = Task::lookup($_SESSION[':form-data']['taskId']); //add internal note to new ticket. //New ticket should have link to old task/ticket: $link = sprintf('#%s', $oldTicket ? 'tickets' : 'tasks', $oldTicket ? $oldTicket->getId() : $oldTask->getId(), $oldTicket ? $oldTicket->getNumber() : $oldTask->getNumber()); $note = array( 'title' => __('Ticket Created From Thread Entry'), 'body' => sprintf(__( // %1$s is the word Ticket or Task, %2$s will be a link to it 'This Ticket was created from %1$s %2$s'), $oldTicket ? __('Ticket') : __('Task'), $link) ); $ticket->logNote($note['title'], $note['body'], $thisstaff); //add internal note to referenced ticket/task // Old ticket/task should have link to new ticket $ticketLink = sprintf('#%s', $ticket->getId(), $ticket->getNumber()); $entryLink = sprintf('%s', $_SESSION[':form-data']['eid'], Format::datetime($_SESSION[':form-data']['timestamp'])); $ticketNote = array( 'title' => __('Ticket Created From Thread Entry'), 'body' => sprintf(__('Ticket %1$s
Thread Entry: %2$s'), $ticketLink, $entryLink) ); $taskNote = array( 'title' => __('Ticket Created From Thread Entry'), 'note' => sprintf(__('Ticket %1$s
Thread Entry: %2$s'), $ticketLink, $entryLink) ); if ($oldTicket) $oldTicket->logNote($ticketNote['title'], $ticketNote['body'], $thisstaff); elseif ($oldTask) $oldTask->postNote($taskNote, $errors, $thisstaff); } // Configure service-level-agreement for this ticket $ticket->selectSLAId($vars['slaId']); // Set status $status = TicketStatus::lookup($statusId); if (!$status || !$ticket->setStatus($status, false, $errors, !strcasecmp($origin, 'staff'))) { // Tickets _must_ have a status. Forceably set one here $ticket->setStatusId($cfg->getDefaultTicketStatusId()); } // Only do assignment if the ticket is in an open state if ($ticket->isOpen()) { // Assign ticket to staff or team (new ticket by staff) if ($vars['assignId']) { $asnform = $ticket->getAssignmentForm(array( 'assignee' => $vars['assignId'], 'comments' => $vars['note']) ); $e = array(); $ticket->assign($asnform, $e); } else { // Auto assign staff or team - auto assignment based on filter // rules. Both team and staff can be assigned $username = __('Ticket Filter'); if ($vars['staffId']) $ticket->assignToStaff($vars['staffId'], false, true, $username); if ($vars['teamId']) // No team alert if also assigned to an individual agent $ticket->assignToTeam($vars['teamId'], false, !$vars['staffId'], $username); } } // Update the estimated due date in the database $ticket->updateEstDueDate(); /********** 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['mailflags']) && $vars['mailflags']['bounce']) $autorespond = false; if ($autorespond && $message instanceof ThreadEntry && $message->isAutoReply()) $autorespond = false; // Post canned auto-response IF any (disables new ticket auto-response). if ($vars['cannedResponseId'] && $ticket->postCannedReply($vars['cannedResponseId'], $message, $autorespond)) { $ticket->markUnAnswered(); //Leave the ticket as unanswred. $autorespond = false; } if ($vars['system_emails']) $ticket->systemReferral($vars['system_emails']); // 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 instanceof ThreadEntry && $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')); } // Fire post-create signal (for extra email sending, searching) Signal::send('ticket.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) return false; if ($vars['deptId'] && ($dept=Dept::lookup($vars['deptId'])) && ($role = $thisstaff->getRole($dept)) && !$role->hasPerm(Ticket::PERM_CREATE) ) { $errors['err'] = sprintf(__('You do not have permission to create a ticket in %s'), __('this department')); return false; } if (isset($vars['source']) // Check ticket source if provided && !array_key_exists($vars['source'], Ticket::getSources())) $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'); } // Ensure agent has rights to make assignment in the cited // department if ($vars['assignId'] && !( $role ? ($role->hasPerm(Ticket::PERM_ASSIGN) || $role->__new__) : $thisstaff->hasPerm(Ticket::PERM_ASSIGN, false) )) { $errors['assignId'] = __('Action Denied. You are not allowed to assign/reassign tickets.'); } // TODO: Deny action based on selected department. $vars['response'] = ThreadEntryBody::clean($vars['response']); $vars['note'] = ThreadEntryBody::clean($vars['note']); $create_vars = $vars; $tform = TicketForm::objects()->one()->getForm($create_vars); $mfield = $tform->getField('message'); $create_vars['message'] = $mfield->getClean(); $create_vars['files'] = $mfield->getWidget()->getAttachments()->getFiles(); if (!($ticket=self::create($create_vars, $errors, 'staff', false))) return false; $vars['msgId']=$ticket->getLastMsgId(); // Effective role for the department $role = $ticket->getRole($thisstaff); $alert = strcasecmp('none', $vars['reply-to']); // post response - if any $response = null; if ($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) { $vars['response'] = $ticket->replaceVars($vars['response']); // $vars['cannedatachments'] contains the attachments placed on // the response form. $response = $ticket->postReply($vars, $errors, ($alert && !$cfg->notifyONNewStaffTicket())); } // Not assigned...save optional note if any if (!$vars['assignId'] && $vars['note']) { if (!$cfg->isRichTextEnabled()) $vars['note'] = new TextThreadEntryBody($vars['note']); $ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false); } if (!$cfg->notifyONNewStaffTicket() || !$alert || !($dept=$ticket->getDept()) ) { return $ticket; //No alerts. } // Notice Recipients $recipients = $ticket->getRecipients($vars['reply-to']); // Send Notice to user --- if requested AND enabled!! if (($tpl=$dept->getTemplate()) && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) && ($email=$dept->getEmail()) ) { $attachments = array(); $message = $ticket->getLastMessage(); if ($cfg->emailAttachments()) { if ($message && $message->getNumAttachments()) { foreach ($message->getAttachments() as $attachment) $attachments[] = $attachment; } if ($response && $response->getNumAttachments()) { foreach ($response->getAttachments() as $attachment) $attachments[] = $attachment; } } if ($vars['signature']=='mine') $signature=$thisstaff->getSignature(); elseif ($vars['signature']=='dept' && $dept && $dept->isPublic()) $signature=$dept->getSignature(); else $signature=''; $msg = $ticket->replaceVars($msg->asArray(), array( 'message' => $message ?: '', 'response' => $response ?: '', 'signature' => $signature, 'recipient' => $ticket->getOwner(), //End user 'staff' => $thisstaff, ) ); $message = $ticket->getLastMessage(); $options = array( 'thread' => $message ?: $ticket->getThread(), ); //ticket created on user's behalf $email->send($recipients, $msg['subj'], $msg['body'], $attachments, $options); } return $ticket; } static function checkOverdue() { $overdue = static::objects() ->filter(array( 'isoverdue' => 0, 'status__state' => 'open', Q::any(array( Q::all(array( 'duedate__isnull' => true, 'est_duedate__isnull' => false, 'est_duedate__lt' => SqlFunction::NOW()) ), Q::all(array( 'duedate__isnull' => false, 'duedate__lt' => SqlFunction::NOW()) ) )) )) ->limit(100); foreach ($overdue as $ticket) $ticket->markOverdue(); } static function agentActions($agent, $options=array()) { if (!$agent) return; require STAFFINC_DIR.'templates/tickets-actions.tmpl.php'; } static function getLink($id) { global $thisstaff; switch (true) { case ($thisstaff instanceof Staff): return ROOT_PATH . sprintf('scp/tickets.php?id=%s', $id); } } static function getPermissions() { return self::$perms; } static function getSources() { static $translated = false; if (!$translated) { foreach (static::$sources as $k=>$v) static::$sources[$k] = __($v); } return static::$sources; } // TODO: Create internal Form for internal fields static function duedateField($name, $default='', $hint='') { return DateTimeField::init(array( 'id' => $name, 'name' => $name, 'default' => $default ?: false, 'label' => __('Due Date'), 'hint' => $hint, 'configuration' => array( 'min' => Misc::gmtime(), 'time' => true, 'gmt' => false, 'future' => true, ) )); } static function registerCustomData(DynamicForm $form) { if (!isset(static::$meta['joins']['cdata+'.$form->id])) { $cdata_class = <<id} extends DynamicForm { static function getInstance() { static \$instance; if (!isset(\$instance)) \$instance = static::lookup({$form->id}); return \$instance; } } class TicketCdataForm{$form->id} extends VerySimpleModel { static \$meta = array( 'view' => true, 'pk' => array('ticket_id'), 'joins' => array( 'ticket' => array( 'constraint' => array('ticket_id' => 'Ticket.ticket_id'), ), ) ); static function getQuery(\$compiler) { return '('.DynamicForm{$form->id}::getCrossTabQuery('T', 'ticket_id').')'; } } EOF; eval($cdata_class); $join = array( 'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'), 'list' => true, ); // This may be necessary if the model has already been inspected if (static::$meta instanceof ModelMeta) static::$meta->addJoin('cdata+'.$form->id, $join); else { static::$meta['joins']['cdata+'.$form->id] = array( 'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'), 'list' => true, ); } } } } RolePermission::register(/* @trans */ 'Tickets', Ticket::getPermissions(), true); class TicketCData extends VerySimpleModel { static $meta = array( 'pk' => array('ticket_id'), 'joins' => array( 'ticket' => array( 'constraint' => array('ticket_id' => 'Ticket.ticket_id'), ), ':priority' => array( 'constraint' => array('priority' => 'Priority.priority_id'), 'null' => true, ), ), ); } TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';