golden hour
/home/doctorbruno/public_html/doctorbruno.info/taeionline/support/include
⬆️ Go Up
Upload
File/Folder
Size
Actions
.MANIFEST
194.97 KB
Del
OK
.htaccess
14 B
Del
OK
JSON.php
33.13 KB
Del
OK
PasswordHash.php
6.92 KB
Del
OK
Spyc.php
31.74 KB
Del
OK
UniversalClassLoader.php
8.61 KB
Del
OK
ajax.admin.php
7.29 KB
Del
OK
ajax.config.php
4.61 KB
Del
OK
ajax.content.php
9.76 KB
Del
OK
ajax.draft.php
13.03 KB
Del
OK
ajax.email.php
2.33 KB
Del
OK
ajax.export.php
1006 B
Del
OK
ajax.filter.php
874 B
Del
OK
ajax.forms.php
13.84 KB
Del
OK
ajax.i18n.php
5.1 KB
Del
OK
ajax.kbase.php
2.93 KB
Del
OK
ajax.note.php
2.05 KB
Del
OK
ajax.orgs.php
11.73 KB
Del
OK
ajax.plugins.php
2.15 KB
Del
OK
ajax.schedule.php
4.29 KB
Del
OK
ajax.search.php
12.72 KB
Del
OK
ajax.sequence.php
3.2 KB
Del
OK
ajax.staff.php
11.62 KB
Del
OK
ajax.tasks.php
33.87 KB
Del
OK
ajax.thread.php
8.81 KB
Del
OK
ajax.tickets.php
79.7 KB
Del
OK
ajax.tips.php
1.66 KB
Del
OK
ajax.upgrader.php
2.24 KB
Del
OK
ajax.users.php
18.33 KB
Del
OK
api.cron.php
1.06 KB
Del
OK
api.tickets.php
10.36 KB
Del
OK
class.2fa.php
7.76 KB
Del
OK
class.ajax.php
1.45 KB
Del
OK
class.api.php
15.31 KB
Del
OK
class.app.php
1.49 KB
Del
OK
class.attachment.php
6.95 KB
Del
OK
class.auth.php
50.73 KB
Del
OK
class.avatar.php
6.44 KB
Del
OK
class.banlist.php
2.58 KB
Del
OK
class.base32.php
4.07 KB
Del
OK
class.businesshours.php
7.08 KB
Del
OK
class.canned.php
9.05 KB
Del
OK
class.captcha.php
1.73 KB
Del
OK
class.category.php
11.14 KB
Del
OK
class.charset.php
3.41 KB
Del
OK
class.cli.php
9.48 KB
Del
OK
class.client.php
15.39 KB
Del
OK
class.collaborator.php
5.65 KB
Del
OK
class.company.php
2.65 KB
Del
OK
class.config.php
60.36 KB
Del
OK
class.controller.php
1.15 KB
Del
OK
class.cron.php
3.6 KB
Del
OK
class.crypto.php
18.92 KB
Del
OK
class.csrf.php
2.38 KB
Del
OK
class.dept.php
33.21 KB
Del
OK
class.dispatcher.php
7.12 KB
Del
OK
class.draft.php
6.23 KB
Del
OK
class.dynamic_forms.php
64.98 KB
Del
OK
class.email.php
49 KB
Del
OK
class.error.php
1.69 KB
Del
OK
class.export.php
32.92 KB
Del
OK
class.faq.php
15.05 KB
Del
OK
class.file.php
34.59 KB
Del
OK
class.filter.php
31.9 KB
Del
OK
class.filter_action.php
23.67 KB
Del
OK
class.format.php
44.3 KB
Del
OK
class.forms.php
195.58 KB
Del
OK
class.http.php
7.16 KB
Del
OK
class.i18n.php
24.28 KB
Del
OK
class.import.php
6.9 KB
Del
OK
class.json.php
2.71 KB
Del
OK
class.knowledgebase.php
5.8 KB
Del
OK
class.list.php
42 KB
Del
OK
class.lock.php
4.05 KB
Del
OK
class.log.php
1.55 KB
Del
OK
class.mail.php
35.76 KB
Del
OK
class.mailer.php
25.75 KB
Del
OK
class.mailfetch.php
9.72 KB
Del
OK
class.mailparse.php
30.99 KB
Del
OK
class.message.php
6.42 KB
Del
OK
class.migrater.php
5.2 KB
Del
OK
class.misc.php
7.5 KB
Del
OK
class.model.php
2.3 KB
Del
OK
class.nav.php
14.15 KB
Del
OK
class.note.php
2.39 KB
Del
OK
class.oauth2.php
4.04 KB
Del
OK
class.organization.php
22.45 KB
Del
OK
class.orm.php
120.9 KB
Del
OK
class.osticket.php
20.64 KB
Del
OK
class.ostsession.php
21.86 KB
Del
OK
class.page.php
10.59 KB
Del
OK
class.pagenate.php
5.53 KB
Del
OK
class.passwd.php
1.21 KB
Del
OK
class.pdf.php
3.83 KB
Del
OK
class.plugin.php
36.13 KB
Del
OK
class.priority.php
1.81 KB
Del
OK
class.queue.php
103.51 KB
Del
OK
class.report.php
11.76 KB
Del
OK
class.role.php
11.27 KB
Del
OK
class.schedule.php
46.28 KB
Del
OK
class.search.php
61.66 KB
Del
OK
class.sequence.php
7.27 KB
Del
OK
class.session.php
19.25 KB
Del
OK
class.setup.php
3.55 KB
Del
OK
class.signal.php
4.16 KB
Del
OK
class.sla.php
9.11 KB
Del
OK
class.staff.php
60.21 KB
Del
OK
class.task.php
56.21 KB
Del
OK
class.team.php
12.31 KB
Del
OK
class.template.php
23.45 KB
Del
OK
class.thread.php
108.96 KB
Del
OK
class.thread_actions.php
17.08 KB
Del
OK
class.ticket.php
167.39 KB
Del
OK
class.timezone.php
21.94 KB
Del
OK
class.topic.php
20.06 KB
Del
OK
class.translation.php
34.79 KB
Del
OK
class.upgrader.php
13.76 KB
Del
OK
class.user.php
43.4 KB
Del
OK
class.usersession.php
7.55 KB
Del
OK
class.util.php
10.07 KB
Del
OK
class.validator.php
12.94 KB
Del
OK
class.variable.php
11.93 KB
Del
OK
class.xml.php
3.23 KB
Del
OK
class.yaml.php
1.15 KB
Del
OK
cli
-
Del
OK
client
-
Del
OK
config
-
Del
OK
fpdf
-
Del
OK
htmLawed.php
53.53 KB
Del
OK
html2text.php
33.71 KB
Del
OK
i18n
-
Del
OK
index.php
37 B
Del
OK
laminas-mail
-
Del
OK
mpdf
-
Del
OK
mysqli.php
9.55 KB
Del
OK
ost-config.php
5.63 KB
Del
OK
ost-sampleconfig.php
6.24 KB
Del
OK
pear
-
Del
OK
plugins
-
Del
OK
staff
-
Del
OK
tnef_decoder.php
19.82 KB
Del
OK
upgrader
-
Del
OK
Edit: class.ticket.php
<?php /********************************************************************* class.ticket.php The most important class! Don't play with fire please. Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket http://www.osticket.com Released under the GNU General Public License WITHOUT ANY WARRANTY. See LICENSE.TXT for details. vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ 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', ), 'searchable' => false, '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(); } static 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() { if ($this->hasFlag(self::FLAG_PARENT)) return true; return false; } static function isParentStatic($flag=false) { if (is_numeric($flag) && ($flag & self::FLAG_PARENT) != 0) 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 = 0; 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 $dept = $this->getDept(); switch (strtolower($options['target'])) { case 'agents': if (!$source && $this->isOpen() && $this->staff) $assignee = sprintf('s%d', $this->staff->getId()); $prompt = __('Select an Agent'); break; case 'teams': 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 (($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')) { $f->configure('dept', $dept); $f->configure('staff', $thisstaff); if ($prompt) $f->configure('prompt', $prompt); if ($options['target']) $f->configure('target', $options['target']); } return $form; } function getReferralForm($source=null, $options=array()) { global $thisstaff; $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::getActiveDepartments()); // Field configurations if ($f=$form->getField('agent')) { $f->configure('dept', $dept); $f->configure('staff', $thisstaff); } if ($f = $form->getField('dept')) $f->configure('hideDisabled', true); 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) { global $thisstaff; if (!$source) $source = array('dept' => array($this->getDeptId()), 'refer' => false); $form = TransferForm::instantiate($source); $form->hideDisabled(); return $form; } 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: // <user type><algo id used>x<pack of uid & tid><hash of the algo> $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'); $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() { global $thisstaff; $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'), 'configuration' => array('staff' => $thisstaff), )), '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(); $count = count($children); if ($children) { foreach ($children as $child) { $child = Ticket::lookup($child[0]); $child->unlinkChild($parent); $count--; } } elseif ($child) $child->unlinkChild($parent); if ($this->isParent() && $count == 0) { $parent->setFlag(Ticket::FLAG_LINKED, false); $parent->setFlag(Ticket::FLAG_PARENT, false); $parent->save(); } return true; } static 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; } static 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('<tr> <td width="8px"> </td> <td> <a class="Icon strtolower(%s) Ticket preview" data-preview="#tickets/%d/preview" href="tickets.php?id=%d">%s</a> </td> <td>%s</td> <td>%s</td> <td>%s</td> <td>%s</td> </tr>', 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->getThread()->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(?array $info=array(), &$errors) { if (isset($info['sid']) && isset($info['tid'])) return $this->unassign(); elseif (isset($info['sid']) && $this->setStaffId(0)) return true; elseif (isset($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) { $v = array(); $c = $ticket->getThread()->addCollaborator($user, $v, $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; } //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('<hr>%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 static 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); if ($postCreate && $filterMatches = $ticket_filter->getMatchingFilterList()) { $username = __('Ticket Filter'); foreach ($filterMatches as $f) { $actions = $f->getActions(); foreach ($actions as $key => $value) { $filterName = $f->getName(); if (!$coreClass = $value->lookupByType($value->type)) continue; if ($description = $coreClass->getEventDescription($value, $filterName)) $postCreate->logEvent($description['type'], $description['desc'], $username); } if ($f->stopOnMatch()) break; } } } 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 (is_numeric($vars['topicId']) && ($topic=Topic::lookup((int) $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(); $dept = Dept::lookup($deptId); if ($dept && !$dept->isActive()) $deptId = $cfg->getDefaultDeptId(); $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'] && in_array(strtolower($origin), ['staff', 'api'])) $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 ------------------------ */ // 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); $vars['ticket'] = $ticket; self::filterTicketData($origin, $vars, array_merge(array($form), $topic_forms), $user, $ticket); // 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('<a href="%s.php?id=%d"><b>#%s</b></a>', $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('<a href="tickets.php?id=%d"><b>#%s</b></a>', $ticket->getId(), $ticket->getNumber()); $entryLink = sprintf('<a href="#entry-%d"><b>%s</b></a>', $_SESSION[':form-data']['eid'], Format::datetime($_SESSION[':form-data']['timestamp'])); $ticketNote = array( 'title' => __('Ticket Created From Thread Entry'), 'body' => sprintf(__('Ticket %1$s<br/> Thread Entry: %2$s'), $ticketLink, $entryLink) ); $taskNote = array( 'title' => __('Ticket Created From Thread Entry'), 'note' => sprintf(__('Ticket %1$s<br/> 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 = <<<EOF class DynamicForm{$form->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';
Save