Helpdesk da PluGzOne, baseado no osTicket
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

4832 lines
166 KiB

<?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',
),
'null' => true,
),
'cdata' => array(
'reverse' => 'TicketCData.ticket',
'list' => false,
),
'entries' => array(
'constraint' => array(
"'T'" => 'DynamicFormEntry.object_type',
'ticket_id' => 'DynamicFormEntry.object_id',
),
'list' => true,
),
)
);
const PERM_CREATE = 'ticket.create';
const PERM_EDIT = 'ticket.edit';
const PERM_ASSIGN = 'ticket.assign';
const PERM_RELEASE = 'ticket.release';
const PERM_TRANSFER = 'ticket.transfer';
const PERM_REFER = 'ticket.refer';
const PERM_MERGE = 'ticket.merge';
const PERM_LINK = 'ticket.link';
const PERM_REPLY = 'ticket.reply';
const PERM_MARKANSWERED = 'ticket.markanswered';
const PERM_CLOSE = 'ticket.close';
const PERM_DELETE = 'ticket.delete';
const FLAG_COMBINE_THREADS = 0x0001;
const FLAG_SEPARATE_THREADS = 0x0002;
const FLAG_LINKED = 0x0008;
const FLAG_PARENT = 0x0010;
static protected $perms = array(
self::PERM_CREATE => array(
'title' =>
/* @trans */ 'Create',
'desc' =>
/* @trans */ 'Ability to open tickets on behalf of users'),
self::PERM_EDIT => array(
'title' =>
/* @trans */ 'Edit',
'desc' =>
/* @trans */ 'Ability to edit tickets'),
self::PERM_ASSIGN => array(
'title' =>
/* @trans */ 'Assign',
'desc' =>
/* @trans */ 'Ability to assign tickets to agents or teams'),
self::PERM_RELEASE => array(
'title' =>
/* @trans */ 'Release',
'desc' =>
/* @trans */ 'Ability to release ticket assignment'),
self::PERM_TRANSFER => array(
'title' =>
/* @trans */ 'Transfer',
'desc' =>
/* @trans */ 'Ability to transfer tickets between departments'),
self::PERM_REFER => array(
'title' =>
/* @trans */ 'Refer',
'desc' =>
/* @trans */ 'Ability to manage ticket referrals'),
self::PERM_MERGE => array(
'title' =>
/* @trans */ 'Merge',
'desc' =>
/* @trans */ 'Ability to merge tickets'),
self::PERM_LINK => array(
'title' =>
/* @trans */ 'Link',
'desc' =>
/* @trans */ 'Ability to link tickets'),
self::PERM_REPLY => array(
'title' =>
/* @trans */ 'Post Reply',
'desc' =>
/* @trans */ 'Ability to post a ticket reply'),
self::PERM_MARKANSWERED => array(
'title' =>
/* @trans */ 'Mark as Answered',
'desc' =>
/* @trans */ 'Ability to mark a ticket as Answered/Unanswered'),
self::PERM_CLOSE => array(
'title' =>
/* @trans */ 'Close',
'desc' =>
/* @trans */ 'Ability to close tickets'),
self::PERM_DELETE => array(
'title' =>
/* @trans */ 'Delete',
'desc' =>
/* @trans */ 'Ability to delete tickets'),
);
// Ticket Sources
static protected $sources = array(
'Phone' =>
/* @trans */ 'Phone',
'Email' =>
/* @trans */ 'Email',
'Web' =>
/* @trans */ 'Web',
'API' =>
/* @trans */ 'API',
'Other' =>
/* @trans */ 'Other',
);
var $lastMsgId;
var $last_message;
var $owner; // TicketOwner
var $_user; // EndUser
var $_answers;
var $collaborators;
var $active_collaborators;
var $recipients;
var $lastrespondent;
var $lastuserrespondent;
var $_children;
function loadDynamicData($force=false) {
if (!isset($this->_answers) || $force) {
$this->_answers = array();
foreach (DynamicFormEntryAnswer::objects()
->filter(array(
'entry__object_id' => $this->getId(),
'entry__object_type' => 'T'
)) as $answer
) {
$tag = mb_strtolower($answer->field->name)
?: 'field.' . $answer->field->id;
$this->_answers[$tag] = $answer;
}
}
return $this->_answers;
}
function getAnswer($field, $form=null) {
// TODO: Prefer CDATA ORM relationship if already loaded
$this->loadDynamicData();
return $this->_answers[$field];
}
function getId() {
return $this->ticket_id;
}
function getPid() {
return $this->ticket_pid;
}
function getChildren() {
if (!isset($this->_children) && $this->isParent())
$this->_children = self::getChildTickets($this->getId());
return $this->_children ?: array();
}
function getMergeTypeByFlag($flag) {
if (($flag & self::FLAG_COMBINE_THREADS) != 0)
return 'combine';
if (($flag & self::FLAG_SEPARATE_THREADS) != 0)
return 'separate';
else
return 'visual';
return 'visual';
}
function getMergeType() {
if ($this->hasFlag(self::FLAG_COMBINE_THREADS))
return 'combine';
if ($this->hasFlag(self::FLAG_SEPARATE_THREADS))
return 'separate';
else
return 'visual';
return 'visual';
}
function isMerged() {
if (!is_null($this->getPid()) || $this->isParent())
return true;
return false;
}
function isParent($flag=false) {
if (is_numeric($flag) && ($flag & self::FLAG_PARENT) != 0)
return true;
elseif (!is_numeric($flag) && $this->hasFlag(self::FLAG_PARENT))
return true;
return false;
}
function hasFlag($flag) {
return ($this->get('flags', 0) & $flag) != 0;
}
function isChild($pid=false) {
return ($this->getPid() ? true : false);
}
function hasState($state) {
return strcasecmp($this->getState(), $state) == 0;
}
function isOpen() {
return $this->hasState('open');
}
function isReopened() {
return null !== $this->getReopenDate();
}
function isReopenable() {
return ($this->getStatus()->isReopenable() && $this->getDept()->allowsReopen()
&& ($this->getTopic() ? $this->getTopic()->allowsReopen() : true));
}
function isClosed() {
return $this->hasState('closed');
}
function isCloseable() {
global $cfg;
if ($this->isClosed())
return true;
$warning = null;
if (self::getMissingRequiredFields($this)) {
$warning = sprintf(
__( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
__('This ticket'),
'', '');
} elseif (($num=$this->getNumOpenTasks())) {
$warning = sprintf(__('%1$s has %2$d open tasks and cannot be closed'),
__('This ticket'), $num);
} elseif ($cfg->requireTopicToClose() && !$this->getTopicId()) {
$warning = sprintf(
__( '%1$s is missing a %2$s and cannot be closed'),
__('This ticket'), __('Help Topic'), '');
}
return $warning ?: true;
}
function isArchived() {
return $this->hasState('archived');
}
function isDeleted() {
return $this->hasState('deleted');
}
function isAssigned($to=null) {
if (!$this->isOpen())
return false;
if (is_null($to))
return ($this->getStaffId() || $this->getTeamId());
switch (true) {
case $to instanceof Staff:
return ($to->getId() == $this->getStaffId() ||
$to->isTeamMember($this->getTeamId()));
break;
case $to instanceof Team:
return ($to->getId() == $this->getTeamId());
break;
}
return false;
}
function isOverdue() {
return $this->ht['isoverdue'];
}
function isAnswered() {
return $this->ht['isanswered'];
}
function isLocked() {
return null !== $this->getLock();
}
function getRole($staff) {
if (!$staff instanceof Staff)
return null;
return $staff->getRole($this->getDept(), $this->isAssigned($staff));
}
function checkStaffPerm($staff, $perm=null) {
// Must be a valid staff
if ((!$staff instanceof Staff) && !($staff=Staff::lookup($staff)))
return false;
// check department access first
if (!$staff->canAccessDept($this->getDept())
// check assignment
&& !$this->isAssigned($staff)
// check referral
&& !$this->getThread()->isReferred($staff))
return false;
// At this point staff has view access unless a specific permission is
// requested
if ($perm === null)
return true;
// Permission check requested -- get role if any
if (!($role=$this->getRole($staff)))
return false;
// Check permission based on the effective role
return $role->hasPerm($perm);
}
function checkUserAccess($user) {
if (!$user || !($user instanceof EndUser))
return false;
// Ticket Owner
if ($user->getId() == $this->getUserId())
return true;
// Organization
if ($user->canSeeOrgTickets()
&& ($U = $this->getUser())
&& ($U->getOrgId() == $user->getOrgId())
) {
// The owner of this ticket is in the same organization as the
// user in question, and the organization is configured to allow
// the user in question to see other tickets in the
// organization.
return true;
}
// Collaborator?
// 1) If the user was authorized via this ticket.
if ($user->getTicketId() == $this->getId()
&& !strcasecmp($user->getUserType(), 'collaborator')
) {
return true;
}
// 2) Query the database to check for expanded access...
if (Collaborator::lookup(array(
'user_id' => $user->getId(),
'thread_id' => $this->getThreadId()))
) {
return true;
}
// 3) If the ticket is a child of a merge
if ($this->isParent() && $this->getMergeType() != 'visual') {
$children = Ticket::objects()
->filter(array('ticket_pid'=>$this->getId()))
->order_by('sort');
foreach ($children as $child)
if ($child->checkUserAccess($user))
return true;
}
return false;
}
// Getters
function getNumber() {
return $this->number;
}
function getOwnerId() {
return $this->user_id;
}
function getOwner() {
if (!isset($this->owner)) {
$this->owner = new TicketOwner(new EndUser($this->user), $this);
}
return $this->owner;
}
function getEmail() {
if ($o = $this->getOwner()) {
return $o->getEmail();
}
return null;
}
function getReplyToEmail() {
//TODO: Determine the email to use (once we enable multi-email support)
return $this->getEmail();
}
// Deprecated
function getOldAuthToken() {
# XXX: Support variable email address (for CCs)
return md5($this->getId() . strtolower($this->getEmail()) . SECRET_SALT);
}
function getName(){
if ($o = $this->getOwner()) {
return $o->getName();
}
return null;
}
function getSubject() {
return (string) $this->getAnswer('subject');
}
/* Help topic title - NOT object -> $topic */
function getHelpTopic() {
if ($this->topic)
return $this->topic->getFullName();
}
function getCreateDate() {
return $this->created;
}
function getOpenDate() {
return $this->getCreateDate();
}
function getReopenDate() {
return $this->reopened;
}
function getUpdateDate() {
return $this->updated;
}
function getEffectiveDate() {
return $this->lastupdate;
}
function getDueDate() {
return $this->duedate;
}
function getSLADueDate($recompute=false) {
global $cfg;
if (!$recompute && $this->est_duedate)
return $this->est_duedate;
if (($sla = $this->getSLA()) && $sla->isActive()) {
$schedule = $this->getDept()->getSchedule();
$tz = new DateTimeZone($cfg->getDbTimezone());
$dt = new DateTime($this->getReopenDate() ?:
$this->getCreateDate(), $tz);
$dt = $sla->addGracePeriod($dt, $schedule);
// Make sure time is in DB timezone
$dt->setTimezone($tz);
return $dt->format('Y-m-d H:i:s');
}
}
function updateEstDueDate($clearOverdue=true) {
if ($this->isOverdue() && $clearOverdue)
$this->clearOverdue(false);
$this->est_duedate = $this->getSLADueDate(true) ?: null;
return $this->save();
}
function getEstDueDate() {
// Real due date or sla due date (If ANY)
return $this->getDueDate() ?: $this->getSLADueDate();
}
function getCloseDate() {
return $this->closed;
}
function getStatusId() {
return $this->status_id;
}
/**
* setStatusId
*
* Forceably set the ticket status ID to the received status ID. No
* checks are made. Use ::setStatus() to change the ticket status
*/
// XXX: Use ::setStatus to change the status. This can be used as a
// fallback if the logic in ::setStatus fails.
function setStatusId($id) {
$this->status_id = $id;
return $this->save();
}
function getStatus() {
return $this->status;
}
function getState() {
if (!$this->getStatus()) {
return '';
}
return $this->getStatus()->getState();
}
function getDeptId() {
return $this->dept_id;
}
function getDeptName() {
if ($this->dept instanceof Dept)
return $this->dept->getFullName();
}
function getPriorityId() {
global $cfg;
if (($priority = $this->getPriority()))
return $priority->getId();
return $cfg->getDefaultPriorityId();
}
function getPriority() {
if (($a = $this->getAnswer('priority')))
return $a->getValue();
return null;
}
function getPriorityField() {
if (($a = $this->getAnswer('priority')))
return $a->getField();
return TicketForm::getInstance()->getField('priority');
}
function getPhoneNumber() {
return (string)$this->getOwner()->getPhoneNumber();
}
function getSource() {
$sources = $this->getSources();
return $sources[$this->source] ?: $this->source;
}
function getIP() {
return $this->ip_address;
}
function getHashtable() {
return $this->ht;
}
function getUpdateInfo() {
global $cfg;
return array(
'source' => $this->source,
'topicId' => $this->getTopicId(),
'slaId' => $this->getSLAId(),
'user_id' => $this->getOwnerId(),
'duedate' => Misc::db2gmtime($this->getDueDate()),
);
}
function getLock() {
$lock = $this->lock;
if ($lock && !$lock->isExpired())
return $lock;
}
function acquireLock($staffId, $lockTime=null) {
global $cfg;
if (!isset($lockTime))
$lockTime = $cfg->getLockTime();
if (!$staffId or !$lockTime) //Lockig disabled?
return null;
// Check if the ticket is already locked.
if (($lock = $this->getLock()) && !$lock->isExpired()) {
if ($lock->getStaffId() != $staffId) //someone else locked the ticket.
return null;
//Lock already exits...renew it
$lock->renew($lockTime); //New clock baby.
return $lock;
}
// No lock on the ticket or it is expired
$this->lock = Lock::acquire($staffId, $lockTime); //Create a new lock..
if ($this->lock) {
$this->save();
}
// load and return the newly created lock if any!
return $this->lock;
}
function releaseLock($staffId=false) {
if (!($lock = $this->getLock()))
return false;
if ($staffId && $lock->staff_id != $staffId)
return false;
if (!$lock->delete())
return false;
$this->lock = null;
return $this->save();
}
function getDept() {
global $cfg;
return $this->dept ?: $cfg->getDefaultDept();
}
function getUserId() {
return $this->getOwnerId();
}
function getUser() {
if (!isset($this->_user) && $this->user) {
$this->_user = new EndUser($this->user);
}
return $this->_user;
}
function getStaffId() {
return $this->staff_id;
}
function getStaff() {
return $this->staff;
}
function getTeamId() {
return $this->team_id;
}
function getTeam() {
return $this->team;
}
function getAssigneeId() {
if (!($assignee=$this->getAssignee()))
return null;
$id = '';
if ($assignee instanceof Staff)
$id = 's'.$assignee->getId();
elseif ($assignee instanceof Team)
$id = 't'.$assignee->getId();
return $id;
}
function getAssignee() {
if (!$this->isOpen() || !$this->isAssigned())
return false;
if ($this->staff)
return $this->staff;
if ($this->team)
return $this->team;
return null;
}
function getAssignees() {
$assignees = array();
if ($staff = $this->getStaff())
$assignees[] = $staff->getName();
if ($team = $this->getTeam())
$assignees[] = $team->getName();
return $assignees;
}
function getAssigned($glue='/') {
$assignees = $this->getAssignees();
return $assignees ? implode($glue, $assignees) : '';
}
function getTopicId() {
return $this->topic_id;
}
function getTopic() {
return $this->topic;
}
function getSLAId() {
return $this->sla_id;
}
function getSLA() {
return $this->sla;
}
function getLastRespondent() {
if (!isset($this->lastrespondent)) {
if (!$this->getThread() || !$this->getThread()->entries)
return $this->lastrespondent = false;
$this->lastrespondent = Staff::objects()
->filter(array(
'staff_id' => $this->getThread()->entries
->filter(array(
'type' => 'R',
'staff_id__gt' => 0,
))
->values_flat('staff_id')
->order_by('-id')
->limit('1,1')
))
->first()
?: false;
}
return $this->lastrespondent;
}
function getLastUserRespondent() {
if (!isset($this->$lastuserrespondent)) {
if (!$this->getThread() || !$this->getThread()->entries)
return $this->$lastuserrespondent = false;
$this->$lastuserrespondent = User::objects()
->filter(array(
'id' => $this->getThread()->entries
->filter(array(
'user_id__gt' => 0,
))
->values_flat('user_id')
->order_by('-id')
->limit(1)
))
->first()
?: false;
}
return $this->$lastuserrespondent;
}
function getLastMessageDate() {
return $this->getThread()->lastmessage;
}
function getLastMsgDate() {
return $this->getLastMessageDate();
}
function getLastResponseDate() {
return $this->getThread()->lastresponse;
}
function getLastRespDate() {
return $this->getLastResponseDate();
}
function getLastMsgId() {
return $this->lastMsgId;
}
function getLastMessage() {
if (!isset($this->last_message)) {
if ($this->getLastMsgId())
$this->last_message = MessageThreadEntry::lookup(
$this->getLastMsgId(), $this->getThreadId());
if (!$this->last_message)
$this->last_message = $this->getThread() ? $this->getThread()->getLastMessage() : '';
}
return $this->last_message;
}
function getNumTasks() {
// FIXME: Implement this after merging Tasks
return count($this->tasks);
}
function getNumOpenTasks() {
return count($this->tasks->filter(array(
'flags__hasbit' => TaskModel::ISOPEN)));
}
function getThreadId() {
if ($this->getThread())
return $this->getThread()->getId();
}
function getThread() {
if (is_null($this->thread) && $this->child_thread)
return $this->child_thread;
return $this->thread;
}
function getThreadCount() {
return $this->getClientThread()->count();
}
function getNumMessages() {
return $this->getThread()->getNumMessages();
}
function getNumResponses() {
return $this->getThread()->getNumResponses();
}
function getNumNotes() {
return $this->getThread()->getNumNotes();
}
function getMessages() {
return $this->getThreadEntries(array('M'));
}
function getResponses() {
return $this->getThreadEntries(array('R'));
}
function getNotes() {
return $this->getThreadEntries(array('N'));
}
function getClientThread() {
return $this->getThreadEntries(array('M', 'R'));
}
function getThreadEntry($id) {
return $this->getThread()->getEntry($id);
}
function getThreadEntries($type=false) {
if ($this->getThread()) {
$entries = $this->getThread()->getEntries();
if ($type && is_array($type))
$entries->filter(array('type__in' => $type));
}
return $entries;
}
// MailingList of participants (owner + collaborators)
function getRecipients($who='all', $whitelist=array(), $active=true) {
$list = new MailingList();
switch (strtolower($who)) {
case 'user':
$list->addTo($this->getOwner());
break;
case 'all':
$list->addTo($this->getOwner());
// Fall-trough
case 'collabs':
if (($collabs = $active ? $this->getActiveCollaborators() :
$this->getCollaborators())) {
foreach ($collabs as $c)
if (!$whitelist || in_array($c->getUserId(),
$whitelist))
$list->addCc($c);
}
break;
default:
return null;
}
return $list;
}
function getCollaborators() {
return $this->getThread() ? $this->getThread()->getCollaborators() : '';
}
function getNumCollaborators() {
return $this->getThread() ? $this->getThread()->getNumCollaborators() : '';
}
function getActiveCollaborators() {
return $this->getThread() ? $this->getThread()->getActiveCollaborators() : '';
}
function getNumActiveCollaborators() {
return $this->getThread() ? $this->getThread()->getNumActiveCollaborators() : '';
}
function getAssignmentForm($source=null, $options=array()) {
global $thisstaff;
$prompt = $assignee = '';
// Possible assignees
$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');
$type = array('type' => 'closed');
Signal::send('object.edited', $t, $type);
$t->deleteDrafts();
};
break;
case 'open':
if ($this->isClosed() && $this->isReopenable()) {
// Auto-assign to closing staff or the last respondent if the
// agent is available and has access. Otherwise, put the ticket back
// to unassigned pool.
$dept = $this->getDept();
$staff = $this->getStaff() ?: $this->getLastRespondent();
$autoassign = (!$dept->disableReopenAutoAssign());
if ($autoassign
&& $staff
// Is agent on vacation ?
&& $staff->isAvailable()
// Does the agent have access to dept?
&& $staff->canAccessDept($dept))
$this->setStaffId($staff->getId());
else
$this->setStaffId(0); // Clear assignment
}
if ($this->isClosed()) {
$this->closed = null;
$this->lastupdate = $this->reopened = SqlFunction::NOW();
$ecb = function ($t) {
$t->logEvent('reopened', false, null, 'closed');
// Set new sla duedate if any
$t->updateEstDueDate();
};
}
// If the ticket is not open then clear answered flag
if (!$this->isOpen())
$this->isanswered = 0;
break;
default:
return false;
}
$this->status = $status;
if (!$this->save(true))
return false;
// Refer thread to previously assigned or closing agent
if ($refer && $cfg->autoReferTicketsOnClose())
$this->getThread()->refer($refer);
// Log status change b4 reload — if currently has a status. (On new
// ticket, the ticket is opened and thereafter the status is set to
// the requested status).
if ($hadStatus) {
$alert = false;
if ($comments = ThreadEntryBody::clean($comments)) {
// Send out alerts if comments are included
$alert = true;
$this->logNote(__('Status Changed'), $comments, $thisstaff, $alert);
}
}
// Log events via callback
if ($ecb)
$ecb($this);
elseif ($hadStatus)
// Don't log the initial status change
$this->logEvent('edited', array('status' => $status->getId()));
return true;
}
function setState($state, $alerts=false) {
switch (strtolower($state)) {
case 'open':
return $this->setStatus('open');
case 'closed':
return $this->setStatus('closed');
case 'answered':
return $this->setAnsweredState(1);
case 'unanswered':
return $this->setAnsweredState(0);
case 'overdue':
return $this->markOverdue();
case 'notdue':
return $this->clearOverdue();
case 'unassined':
return $this->unassign();
}
// FIXME: Throw and excception and add test cases
return false;
}
function setAnsweredState($isanswered) {
$this->isanswered = $isanswered;
return $this->save();
}
function reopen() {
global $cfg;
if (!$this->isClosed())
return false;
// Set status to open based on current closed status settings
// If the closed status doesn't have configured "reopen" status then use the
// the default ticket status.
if (!($status=$this->getStatus()->getReopenStatus()))
$status = $cfg->getDefaultTicketStatusId();
return $status ? $this->setStatus($status) : false;
}
function onNewTicket($message, $autorespond=true, $alertstaff=true) {
global $cfg;
//Log stuff here...
if (!$autorespond && !$alertstaff)
return true; //No alerts to send.
/* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/
if(!$cfg
|| !($dept=$this->getDept())
|| !($tpl = $dept->getTemplate())
|| !($email=$dept->getAutoRespEmail())
) {
return false; //bail out...missing stuff.
}
$options = array();
if (($message instanceof ThreadEntry)
&& $message->getEmailMessageId()) {
$options += array(
'inreplyto'=>$message->getEmailMessageId(),
'references'=>$message->getEmailReferences(),
'thread'=>$message
);
}
else {
$options += array(
'thread' => $this->getThread(),
);
}
//Send auto response - if enabled.
if ($autorespond
&& $cfg->autoRespONNewTicket()
&& $dept->autoRespONNewTicket()
&& ($msg = $tpl->getAutoRespMsgTemplate())
) {
$msg = $this->replaceVars(
$msg->asArray(),
array('message' => $message,
'recipient' => $this->getOwner(),
'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''
)
);
$email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'],
null, $options);
}
// Send alert to out sleepy & idle staff.
if ($alertstaff
&& $cfg->alertONNewTicket()
&& ($email=$dept->getAlertEmail())
&& ($msg=$tpl->getNewTicketAlertMsgTemplate())
) {
$msg = $this->replaceVars($msg->asArray(), array('message' => $message));
$recipients = $sentlist = array();
// Exclude the auto responding email just incase it's from staff member.
if ($message instanceof ThreadEntry && $message->isAutoReply())
$sentlist[] = $this->getEmail();
if ($dept->getNumMembersForAlerts()) {
// Only alerts dept members if the ticket is NOT assigned.
$manager = $dept->getManager();
if ($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()
&& ($members = $dept->getMembersForAlerts())
) {
foreach ($members as $M)
if ($M != $manager)
$recipients[] = $M;
}
if ($cfg->alertDeptManagerONNewTicket() && $manager) {
$recipients[] = $manager;
}
// Account manager
if ($cfg->alertAcctManagerONNewTicket()
&& ($org = $this->getOwner()->getOrganization())
&& ($acct_manager = $org->getAccountManager())
) {
if ($acct_manager instanceof Team)
$recipients = array_merge($recipients, $acct_manager->getMembersForAlerts());
else
$recipients[] = $acct_manager;
}
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
|| !$staff->isAvailable()
|| in_array($staff->getEmail(), $sentlist)
) {
continue;
}
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
// Alert admin ONLY if not already a staff??
if ($cfg->alertAdminONNewTicket()
&& !in_array($cfg->getAdminEmail(), $sentlist)
&& ($dept->isGroupMembershipEnabled() != Dept::ALERTS_DISABLED)) {
$options += array('utype'=>'A');
$alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
$email->sendAlert($cfg->getAdminEmail(), $alert['subj'],
$alert['body'], null, $options);
}
}
return true;
}
function onOpenLimit($sendNotice=true) {
global $ost, $cfg;
//Log the limit notice as a warning for admin.
$msg=sprintf(_S('Maximum open tickets (%1$d) reached for %2$s'),
$cfg->getMaxOpenTickets(), $this->getEmail());
$ost->logWarning(sprintf(_S('Maximum Open Tickets Limit (%s)'),$this->getEmail()),
$msg);
if (!$sendNotice || !$cfg->sendOverLimitNotice())
return true;
//Send notice to user.
if (($dept = $this->getDept())
&& ($tpl=$dept->getTemplate())
&& ($msg=$tpl->getOverlimitMsgTemplate())
&& ($email=$dept->getAutoRespEmail())
) {
$msg = $this->replaceVars(
$msg->asArray(),
array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
);
$email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']);
}
$user = $this->getOwner();
// Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
$alert=sprintf(__('Maximum open tickets reached for %s.'), $this->getEmail())."\n"
.sprintf(__('Open tickets: %d'), $user->getNumOpenTickets())."\n"
.sprintf(__('Max allowed: %d'), $cfg->getMaxOpenTickets())
."\n\n".__("Notice sent to the user.");
$ost->alertAdmin(__('Overlimit Notice'), $alert);
return true;
}
function onResponse($response, $options=array()) {
$this->isanswered = 1;
$this->save();
$vars = array_merge($options,
array(
'activity' => _S('New Response'),
'threadentry' => $response
)
);
$this->onActivity($vars);
}
/*
* Notify collaborators on response or new message
*
*/
function notifyCollaborators($entry, $vars = array()) {
global $cfg;
if (!$entry instanceof ThreadEntry
|| !($recipients=$this->getRecipients())
|| !($dept=$this->getDept())
|| !($tpl=$dept->getTemplate())
|| !($msg=$tpl->getActivityNoticeMsgTemplate())
|| !($email=$dept->getEmail())
) {
return;
}
$poster = User::lookup($entry->user_id);
$posterEmail = $poster->getEmail()->address;
$vars = array_merge($vars, array(
'message' => (string) $entry,
'poster' => $poster ?: _S('A collaborator'),
)
);
$msg = $this->replaceVars($msg->asArray(), $vars);
$attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
$options = array('thread' => $entry);
if ($vars['from_name'])
$options += array('from_name' => $vars['from_name']);
$skip = array();
if ($entry instanceof MessageThreadEntry) {
foreach ($entry->getAllEmailRecipients() as $R) {
$skip[] = $R->mailbox.'@'.$R->host;
}
}
foreach ($recipients as $key => $recipient) {
$recipient = $recipient->getContact();
if(get_class($recipient) == 'TicketOwner')
$owner = $recipient;
if ((get_class($recipient) == 'Collaborator' ? $recipient->getUserId() : $recipient->getId()) == $entry->user_id)
unset($recipients[$key]);
}
if (!count($recipients))
return true;
//see if the ticket user is a recipient
if ($owner->getEmail()->address != $poster->getEmail()->address && !in_array($owner->getEmail()->address, $skip))
$owner_recip = $owner->getEmail()->address;
//say dear collaborator if the ticket user is not a recipient
if (!$owner_recip) {
$nameFormats = array_keys(PersonsName::allFormats());
$names = array();
foreach ($nameFormats as $key => $value) {
$names['recipient.name.' . $value] = __('Collaborator');
}
$names = array_merge($names, array('recipient' => $recipient));
$cnotice = $this->replaceVars($msg, $names);
}
//otherwise address email to ticket user
else
$cnotice = $this->replaceVars($msg, array('recipient' => $owner));
$email->send($recipients, $cnotice['subj'], $cnotice['body'], $attachments,
$options);
}
function onMessage($message, $autorespond=true, $reopen=true) {
global $cfg;
$this->isanswered = 0;
$this->lastupdate = SqlFunction::NOW();
$this->save();
// Reopen if closed AND reopenable
// We're also checking autorespond flag because we don't want to
// reopen closed tickets on auto-reply from end user. This is not to
// confused with autorespond on new message setting
if ($reopen && $this->isClosed() && $this->isReopenable())
$this->reopen();
if (!$autorespond)
return;
// Figure out the user
if ($this->getOwnerId() == $message->getUserId())
$user = new TicketOwner(
User::lookup($message->getUserId()), $this);
else
$user = Collaborator::lookup(array(
'user_id' => $message->getUserId(),
'thread_id' => $this->getThreadId()));
/********** double check auto-response ************/
if (!$user)
$autorespond = false;
elseif ((Email::getIdByEmail($user->getEmail())))
$autorespond = false;
elseif (($dept=$this->getDept()))
$autorespond = $dept->autoRespONNewMessage();
if (!$autorespond
|| !$cfg->autoRespONNewMessage()
|| !$message
) {
return; //no autoresp or alerts.
}
$dept = $this->getDept();
$email = $dept->getAutoRespEmail();
// If enabled...send confirmation to user. ( New Message AutoResponse)
if ($email
&& ($tpl=$dept->getTemplate())
&& ($msg=$tpl->getNewMessageAutorepMsgTemplate())
) {
$msg = $this->replaceVars($msg->asArray(),
array(
'recipient' => $user,
'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''
)
);
$options = array('thread' => $message);
if ($message->getEmailMessageId()) {
$options += array(
'inreplyto' => $message->getEmailMessageId(),
'references' => $message->getEmailReferences()
);
}
$email->sendAutoReply($user, $msg['subj'], $msg['body'],
null, $options);
}
}
function onActivity($vars, $alert=true) {
global $cfg, $thisstaff;
//TODO: do some shit
if (!$alert // Check if alert is enabled
|| !$cfg->alertONNewActivity()
|| !($dept=$this->getDept())
|| !$dept->getNumMembersForAlerts()
|| !($email=$cfg->getAlertEmail())
|| !($tpl = $dept->getTemplate())
|| !($msg=$tpl->getNoteAlertMsgTemplate())
) {
return;
}
// Alert recipients
$recipients = array();
//Last respondent.
if ($cfg->alertLastRespondentONNewActivity())
$recipients[] = $this->getLastRespondent();
// Assigned staff / team
if ($cfg->alertAssignedONNewActivity()) {
if (isset($vars['assignee'])
&& $vars['assignee'] instanceof Staff)
$recipients[] = $vars['assignee'];
elseif ($this->isOpen() && ($assignee = $this->getStaff()))
$recipients[] = $assignee;
if ($team = $this->getTeam())
$recipients = array_merge($recipients, $team->getMembersForAlerts());
}
// Dept manager
if ($cfg->alertDeptManagerONNewActivity() && $dept && $dept->getManagerId())
$recipients[] = $dept->getManager();
$options = array();
$staffId = $thisstaff ? $thisstaff->getId() : 0;
if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) {
$options = array('thread' => $vars['threadentry']);
// Activity details
if (!$vars['comments'])
$vars['comments'] = $vars['threadentry'];
// Staff doing the activity
$staffId = $vars['threadentry']->getStaffId() ?: $staffId;
}
$msg = $this->replaceVars($msg->asArray(),
array(
'note' => $vars['threadentry'], // For compatibility
'activity' => $vars['activity'],
'comments' => $vars['comments']));
$isClosed = $this->isClosed();
$sentlist=array();
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
// Don't bother vacationing staff.
|| !$staff->isAvailable()
// No need to alert the poster!
|| $staffId == $staff->getId()
// No duplicates.
|| isset($sentlist[$staff->getEmail()])
// Make sure staff has access to ticket
|| ($isClosed && !$this->checkStaffPerm($staff))
) {
continue;
}
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
$sentlist[$staff->getEmail()] = 1;
}
}
function onAssign($assignee, $comments, $alert=true) {
global $cfg, $thisstaff;
if ($this->isClosed())
$this->reopen(); //Assigned tickets must be open - otherwise why assign?
// Assignee must be an object of type Staff or Team
if (!$assignee || !is_object($assignee))
return false;
$user_comments = (bool) $comments;
$assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');
//Log an internal note - no alerts on the internal note.
if ($user_comments) {
if ($assignee instanceof Staff
&& $thisstaff
// self assignment
&& $assignee->getId() == $thisstaff->getId())
$title = sprintf(_S('Ticket claimed by %s'),
$thisstaff->getName());
else
$title = sprintf(_S('Ticket Assigned to %s'),
$assignee->getName());
$note = $this->logNote($title, $comments, $assigner, false);
}
$dept = $this->getDept();
// See if we need to send alerts
if (!$alert || !$cfg->alertONAssignment() || !$dept->getNumMembersForAlerts())
return true; //No alerts!
if (!$dept
|| !($tpl = $dept->getTemplate())
|| !($email = $dept->getAlertEmail())
) {
return true;
}
// Recipients
$recipients = array();
if ($assignee instanceof Staff) {
if ($cfg->alertStaffONAssignment())
$recipients[] = $assignee;
} elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) {
if ($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembersForAlerts()))
$recipients = array_merge($recipients, $members);
elseif ($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead()))
$recipients[] = $lead;
}
// Get the message template
if ($recipients
&& ($msg=$tpl->getAssignedAlertMsgTemplate())
) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments,
'assignee' => $assignee,
'assigner' => $assigner
)
);
// Send the alerts.
$sentlist = array();
$options = $note instanceof ThreadEntry
? array('thread'=>$note)
: array();
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
|| !$staff->isAvailable()
|| in_array($staff->getEmail(), $sentlist)
) {
continue;
}
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
function onOverdue($whine=true, $comments="") {
global $cfg;
if ($whine && ($sla = $this->getSLA()) && !$sla->alertOnOverdue())
$whine = false;
// Check if we need to send alerts.
if (!$whine
|| !$cfg->alertONOverdueTicket()
|| !($dept = $this->getDept())
|| !$dept->getNumMembersForAlerts()
) {
return true;
}
// Get the message template
if (($tpl = $dept->getTemplate())
&& ($msg=$tpl->getOverdueAlertMsgTemplate())
&& ($email = $dept->getAlertEmail())
) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments)
);
// Recipients
$recipients = array();
// Assigned staff or team... if any
if ($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
if ($this->getStaffId()) {
$recipients[]=$this->getStaff();
}
elseif ($this->getTeamId()
&& ($team = $this->getTeam())
&& ($members = $team->getMembersForAlerts())
) {
$recipients=array_merge($recipients, $members);
}
}
elseif ($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
// Only alerts dept members if the ticket is NOT assigned.
foreach ($dept->getMembersForAlerts() as $M)
$recipients[] = $M;
}
// Always alert dept manager??
if ($cfg->alertDeptManagerONOverdueTicket()
&& $dept && ($manager=$dept->getManager())
) {
$recipients[]= $manager;
}
$sentlist = array();
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
|| !$staff->isAvailable()
|| in_array($staff->getEmail(), $sentlist)
) {
continue;
}
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff, $alert['subj'], $alert['body'], null);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
// TemplateVariable interface
function asVar() {
return $this->getNumber();
}
function getVar($tag) {
global $cfg;
switch(mb_strtolower($tag)) {
case 'phone':
case 'phone_number':
return $this->getPhoneNumber();
break;
case 'auth_token':
return $this->getOldAuthToken();
break;
case 'client_link':
return sprintf('%s/view.php?t=%s',
$cfg->getBaseUrl(), $this->getNumber());
break;
case 'staff_link':
return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId());
break;
case 'create_date':
return new FormattedDate($this->getCreateDate());
break;
case 'due_date':
if ($due = $this->getEstDueDate())
return new FormattedDate($due);
break;
case 'close_date':
if ($this->isClosed())
return new FormattedDate($this->getCloseDate());
break;
case 'last_update':
return new FormattedDate($this->lastupdate);
case 'user':
return $this->getOwner();
default:
if ($a = $this->getAnswer($tag))
// The answer object is retrieved here which will
// automatically invoke the toString() method when the
// answer is coerced into text
return $a;
}
}
static function getVarScope() {
$base = array(
'assigned' => __('Assigned Agent / Team'),
'close_date' => array(
'class' => 'FormattedDate', 'desc' => __('Date Closed'),
),
'create_date' => array(
'class' => 'FormattedDate', 'desc' => __('Date Created'),
),
'dept' => array(
'class' => 'Dept', 'desc' => __('Department'),
),
'due_date' => array(
'class' => 'FormattedDate', 'desc' => __('Due Date'),
),
'email' => __('Default email address of ticket owner'),
'id' => __('Ticket ID (internal ID)'),
'name' => array(
'class' => 'PersonsName', 'desc' => __('Name of ticket owner'),
),
'number' => __('Ticket Number'),
'phone' => __('Phone number of ticket owner'),
'priority' => array(
'class' => 'Priority', 'desc' => __('Priority'),
),
'recipients' => array(
'class' => 'UserList', 'desc' => __('List of all recipient names'),
),
'source' => __('Source'),
'status' => array(
'class' => 'TicketStatus', 'desc' => __('Status'),
),
'staff' => array(
'class' => 'Staff', 'desc' => __('Assigned/closing agent'),
),
'subject' => 'Subject',
'team' => array(
'class' => 'Team', 'desc' => __('Assigned/closing team'),
),
'thread' => array(
'class' => 'TicketThread', 'desc' => __('Ticket Thread'),
),
'topic' => array(
'class' => 'Topic', 'desc' => __('Help Topic'),
),
// XXX: Isn't lastreponse and lastmessage more useful
'last_update' => array(
'class' => 'FormattedDate', 'desc' => __('Time of last update'),
),
'user' => array(
'class' => 'User', 'desc' => __('Ticket Owner'),
),
);
$extra = VariableReplacer::compileFormScope(TicketForm::getInstance());
return $base + $extra;
}
// Searchable interface
static function getSearchableFields() {
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;
}
function manageMerge($tickets) {
global $thisstaff;
$permission = ($tickets['title'] && $tickets['title'] == 'link') ? (Ticket::PERM_LINK) : (Ticket::PERM_MERGE);
$eventName = ($tickets['title'] && $tickets['title'] == 'link') ? 'linked' : 'merged';
//see if any tickets should be unlinked
if ($tickets['dtids']) {
foreach($tickets['dtids'] as $key => $value) {
if (is_numeric($key) && $ticket = Ticket::lookup($value))
$ticket->unlink();
}
return true;
} elseif ($tickets['tids']) { //see if any tickets should be merged
$ticketObjects = array();
foreach($tickets['tids'] as $key => $value) {
if ($ticket = Ticket::lookupByNumber($value)) {
$ticketObjects[] = $ticket;
if (!$ticket->checkStaffPerm($thisstaff, $permission) && !$ticket->getThread()->isReferred())
return false;
if ($key == 0)
$parent = $ticket;
//changing from link to merge
if (($ticket->isParent() || $ticket->isChild()) &&
$ticket->getMergeType() == 'visual' && $tickets['combine'] != 2 ||
($tickets['combine'] == 2 && !$parent->isParent() && $parent->isChild())) { //changing link parent
$ticket->unlink();
$changeParent = true;
}
if ($ticket->getMergeType() == 'visual') {
$ticket->setSort($key);
$ticket->save();
}
if ($parent && $parent->getId() != $ticket->getId()) {
if (($changeParent) || ($parent->isParent() && $ticket->getMergeType() == 'visual' && !$ticket->isChild()) || //adding to link/merge
(!$parent->isParent() && !$ticket->isChild())) { //creating fresh link/merge
$parent->logEvent($eventName, array('ticket' => sprintf('Ticket #%s', $ticket->getNumber()), 'id' => $ticket->getId()));
$ticket->logEvent($eventName, array('ticket' => sprintf('Ticket #%s', $parent->getNumber()), 'id' => $parent->getId()));
if ($ticket->getPid() != $parent->getId())
$ticket->setPid($parent->getId());
$parent->setMergeType($tickets['combine'], true);
$ticket->setMergeType($tickets['combine']);
//referrals for merged tickets
if ($parent->getDeptId() != ($ticketDeptId = $ticket->getDeptId()) && $tickets['combine'] != 2) {
$refDept = $ticket->getDept();
$parent->getThread()->refer($refDept);
$evd = array('dept' => $ticketDeptId);
$parent->logEvent('referred', $evd);
}
}
//switch between combine and separate
} elseif ($parent->isParent() && $ticket->getMergeType() != 'visual' && $parent->getId() != $ticket->getId()) {
$ticket->setMergeType($tickets['combine']);
} elseif ($parent->isParent() && $ticket->getMergeType() != 'visual' && $parent->getId() == $ticket->getId())
$parent->setMergeType($tickets['combine'], true);
}
}
}
return $ticketObjects;
}
function merge($tickets) {
$options = $tickets;
if (!$tickets = self::manageMerge($tickets))
return false;
if (is_bool($tickets))
return true;
$children = array();
foreach ($tickets as $ticket) {
if ($ticket->isParent())
$parent = $ticket;
else
$children[] = $ticket;
}
if ($parent && $parent->getMergeType() != 'visual') {
$errors = array();
foreach ($children as $child) {
if ($options['participants'] == 'all' && $collabs = $child->getCollaborators()) {
foreach ($collabs as $collab) {
$collab = $collab->getUser();
if ($collab->getId() != $parent->getOwnerId())
$parent->addCollaborator($collab, array(), $errors);
}
}
$cUser = $child->getUser();
if ($cUser->getId() != $parent->getOwnerId())
$parent->addCollaborator($cUser, array(), $errors);
$parentThread = $parent->getThread();
$deletedChild = Thread::objects()
->filter(array('extra__contains'=>'"ticket_id":'.$child->getId()))
->values_flat('id', 'extra')
->first();
if ($deletedChild) {
$extraThread = Thread::lookup($deletedChild[0]);
$extraThread->setExtra($parentThread, array('extra' => $deletedChild[1], 'threadId' => $extraThread->getId()));
}
if ($child->getThread())
$child->getThread()->setExtra($parentThread);
$child->setMergeType($options['combine']);
$child->setStatus(intval($options['childStatusId']), false, $errors, true, true); //force close status for children
if ($options['parentStatusId'])
$parent->setStatus(intval($options['parentStatusId']));
if ($options['delete-child'] || $options['move-tasks']) {
if ($tasks = Task::objects()
->filter(array('object_id' => $child->getId()))
->values_flat('id')) {
foreach ($tasks as $key => $tid) {
$task = Task::lookup($tid[0]);
$task->object_id = $parent->getId();
$task->save();
}
}
}
if ($options['delete-child'])
$child->delete();
}
return $parent;
}
return false;
}
function getRelatedTickets() {
return sprintf('<tr>
<td width="8px">&nbsp;</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->thread->getReferral($object->getId(), $type)))
return $referral;
return false;
}
//Dept Transfer...with alert.. done by staff
function transfer(TransferForm $form, &$errors, $alert=true) {
global $thisstaff, $cfg;
// Check if staff can do the transfer
if (!$this->checkStaffPerm($thisstaff, Ticket::PERM_TRANSFER))
return false;
$cdept = $this->getDept(); // Current department
$dept = $form->getDept(); // Target department
if (!$dept || !($dept instanceof Dept))
$errors['dept'] = __('Department selection required');
elseif ($dept->getid() == $this->getDeptId())
$errors['dept'] = sprintf(
__('%s already in the department'), __('Ticket'));
else {
$this->dept_id = $dept->getId();
// Make sure the new department allows assignment to the
// currently assigned agent (if any)
if ($this->isAssigned()
&& ($staff=$this->getStaff())
&& $dept->assignMembersOnly()
&& !$dept->isMember($staff)
) {
$this->staff_id = 0;
}
}
if ($errors || !$this->save(true))
return false;
// Reopen ticket if closed
if ($this->isClosed())
$this->reopen();
// Set SLA of the new department
if (!$this->getSLAId() || $this->getSLA()->isTransient())
if (($slaId=$this->getDept()->getSLAId()))
$this->selectSLAId($slaId);
// Log transfer event
$this->logEvent('transferred', array('dept' => $dept->getName()));
if (($referral=$this->hasReferral($dept,ObjectModel::OBJECT_TYPE_DEPT)))
$referral->delete();
// Post internal note if any
$note = null;
$comments = $form->getField('comments')->getClean();
if ($comments) {
$title = sprintf(__('%1$s transferred from %2$s to %3$s'),
__('Ticket'),
$cdept->getName(),
$dept->getName());
$_errors = array();
$note = $this->postNote(
array('note' => $comments, 'title' => $title),
$_errors, $thisstaff, false);
}
if ($form->refer() && $cdept)
$this->getThread()->refer($cdept);
//Send out alerts if enabled AND requested
if (!$alert || !$cfg->alertONTransfer() || !$dept->getNumMembersForAlerts())
return true; //no alerts!!
if (($email = $dept->getAlertEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg=$tpl->getTransferAlertMsgTemplate())
) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $note, 'staff' => $thisstaff));
// Recipients
$recipients = array();
// Assigned staff or team... if any
if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
if($this->getStaffId())
$recipients[] = $this->getStaff();
elseif ($this->getTeamId()
&& ($team=$this->getTeam())
&& ($members=$team->getMembersForAlerts())
) {
$recipients = array_merge($recipients, $members);
}
}
elseif ($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
// Only alerts dept members if the ticket is NOT assigned.
foreach ($dept->getMembersForAlerts() as $M)
$recipients[] = $M;
}
// Always alert dept manager??
if ($cfg->alertDeptManagerONTransfer()
&& $dept
&& ($manager=$dept->getManager())
) {
$recipients[] = $manager;
}
$sentlist = $options = array();
if ($note) {
$options += array('thread'=>$note);
}
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
|| !$staff->isAvailable()
|| in_array($staff->getEmail(), $sentlist)
) {
continue;
}
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
function claim(ClaimForm $form, &$errors) {
global $thisstaff;
$dept = $this->getDept();
$assignee = $form->getAssignee();
if (!($assignee instanceof Staff)
|| !$thisstaff
|| $thisstaff->getId() != $assignee->getId()) {
$errors['err'] = __('Unknown assignee');
} elseif (!$assignee->isAvailable()) {
$errors['err'] = __('Agent is unavailable for assignment');
} elseif (!$dept->canAssign($assignee)) {
$errors['err'] = __('Permission denied');
}
if ($errors)
return false;
return $this->assignToStaff($assignee, $form->getComments(), false);
}
function assignToStaff($staff, $note, $alert=true, $user=null) {
if(!is_object($staff) && !($staff = Staff::lookup($staff)))
return false;
if (!$staff->isAvailable() || !$this->setStaffId($staff->getId()))
return false;
$this->onAssign($staff, $note, $alert);
global $thisstaff;
$data = array();
if ($thisstaff && $staff->getId() == $thisstaff->getId())
$data['claim'] = true;
else
$data['staff'] = $staff->getId();
$this->logEvent('assigned', $data, $user);
$key = $data['claim'] ? 'claim' : 'auto';
$type = array('type' => 'assigned', $key => true);
Signal::send('object.edited', $this, $type);
if (($referral=$this->hasReferral($staff,ObjectModel::OBJECT_TYPE_STAFF)))
$referral->delete();
return true;
}
function assignToTeam($team, $note, $alert=true, $user=null) {
if(!is_object($team) && !($team = Team::lookup($team)))
return false;
if (!$team->isActive() || !$this->setTeamId($team->getId()))
return false;
//Clear - staff if it's a closed ticket
// staff_id is overloaded -> assigned to & closed by.
if ($this->isClosed())
$this->setStaffId(0);
$this->onAssign($team, $note, $alert);
$this->logEvent('assigned', array('team' => $team->getId()), $user);
if (($referral=$this->hasReferral($team,ObjectModel::OBJECT_TYPE_TEAM)))
$referral->delete();
return true;
}
function assign(AssignmentForm $form, &$errors, $alert=true) {
global $thisstaff;
$evd = array();
$audit = array();
$refer = null;
$dept = $this->getDept();
$assignee = $form->getAssignee();
if ($assignee instanceof Staff) {
if ($this->getStaffId() == $assignee->getId()) {
$errors['assignee'] = sprintf(__('%s already assigned to %s'),
__('Ticket'),
__('the agent')
);
} elseif (!$assignee->isAvailable()) {
$errors['assignee'] = __('Agent is unavailable for assignment');
} elseif (!$dept->canAssign($assignee)) {
$errors['err'] = __('Permission denied');
} else {
$refer = $this->staff ?: null;
$this->staff_id = $assignee->getId();
if ($thisstaff && $thisstaff->getId() == $assignee->getId()) {
$alert = false;
$evd['claim'] = true;
$audit = array('staff' => $assignee->getName()->name,'claim' => true);
} else {
$evd['staff'] = array($assignee->getId(), (string) $assignee->getName()->getOriginal());
$audit = array('staff' => $assignee->getName()->name);
}
if (($referral=$this->hasReferral($assignee,ObjectModel::OBJECT_TYPE_STAFF)))
$referral->delete();
}
} elseif ($assignee instanceof Team) {
if ($this->getTeamId() == $assignee->getId()) {
$errors['assignee'] = sprintf(__('%s already assigned to %s'),
__('Ticket'),
__('the team')
);
} elseif (!$dept->canAssign($assignee)) {
$errors['err'] = __('Permission denied');
} else {
$refer = $this->team ?: null;
$this->team_id = $assignee->getId();
$evd = array('team' => $assignee->getId());
$audit = array('team' => $assignee->getName());
if (($referral=$this->hasReferral($assignee,ObjectModel::OBJECT_TYPE_TEAM)))
$referral->delete();
}
} else {
$errors['assignee'] = __('Unknown assignee');
}
if ($errors || !$this->save(true))
return false;
$this->logEvent('assigned', $evd);
$type = array('type' => 'assigned');
$type += $audit;
Signal::send('object.edited', $this, $type);
$this->onAssign($assignee, $form->getComments(), $alert);
if ($refer && $form->refer())
$this->getThread()->refer($refer);
return true;
}
// Unassign primary assignee
function unassign() {
// We can't release what is not assigned buddy!
if (!$this->isAssigned())
return true;
// We can only unassigned OPEN tickets.
if ($this->isClosed())
return false;
// Unassign staff (if any)
if ($this->getStaffId() && !$this->setStaffId(0))
return false;
// Unassign team (if any)
if ($this->getTeamId() && !$this->setTeamId(0))
return false;
return true;
}
function release($info=array(), &$errors) {
if ($info['sid'] && $info['tid'])
return $this->unassign();
elseif ($info['sid'] && $this->setStaffId(0))
return true;
elseif ($info['tid'] && $this->setTeamId(0))
return true;
return false;
}
function refer(ReferralForm $form, &$errors, $alert=true) {
global $thisstaff;
$evd = array();
$audit = array();
$referee = $form->getReferee();
switch (true) {
case $referee instanceof Staff:
$dept = $this->getDept();
if ($this->getStaffId() == $referee->getId()) {
$errors['agent'] = sprintf(__('%s is assigned to %s'),
__('Ticket'),
__('the agent')
);
} elseif(!$referee->isAvailable()) {
$errors['agent'] = sprintf(__('Agent is unavailable for %s'),
__('referral'));
} else {
$evd['staff'] = array($referee->getId(), (string) $referee->getName()->getOriginal());
$audit = array('staff' => $referee->getName()->name);
}
break;
case $referee instanceof Team:
if ($this->getTeamId() == $referee->getId()) {
$errors['team'] = sprintf(__('%s is assigned to %s'),
__('Ticket'),
__('the team')
);
} else {
//TODO::
$evd = array('team' => $referee->getId());
$audit = array('team' => $referee->getName());
}
break;
case $referee instanceof Dept:
if ($this->getDeptId() == $referee->getId()) {
$errors['dept'] = sprintf(__('%s is already in %s'),
__('Ticket'),
__('the department')
);
} else {
//TODO::
$evd = array('dept' => $referee->getId());
$audit = array('dept' => $referee->getName());
}
break;
default:
$errors['target'] = __('Unknown referral');
}
if (!$errors && !$this->getThread()->refer($referee))
$errors['err'] = __('Unable to refer ticket');
if ($errors)
return false;
$this->logEvent('referred', $evd);
$type = array('type' => 'referred');
$type += $audit;
Signal::send('object.edited', $this, $type);
return true;
}
function systemReferral($emails) {
global $cfg;
if (!$thread = $this->getThread())
return;
$eventEmails = array();
$events = ThreadEvent::objects()
->filter(array('thread_id' => $thread->getId(),
'event__name' => 'transferred'));
if ($events) {
foreach ($events as $e) {
$emailId = Dept::getEmailIdById($e->dept_id) ?: $cfg->getDefaultEmailId();
if (!in_array($emailId, $eventEmails))
$eventEmails[] = $emailId;
}
}
foreach ($emails as $id) {
$refer = $eventEmails ? !in_array($id, $eventEmails) : true;
if ($id != $this->email_id
&& $refer
&& ($email=Email::lookup($id))
&& $this->getDeptId() != $email->getDeptId()
&& ($dept=Dept::lookup($email->getDeptId()))
&& $this->getThread()->refer($dept)
)
$this->logEvent('referred',
array('dept' => $dept->getId()));
}
}
//Change ownership
function changeOwner($user) {
global $thisstaff;
if (!$user
|| ($user->getId() == $this->getOwnerId())
|| !($this->checkStaffPerm($thisstaff,
Ticket::PERM_EDIT))
) {
return false;
}
$this->user_id = $user->getId();
if (!$this->save())
return false;
unset($this->user);
$this->collaborators = null;
$this->recipients = null;
// Remove the new owner from list of collaborators
$c = Collaborator::lookup(array(
'user_id' => $user->getId(),
'thread_id' => $this->getThreadId()
));
if ($c)
$c->delete();
$this->logEvent('edited', array('owner' => $user->getId(), 'fields' => array('Ticket Owner' => $user->getName()->name)));
return true;
}
// Insert message from client
function postMessage($vars, $origin='', $alerts=true) {
global $cfg;
if ($origin)
$vars['origin'] = $origin;
if (isset($vars['ip']))
$vars['ip_address'] = $vars['ip'];
elseif (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
$vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
//see if message should go to a parent ticket
if ($this->isChild() && $this->getMergeType() != 'visual')
$parent = self::lookup($this->getPid());
$ticket = $parent ?: $this;
$errors = array();
if ($vars['userId'] != $ticket->user_id) {
if ($vars['userId']) {
$user = User::lookup($vars['userId']);
} elseif ($vars['header']
&& ($hdr= Mail_parse::splitHeaders($vars['header'], true))
&& $hdr['From']
&& ($addr= Mail_Parse::parseAddressList($hdr['From']))) {
$info = array(
'name' => $addr[0]->personal,
'email' => $addr[0]->mailbox.'@'.$addr[0]->host);
if ($user=User::fromVars($info))
$vars['userId'] = $user->getId();
}
if ($user) {
$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 function filterTicketData($origin, $vars, $forms, $user=false, $postCreate=false) {
global $cfg;
// Unset all the filter data field data in case things change
// during recursive calls
foreach ($vars as $k=>$v)
if (strpos($k, 'field.') === 0)
unset($vars[$k]);
foreach ($forms as $F) {
if ($F) {
$vars += $F->getFilterData();
}
}
if (!$user) {
$interesting = array('name', 'email');
$user_form = UserForm::getUserForm()->getForm($vars);
// Add all the user-entered info for filtering
foreach ($interesting as $F) {
if ($field = $user_form->getField($F))
$vars[$F] = $field->toString($field->getClean());
}
// Attempt to lookup the user and associated data
$user = User::lookupByEmail($vars['email']);
}
// Add in user and organization data for filtering
if ($user) {
$vars += $user->getFilterData();
$vars['email'] = $user->getEmail();
$vars['name'] = $user->getName()->getOriginal();
if ($org = $user->getOrganization()) {
$vars += $org->getFilterData();
}
}
// Don't include org information based solely on email domain
// for existing user instances
else {
// Unpack all known user info from the request
foreach ($user_form->getFields() as $f) {
$vars['field.'.$f->get('id')] = $f->toString($f->getClean());
}
// Add in organization data if one exists for this email domain
list($mailbox, $domain) = explode('@', $vars['email'], 2);
if ($org = Organization::forDomain($domain)) {
$vars += $org->getFilterData();
}
}
try {
// Make sure the email address is not banned
if (($filter=Banlist::isBanned($vars['email']))) {
throw new RejectedException($filter, $vars);
}
// Init ticket filters...
$ticket_filter = new TicketFilter($origin, $vars);
$ticket_filter->apply($vars, $postCreate);
}
catch (FilterDataChanged $ex) {
// Don't pass user recursively, assume the user has changed
return self::filterTicketData($origin, $ex->getData(), $forms);
}
return $vars;
}
/*
* The mother of all functions...You break it you fix it!
*
* $autorespond and $alertstaff overrides config settings...
*/
static function create($vars, &$errors, $origin, $autorespond=true,
$alertstaff=true) {
global $ost, $cfg, $thisstaff;
// Don't enforce form validation for email
$field_filter = function($type) use ($origin) {
return function($f) use ($origin, $type) {
// Ultimately, only offer validation errors for web for
// non-internal fields. For email, no validation can be
// performed. For other origins, validate as usual
switch (strtolower($origin)) {
case 'email':
return false;
case 'staff':
// Required 'Contact Information' fields aren't required
// when staff open tickets
return $f->isVisibleToStaff();
case 'web':
return $f->isVisibleToUsers();
default:
return true;
}
};
};
$reject_ticket = function($message) use (&$errors) {
global $ost;
$errors = array(
'errno' => 403,
'err' => __('This help desk is for use by authorized users only'));
$ost->logWarning(_S('Ticket denied'), $message, false);
return 0;
};
Signal::send('ticket.create.before', null, $vars);
// Create and verify the dynamic form entry for the new ticket
$form = TicketForm::getNewInstance();
$form->setSource($vars);
// If submitting via email or api, ensure we have a subject and such
if (!in_array(strtolower($origin), array('web', 'staff'))) {
foreach ($form->getFields() as $field) {
$fname = $field->get('name');
if ($fname && isset($vars[$fname]) && !$field->value)
$field->value = $field->parse($vars[$fname]);
}
}
if ($vars['uid'])
$user = User::lookup($vars['uid']);
$id=0;
$fields=array();
switch (strtolower($origin)) {
case 'web':
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Select a Help Topic'));
break;
case 'staff':
$fields['deptId'] = array('type'=>'int', 'required'=>0, 'error'=>__('Department selection is required'));
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Help topic selection is required'));
$fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY'));
case 'api':
$fields['source'] = array('type'=>'string', 'required'=>1, 'error'=>__('Indicate ticket source'));
break;
case 'email':
$fields['emailId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Unknown system email'));
break;
default:
# TODO: Return error message
$errors['err']=$errors['origin'] = __('Invalid ticket origin given');
}
if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
$errors['err'] = sprintf('%s — %s',
__('Missing or invalid data'),
__('Correct any errors below and try again'));
// Make sure the due date is valid
if ($vars['duedate']) {
if (strtotime($vars['duedate']) === false)
$errors['duedate']=__('Invalid due date');
elseif (Misc::user2gmtime($vars['duedate']) <= Misc::user2gmtime())
$errors['duedate']=__('Due date must be in the future');
}
$topic_forms = array();
if (!$errors) {
// Handle the forms associate with the help topics. Instanciate the
// entries, disable and track the requested disabled fields.
if ($vars['topicId']) {
if ($__topic=Topic::lookup($vars['topicId'])) {
foreach ($__topic->getForms() as $idx=>$__F) {
$disabled = array();
foreach ($__F->getFields() as $field) {
if (!$field->isEnabled() && $field->hasFlag(DynamicFormField::FLAG_ENABLED))
$disabled[] = $field->get('id');
}
// Special handling for the ticket form — disable fields
// requested to be disabled as per the help topic.
if ($__F->get('type') == 'T') {
foreach ($form->getFields() as $field) {
if (false !== array_search($field->get('id'), $disabled))
$field->disable();
}
$form->sort = $idx;
$__F = $form;
}
else {
$__F = $__F->instanciate($idx);
$__F->setSource($vars);
$topic_forms[] = $__F;
}
// Track fields currently disabled
$__F->extra = JsonDataEncoder::encode(array(
'disable' => $disabled
));
}
}
}
try {
$vars = self::filterTicketData($origin, $vars,
array_merge(array($form), $topic_forms), $user, false);
}
catch (RejectedException $ex) {
return $reject_ticket(
sprintf(_S('Ticket rejected (%s) by filter "%s"'),
$ex->vars['email'], $ex->getRejectingFilter()->getName())
);
}
//Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
if ($cfg->getMaxOpenTickets() > 0
&& strcasecmp($origin, 'staff')
&& ($_user=TicketUser::lookupByEmail($vars['email']))
&& ($openTickets=$_user->getNumOpenTickets())
&& ($openTickets>=$cfg->getMaxOpenTickets()) ) {
$errors = array('err' => __("You've reached the maximum open tickets allowed."));
$ost->logWarning(sprintf(_S('Ticket denied - %s'), $vars['email']),
sprintf(_S('Max open tickets (%1$d) reached for %2$s'),
$cfg->getMaxOpenTickets(), $vars['email']),
false);
return 0;
}
// Allow vars to be changed in ticket filter and applied to the user
// account created or detected
if (!$user && $vars['email'])
$user = User::lookupByEmail($vars['email']);
if (!$user) {
// Reject emails if not from registered clients (if
// configured)
if (strcasecmp($origin, 'email') === 0
&& !$cfg->acceptUnregisteredEmail()) {
list($mailbox, $domain) = explode('@', $vars['email'], 2);
// Users not yet created but linked to an organization
// are still acceptable
if (!Organization::forDomain($domain)) {
return $reject_ticket(
sprintf(_S('Ticket rejected (%s) (unregistered client)'),
$vars['email']));
}
}
$user_form = UserForm::getUserForm()->getForm($vars);
$can_create = !$thisstaff || $thisstaff->hasPerm(User::PERM_CREATE);
if (!$user_form->isValid($field_filter('user'))
|| !($user=User::fromVars($user_form->getClean(), $can_create))
) {
$errors['user'] = $can_create
? __('Incomplete client information')
: __('You do not have permission to create users.');
}
}
}
if (!$form->isValid($field_filter('ticket')))
$errors += $form->errors();
if ($vars['topicId']) {
if (($topic=Topic::lookup($vars['topicId']))
&& $topic->isActive()) {
foreach ($topic_forms as $topic_form) {
$TF = $topic_form->getForm($vars);
if (!$TF->isValid($field_filter('topic')))
$errors = array_merge($errors, $TF->errors());
}
} else {
$vars['topicId'] = 0;
}
}
// Any errors above are fatal.
if ($errors)
return 0;
Signal::send('ticket.create.validated', null, $vars);
# Some things will need to be unpacked back into the scope of this
# function
if (isset($vars['autorespond']))
$autorespond = $vars['autorespond'];
# Apply filter-specific priority
if ($vars['priorityId'])
$form->setAnswer('priority', null, $vars['priorityId']);
// If the filter specifies a help topic which has a form associated,
// and there was previously either no help topic set or the help
// topic did not have a form, there's no need to add it now as (1)
// validation is closed, (2) there may be a form already associated
// and filled out from the original help topic, and (3) staff
// members can always add more forms now
// OK...just do it.
$statusId = $vars['statusId'];
$deptId = $vars['deptId']; //pre-selected Dept if any.
$source = ucfirst($vars['source']);
// Apply email settings for emailed tickets. Email settings should
// trump help topic settins if the email has an associated help
// topic
if ($vars['emailId'] && ($email=Email::lookup($vars['emailId']))) {
$deptId = $deptId ?: $email->getDeptId();
$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'] && !strcasecmp($origin,'staff'))
$ticket->duedate = date('Y-m-d G:i',
Misc::dbtime($vars['duedate']));
if (!$ticket->save())
return null;
if (!($thread = TicketThread::create($ticket->getId())))
return null;
/* -------------------- POST CREATE ------------------------ */
$vars['ticket'] = $ticket;
self::filterTicketData($origin, $vars,
array_merge(array($form), $topic_forms), $user, true);
// Save the (common) dynamic form
// Ensure we have a subject
$subject = $form->getAnswer('subject');
if ($subject && !$subject->getValue() && $topic)
$subject->setValue($topic->getFullName());
$form->setTicketId($ticket->getId());
$form->save();
// Save the form data from the help-topic form, if any
foreach ($topic_forms as $topic_form) {
$topic_form->setTicketId($ticket->getId());
$topic_form->save();
}
$ticket->loadDynamicData(true);
$dept = $ticket->getDept();
// Start tracking ticket lifecycle events (created should come first!)
$ticket->logEvent('created', null, $thisstaff ?: $user);
// Set default ticket status (if none) for Thread::getObject()
// in addCollaborators()
if ($ticket->getStatusId() <= 0)
$ticket->setStatusId($cfg->getDefaultTicketStatusId());
// Add collaborators (if any)
if (isset($vars['ccs']) && count($vars['ccs']))
$ticket->addCollaborators($vars['ccs'], array(), $errors);
// Add organizational collaborators
if ($org && $org->autoAddCollabs()) {
$pris = $org->autoAddPrimaryContactsAsCollabs();
$members = $org->autoAddMembersAsCollabs();
$settings = array('isactive' => true);
$collabs = array();
foreach ($org->allMembers() as $u) {
$_errors = array();
if ($members || ($pris && $u->isPrimaryContact())) {
if ($c = $ticket->addCollaborator($u, $settings, $_errors)) {
$collabs[] = (string) $c;
}
}
}
//TODO: Can collaborators add others?
if ($collabs) {
$ticket->logEvent('collab', array('org' => $org->getId()));
}
}
//post the message.
$vars['title'] = $vars['subject']; //Use the initial subject as title of the post.
$vars['userId'] = $ticket->getUserId();
$message = $ticket->postMessage($vars , $origin, false);
// If a message was posted, flag it as the orignal message. This
// needs to be done on new ticket, so as to otherwise separate the
// concept from the first message entry in a thread.
if ($message instanceof ThreadEntry) {
$message->setFlag(ThreadEntry::FLAG_ORIGINAL_MESSAGE);
$message->save();
}
//check to see if ticket was created from a thread
if ($_SESSION[':form-data']['ticketId'] || $_SESSION[':form-data']['taskId']) {
$oldTicket = Ticket::lookup($_SESSION[':form-data']['ticketId']);
$oldTask = Task::lookup($_SESSION[':form-data']['taskId']);
//add internal note to new ticket.
//New ticket should have link to old task/ticket:
$link = sprintf('<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';