Newer
Older
<?php
/*********************************************************************
class.ticket.php
The most important class! Don't play with fire please.
Peter Rotich <peter@osticket.com>
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.client.php');
include_once(INCLUDE_DIR.'class.team.php');
include_once(INCLUDE_DIR.'class.email.php');
include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.topic.php');
include_once(INCLUDE_DIR.'class.lock.php');
include_once(INCLUDE_DIR.'class.file.php');
include_once(INCLUDE_DIR.'class.attachment.php');
include_once(INCLUDE_DIR.'class.banlist.php');
include_once(INCLUDE_DIR.'class.template.php');
include_once(INCLUDE_DIR.'class.variable.php');
include_once(INCLUDE_DIR.'class.priority.php');
include_once(INCLUDE_DIR.'class.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.faq.php');
class TicketModel extends VerySimpleModel {
static $meta = array(
'table' => TICKET_TABLE,
'pk' => array('ticket_id'),
'joins' => array(
'user' => array(
'constraint' => array('user_id' => 'User.id')
),
'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'),
'sla' => array(
'constraint' => array('sla_id' => 'SlaModel.id'),
'null' => true,
),
'constraint' => array('staff_id' => 'Staff.staff_id'),
'null' => true,
),
'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' => 'Thread.ticket',
'list' => false,
'null' => true,
),
'cdata' => array(
'reverse' => 'TicketCData.ticket',
'list' => false,
),
)
);
const PERM_CREATE = 'ticket.create';
const PERM_EDIT = 'ticket.edit';
const PERM_ASSIGN = 'ticket.assign';
const PERM_TRANSFER = 'ticket.transfer';
const PERM_REPLY = 'ticket.reply';
const PERM_CLOSE = 'ticket.close';
const PERM_DELETE = 'ticket.delete';
static protected $perms = array(
self::PERM_CREATE => array(
/* @trans */ 'Ability to open tickets on behalf of users'),
self::PERM_EDIT => array(
/* @trans */ 'Ability to edit tickets'),
self::PERM_ASSIGN => array(
/* @trans */ 'Ability to assign tickets to agents or teams'),
self::PERM_TRANSFER => array(
/* @trans */ 'Ability to transfer tickets between departments'),
self::PERM_REPLY => array(
/* @trans */ 'Ability to post a ticket reply'),
self::PERM_CLOSE => array(
/* @trans */ 'Ability to close tickets'),
self::PERM_DELETE => array(
/* @trans */ 'Ability to delete tickets'),
);
function getId() {
return $this->ticket_id;
}
function getEffectiveDate() {
strtotime($this->lastmessage),
strtotime($this->closed),
strtotime($this->reopened),
strtotime($this->created)
}
function delete() {
if (($ticket=Ticket::lookup($this->getId())) && @$ticket->delete())
return true;
return false;
}
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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} {
static \$meta = array(
'view' => true,
'pk' => array('ticket_id'),
'joins' => array(
'ticket' => array(
'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
),
)
);
static function getQuery(\$compiler) {
return '('.DynamicForm{$form->id}::getCrossTabQuery('T', 'ticket_id').')';
}
}
EOF;
eval($cdata_class);
static::$meta['joins']['cdata+'.$form->id] = array(
'reverse' => 'TicketCdataForm'.$form->id.'.ticket',
'null' => true,
);
// This may be necessary if the model has already been inspected
if (static::$meta instanceof ModelMeta)
static::$meta->processJoin(static::$meta['joins']['cdata+'.$form->id]);
}
}
static function getPermissions() {
return self::$perms;
}
RolePermission::register(/* @trans */ 'Tickets', TicketModel::getPermissions(), true);
class TicketCData extends VerySimpleModel {
static $meta = array(
'pk' => array('ticket_id'),
'joins' => array(
'ticket' => array(
'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
),
':priority' => array(
'constraint' => array('priority' => 'Priority.priority_id'),
'null' => true,
),
),
);
}
TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';
implements RestrictedAccess, Threadable, TemplateVariable {
Peter Rotich
committed
var $number;
var $ht;
Peter Rotich
committed
var $dept; //Dept obj
var $sla; // SLA obj
var $staff; //Staff obj
var $team; //Team obj
var $topic; //Topic obj
var $tlock; //TicketLock obj
var $thread; //Thread obj.
Peter Rotich
committed
function Ticket($id) {
Peter Rotich
committed
$sql='SELECT ticket.*, thread.id as thread_id, ticket.lock_id, dept.name as dept_name '
.' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.id) '
.' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
.' LEFT JOIN '.LOCK_TABLE.' tlock
ON ( ticket.lock_id=tlock.lock_id AND tlock.expire>NOW()) '
.' LEFT JOIN '.TASK_TABLE.' task
ON ( task.object_id = ticket.ticket_id AND task.object_type="T" ) '
.' LEFT JOIN '.THREAD_TABLE.' thread
ON ( thread.object_id = ticket.ticket_id AND thread.object_type="T" ) '
.' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry
ON ( entry.thread_id = thread.id ) '
.' LEFT JOIN '.ATTACHMENT_TABLE.' attach
ON ( attach.object_id = entry.id AND attach.`type` = "H") '
.' WHERE ticket.ticket_id='.db_input($id)
.' GROUP BY ticket.ticket_id';
//echo $sql;
Peter Rotich
committed
$this->ht = db_fetch_array($res);
$this->_answers = array();
Peter Rotich
committed
//Reset the sub classes (initiated ondemand)...good for reloads.
$this->team = null;
$this->dept = null;
$this->sla = null;
$this->tlock = null;
$this->stats = null;
$this->topic = null;
$this->thread = null;
Peter Rotich
committed
if (!$this->_answers) {
foreach (DynamicFormEntry::forTicket($this->getId(), true) as $form) {
foreach ($form->getAnswers() as $answer) {
$tag = mb_strtolower($answer->field->name)
?: 'field.' . $answer->field->id;
$this->_answers[$tag] = $answer;
function reload() {
return $this->load();
}
Peter Rotich
committed
function hasState($state) {
return (strcasecmp($this->getState(), $state)==0);
}
}
function isReopened() {
return ($this->getReopenDate());
}
function isReopenable() {
return $this->getStatus()->isReopenable();
}
return $this->hasState('closed');
}
function isArchived() {
return $this->hasState('archived');
}
function isDeleted() {
return $this->hasState('deleted');
}
function isAssigned() {
return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId()));
}
function isOverdue() {
Peter Rotich
committed
return ($this->ht['isoverdue']);
Peter Rotich
committed
function isAnswered() {
return ($this->ht['isanswered']);
}
function isLocked() {
function checkStaffPerm($staff, $perm=null) {
// Must be a valid staff
if (!$staff instanceof Staff && !($staff=Staff::lookup($staff)))
// Check access based on department or assignment
if (!(!$staff->showAssignedOnly()
&& $staff->canAccessDept($this->getDeptId()))
// only open tickets can be considered assigned
&& $this->isOpen()
&& $staff->getId() != $this->getStaffId()
&& !$staff->isTeamMember($this->getTeamId()))
// At this point staff has view access unless a specific permission is
// requested
if ($perm === null)
// Permission check requested -- get role.
if (!($role=$staff->getRole($this->getDeptId())))
return false;
// Check permission based on the effective role
return $role->hasPerm($perm);
function checkUserAccess($user) {
if (!$user || !($user instanceof EndUser))
//Ticket Owner
if ($user->getId() == $this->getUserId())
//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())))
function getId() {
function getNumber() {
Peter Rotich
committed
return $this->number;
Peter Rotich
committed
function getOwnerId() {
return $this->ht['user_id'];
}
function getOwner() {
if (!isset($this->owner)
&& ($u=User::lookup($this->getOwnerId())))
$this->owner = new TicketOwner(new EndUser($u), $this);
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);
return (string) $this->_answers['subject'];
}
/* Help topic title - NOT object -> $topic */
function getHelpTopic() {
Peter Rotich
committed
if(!$this->ht['helptopic'] && ($topic=$this->getTopic()))
$this->ht['helptopic'] = $topic->getFullName();
Peter Rotich
committed
return $this->ht['helptopic'];
Peter Rotich
committed
function getCreateDate() {
return $this->ht['created'];
}
function getOpenDate() {
return $this->getCreateDate();
}
function getReopenDate() {
Peter Rotich
committed
return $this->ht['reopened'];
Peter Rotich
committed
function getUpdateDate() {
return $this->ht['updated'];
function getEffectiveDate() {
return $this->ht['lastupdate'];
}
Peter Rotich
committed
function getDueDate() {
return $this->ht['duedate'];
function getSLADueDate() {
if ($sla = $this->getSLA()) {
$dt = new DateTime($this->getCreateDate());
return $dt
->add(new DateInterval('PT' . $sla->getGracePeriod() . 'H'))
->format('Y-m-d H:i:s');
}
}
function updateEstDueDate() {
$estimatedDueDate = $this->getEstDueDate();
if ($estimatedDueDate != $this->ht['est_duedate']) {
$sql = 'UPDATE '.TICKET_TABLE.' SET `est_duedate`='.db_input($estimatedDueDate)
.' WHERE `ticket_id`='.db_input($this->getId());
db_query($sql);
}
}
function getEstDueDate() {
//Real due date
if(($duedate=$this->getDueDate()))
return $duedate;
//return sla due date (If ANY)
return $this->getSLADueDate();
}
Peter Rotich
committed
function getCloseDate() {
return $this->ht['closed'];
function getStatusId() {
return $this->ht['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) {
$sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
' WHERE ticket_id='.db_input($this->getId());
Peter Rotich
committed
function getStatus() {
if (!$this->status && $this->getStatusId())
$this->status = TicketStatus::lookup($this->getStatusId());
return $this->status;
}
function getState() {
if (!$this->getStatus())
return '';
return $this->getStatus()->getState();
Peter Rotich
committed
function getDeptId() {
return $this->ht['dept_id'];
Peter Rotich
committed
function getDeptName() {
if(!$this->ht['dept_name'] && ($dept = $this->getDept()))
$this->ht['dept_name'] = $dept->getFullName();
Peter Rotich
committed
return $this->ht['dept_name'];
if (($a = $this->_answers['priority'])
&& ($b = $a->getValue()))
return $b->getId();
return $cfg->getDefaultPriorityId();
Peter Rotich
committed
if (($a = $this->_answers['priority']) && ($b = $a->getValue()))
return $b->getDesc();
return '';
Peter Rotich
committed
return (string)$this->getOwner()->getPhoneNumber();
}
function getSource() {
return $this->ht['source'];
}
Peter Rotich
committed
function getIP() {
return $this->ht['ip_address'];
}
function getHashtable() {
return $this->ht;
}
function getUpdateInfo() {
'topicId' => $this->getTopicId(),
'slaId' => $this->getSLAId(),
? Format::date($this->getDueDate())
'time' => $this->getDueDate()?(Format::date($this->getDueDate(), true, 'HH:mm')):'',
Peter Rotich
committed
Peter Rotich
committed
function getLock() {
if (!isset($this->tlock) && $this->ht['lock_id'])
$this->tlock = Lock::lookup($this->ht['lock_id']);
Peter Rotich
committed
Peter Rotich
committed
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.
Peter Rotich
committed
return $lock;
}
//No lock on the ticket or it is expired
$this->tlock = Lock::acquire($staffId, $lockTime); //Create a new lock..
if ($this->tlock) {
$sql = 'UPDATE '.TICKET_TABLE.' SET `lock_id` = '
.db_input($this->tlock->getId())
.' WHERE `ticket_id` = '. db_input($this->getId());
db_query($sql);
}
//load and return the newly created lock if any!
Peter Rotich
committed
function releaseLock($staffId=false) {
if (!($lock = $this->getLock()))
return false;
if ($staffId && $lock->staff_id != $staffId)
return false;
$sql = 'UPDATE '.TICKET_TABLE.' SET `lock_id` = 0 WHERE `ticket_id` = '
. db_input($this->getId());
return ($res = db_query($sql)) && db_affected_rows($res);
}
Peter Rotich
committed
function getDept() {
Peter Rotich
committed
if(!$this->dept)
if(!($this->dept = Dept::lookup($this->getDeptId())))
$this->dept = $cfg->getDefaultDept();
function getUserId() {
return $this->getOwnerId();
}
if(!isset($this->user) && $this->getOwner())
$this->user = new EndUser($this->getOwner());
Peter Rotich
committed
function getStaffId() {
return $this->ht['staff_id'];
Peter Rotich
committed
function getStaff() {
if(!$this->staff && $this->getStaffId())
$this->staff= Staff::lookup($this->getStaffId());
return $this->staff;
}
Peter Rotich
committed
function getTeamId() {
return $this->ht['team_id'];
Peter Rotich
committed
function getTeam() {
if(!$this->team && $this->getTeamId())
$this->team = Team::lookup($this->getTeamId());
return $this->team;
}
function getAssignee() {
if($staff=$this->getStaff())
return $staff->getName();
if($team=$this->getTeam())
return $team->getName();
return '';
}
Peter Rotich
committed
Peter Rotich
committed
function getAssigned($glue='/') {
$assignees = $this->getAssignees();
return $assignees?implode($glue, $assignees):'';
}
Peter Rotich
committed
return $this->ht['topic_id'];
Peter Rotich
committed
function getTopic() {
$this->topic = Topic::lookup($this->getTopicId());
Peter Rotich
committed
Peter Rotich
committed
return $this->ht['sla_id'];
}
function getSLA() {
if(!$this->sla && $this->getSLAId())
return $this->sla;
}
function getLastRespondent() {
$sql ='SELECT resp.staff_id '
.' FROM '.THREAD_ENTRY_TABLE.' resp '
.' LEFT JOIN '.THREAD_TABLE.' t ON( t.id=resp.thread_id) '
.' LEFT JOIN '.STAFF_TABLE. ' s ON(s.staff_id=resp.staff_id) '
.' WHERE t.object_id='.db_input($this->getId())
.' AND t.object_type="T" AND resp.staff_id>0 AND resp.`type`="R" '
.' ORDER BY resp.created DESC LIMIT 1';
if(!($res=db_query($sql)) || !db_num_rows($res))
return null;
list($id)=db_fetch_row($res);
Peter Rotich
committed
Peter Rotich
committed
return $this->ht['lastmessage'];
}
function getLastMsgDate() {
return $this->getLastMessageDate();
}
function getLastResponseDate() {
Peter Rotich
committed
return $this->ht['lastresponse'];
}
function getLastRespDate() {
return $this->getLastResponseDate();
}
Peter Rotich
committed
function getLastMsgId() {
return $this->lastMsgId;
}
function getLastMessage() {
if (!isset($this->last_message)) {
if ($this->getLastMsgId())
$this->last_message = MessageThreadEntry::lookup(
$this->getLastMsgId(), $this->getThreadId());
$this->last_message = $this->getThread()->getLastMessage();
function getNumTasks() {
return $this->ht['tasks'];
}
function getThreadId() {
return $this->ht['thread_id'];
}
function getThread() {
if (!$this->thread && $this->getThreadId())
$this->thread = TicketThread::lookup($this->getThreadId());
return $this->thread;
return $this->getClientThread()->count();
return $this->getThread()->getNumMessages();
return $this->getThread()->getNumResponses();
return $this->getThread()->getNumNotes();
return $this->getThreadEntries(array('M'));
function getResponses() {
return $this->getThreadEntries(array('R'));
return $this->getThreadEntries(array('N'));
return $this->getThreadEntries(array('M', 'R'));
function getThreadEntry($id) {
return $this->getThread()->getEntry($id);
function getThreadEntries($type=false) {
if ($type && is_array($type))
$entries->filter(array('type__in' => $type));
return $entries;
//UserList of recipients (owner + collaborators)
function getRecipients() {
if (!isset($this->recipients)) {
$list = new UserList();
$list->add($this->getOwner());
if ($collabs = $this->getActiveCollaborators()) {
foreach ($collabs as $c)
$list->add($c);
}
$this->recipients = $list;
}
return $this->recipients;
}
function hasClientEditableFields() {
$forms = DynamicFormEntry::forTicket($this->getId());
foreach ($forms as $form) {
foreach ($form->getFields() as $field) {
if ($field->isEditableToUsers())
return true;
}
}
}
function getMissingRequiredFields() {
$returnArray = array();
$forms=DynamicFormEntry::forTicket($this->getId());
foreach ($forms as $form) {
foreach ($form->getFields() as $field) {
if ($field->isRequiredForClose()) {
if (!($field->answer->get('value'))) {
array_push($returnArray, $field->get('label'));
}
}
}
}
return $returnArray;
}
function getMissingRequiredField() {
$fields = $this->getMissingRequiredFields();
return $fields[0];
}
function addCollaborator($user, $vars, &$errors) {
if (!$user || $user->getId()==$this->getOwnerId())
$vars = array_merge(array(
'threadId' => $this->getThreadId(),
'userId' => $user->getId()), $vars);
if (!($c=Collaborator::add($vars, $errors)))
return null;
$this->collaborators = null;
return $c;
}
//XXX: Ugly for now
function updateCollaborators($vars, &$errors) {
global $thisstaff;
if (!$thisstaff) return;
//Deletes
if($vars['del'] && ($ids=array_filter($vars['del']))) {
$collabs = array();
foreach ($ids as $k => $cid) {
if (($c=Collaborator::lookup($cid))
&& $c->getTicketId() == $this->getId()
$this->logNote(_S('Collaborators Removed'),
implode("<br>", $collabs), $thisstaff, false);
}
//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(),
'isactive' => 1,
));
if ($cids) {
$this->getThread()->collaborators->filter(array(
'thread_id' => $this->getThreadId(),
Q::not(array('id__in' => $cids))
))->update(array(
'updated' => SqlFunction::NOW(),
'isactive' => 0,
));
}
unset($this->ht['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;
}
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
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,
);
$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 --------------------- */
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!
Peter Rotich
committed
function setDeptId($deptId) {
if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId())
Peter Rotich
committed
$sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), dept_id='.db_input($deptId)
.' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
Peter Rotich
committed
//Set staff ID...assign/unassign/release (id can be 0)
function setStaffId($staffId) {
if(!is_numeric($staffId)) return false;
Peter Rotich
committed
$sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId)
.' WHERE ticket_id='.db_input($this->getId());
if (!db_query($sql) || !db_affected_rows())
return false;
Peter Rotich
committed
$this->staff = null;
$this->ht['staff_id'] = $staffId;
}
function setSLAId($slaId) {
if ($slaId == $this->getSLAId()) return true;
'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId)
.' WHERE ticket_id='.db_input($this->getId()))
&& db_affected_rows();
if ($rv) {
$this->ht['sla_id'] = $slaId;
$this->sla = null;
}
return $rv;
}
/**
* 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)) {
} elseif ($this->getDept() && $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)
Peter Rotich
committed
Peter Rotich
committed
$sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId)
.' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true) {
if ($thisstaff && !($role=$thisstaff->getRole($this->getDeptId())))
if ($status && is_numeric($status))
$status = TicketStatus::lookup($status);
if (!$status || !$status instanceof TicketStatus)
return false;
// Double check permissions (when changing status)
if ($role && $this->getStatusId()) {
switch ($status->getState()) {
case 'closed':
if (!($role->hasPerm(TicketModel::PERM_CLOSE)))
return false;
break;
case 'deleted':
// XXX: intercept deleted status and do hard delete
if ($role->hasPerm(TicketModel::PERM_DELETE))
return $this->delete($comments);
// Agent doesn't have permission to delete tickets
if ($this->getStatusId() == $status->getId())
return true;
$sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
' ,status_id='.db_input($status->getId());
$ecb = null;
switch($status->getState()) {
if ($this->getMissingRequiredFields()) {
$errors['err'] = sprintf(__(
'This ticket is missing data on %s one or more required fields %s and cannot be closed'),
'', '');
return false;
}
$sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL ';
$sql.=', staff_id='.db_input($thisstaff->getId());
Anthony Lawrence
committed
$this->clearOverdue();
$ecb = function($t) {
$t->reload();
$t->logEvent('closed');
$t->deleteDrafts();
};
break;
case 'open':
// TODO: check current status if it allows for reopening
$sql .= ',closed=NULL, lastupdate=NOW(), reopened=NOW() ';
$ecb = function ($t) {
$t->logEvent('reopened', 'closed');
};
}
// If the ticket is not open then clear answered flag
if (!$this->isOpen())
$sql .= ', isanswered = 0 ';
default:
return false;
$sql.=' WHERE ticket_id='.db_input($this->getId());
if (!db_query($sql) || !db_affected_rows())
return false;
// Log status change b4 reload — if currently has a status. (On new
// ticket, the ticket is opened and thereafter the status is set to
// the requested status).
if ($current_status = $this->getStatus()) {
$note = sprintf(__('Status changed from %1$s to %2$s by %3$s'),
$this->getStatus(),
$status,
$thisstaff ?: 'SYSTEM');
$alert = false;
if ($comments) {
$note .= sprintf('<hr>%s', $comments);
// Send out alerts if comments are included
$alert = true;
}
$this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
// Log events via callback
if ($ecb) $ecb($this);
return true;
}
function setState($state, $alerts=false) {
switch(strtolower($state)) {
case 'open':
return $this->setStatus('open');
break;
case 'closed':
return $this->setStatus('closed');
break;
case 'answered':
return $this->setAnsweredState(1);
break;
case 'unanswered':
return $this->setAnsweredState(0);
break;
case 'overdue':
return $this->markOverdue();
break;
case 'notdue':
return $this->clearOverdue();
break;
case 'unassined':
return $this->unassign();
}
return false;
}
function setAnsweredState($isanswered) {
$sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered)
.' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
if (!$this->isClosed())
return false;
// Set status to open based on current closed status settings
// If the closed status doesn't have configured "reopen" status then use the
// the default ticket status.
if (!($status=$this->getStatus()->getReopenStatus()))
$status = $cfg->getDefaultTicketStatusId();
return $status ? $this->setStatus($status, 'Reopened') : false;
}
function onNewTicket($message, $autorespond=true, $alertstaff=true) {
global $cfg;
//Log stuff here...
Peter Rotich
committed
if(!$autorespond && !$alertstaff) return true; //No alerts to send.
/* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/
Peter Rotich
committed
if(!$cfg
|| !($dept=$this->getDept())
|| !($tpl = $dept->getTemplate())
|| !($email=$dept->getAutoRespEmail())) {
return false; //bail out...missing stuff.
}
$options = array();
if ($message instanceof ThreadEntry) {
$options += array(
'inreplyto'=>$message->getEmailMessageId(),
'references'=>$message->getEmailReferences(),
'thread'=>$message
);
}
else {
$options += array(
'thread' => $this->getThread(),
);
}
if($autorespond
&& $cfg->autoRespONNewTicket()
Peter Rotich
committed
&& $dept->autoRespONNewTicket()
Peter Rotich
committed
$msg = $this->replaceVars($msg->asArray(),
'recipient' => $this->getOwner(),
'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
);
$email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'],
Peter Rotich
committed
Peter Rotich
committed
&& $cfg->alertONNewTicket()
&& ($email=$dept->getAlertEmail())
&& ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
Peter Rotich
committed
$msg = $this->replaceVars($msg->asArray(), array('message' => $message));
//Exclude the auto responding email just incase it's from staff member.
if ($message instanceof ThreadEntry && $message->isAutoReply())
$sentlist[] = $this->getEmail();
//Alert admin??
if($cfg->alertAdminONNewTicket()) {
$alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
$email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options);
Peter Rotich
committed
//Only alerts dept members if the ticket is NOT assigned.
if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) {
if(($members=$dept->getMembersForAlerts()))
$recipients=array_merge($recipients, $members);
}
Peter Rotich
committed
if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager()))
$recipients[]= $manager;
Peter Rotich
committed
// Account manager
if ($cfg->alertAcctManagerONNewMessage()
&& ($org = $this->getOwner()->getOrganization())
&& ($acct_manager = $org->getAccountManager())) {
if ($acct_manager instanceof Team)
$recipients = array_merge($recipients, $acct_manager->getMembers());
else
$recipients[] = $acct_manager;
}
Peter Rotich
committed
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);
Peter Rotich
committed
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;
if(($dept = $this->getDept())
&& ($tpl=$dept->getTemplate())
&& ($msg=$tpl->getOverlimitMsgTemplate())
&& ($email=$dept->getAutoRespEmail())) {
Peter Rotich
committed
$msg = $this->replaceVars($msg->asArray(),
array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
Peter Rotich
committed
$email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']);
$user = $this->getOwner();
Peter Rotich
committed
//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.");
Peter Rotich
committed
$ost->alertAdmin(__('Overlimit Notice'), $alert);
Peter Rotich
committed
db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1, lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId()));
$this->reload();
$vars = array_merge($options,
array(
'activity' => _S('New Response'),
'threadentry' => $response));
$this->onActivity($vars);
/*
* Notify collaborators on response or new message
*
*/
function notifyCollaborators($entry, $vars = array()) {
if (!$entry instanceof ThreadEntry
|| !($dept=$this->getDept())
|| !($tpl=$dept->getTemplate())
|| !($msg=$tpl->getActivityNoticeMsgTemplate())
|| !($email=$dept->getEmail()))
return;
//Who posted the entry?
$skip = array();
if ($entry instanceof Message) {
$poster = $entry->getUser();
// Skip the person who sent in the message
$skip[$entry->getUserId()] = 1;
// Skip all the other recipients of the message
foreach ($entry->getAllEmailRecipients() as $R) {
foreach ($recipients as $R2) {
if (0 === strcasecmp($R2->getEmail(), $R->mailbox.'@'.$R->host)) {
$skip[$R2->getUserId()] = true;
break;
}
}
}
$poster = $entry->getStaff();
// Skip the ticket owner
$skip[$this->getUserId()] = 1;
$vars = array_merge($vars, array(
'message' => (string) $entry,
'poster' => $poster ?: _S('A collaborator'),
)
);
$msg = $this->replaceVars($msg->asArray(), $vars);
$attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
$options = array('inreplyto' => $entry->getEmailMessageId(),
'thread' => $entry);
foreach ($recipients as $recipient) {
// Skip folks who have already been included on this part of
// the conversation
if (isset($skip[$recipient->getUserId()]))
continue;
$notice = $this->replaceVars($msg, array('recipient' => $recipient));
$email->send($recipient, $notice['subj'], $notice['body'], $attachments,
$options);
}
return;
function onMessage($message, $autorespond=true) {
db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastupdate=NOW(),lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
Peter Rotich
committed
// Auto-assign to closing staff or last respondent
// If the ticket is closed and auto-claim is not enabled then put the
// ticket back to unassigned pool.
if ($this->isClosed() && !$cfg->autoClaimTickets()) {
$this->setStaffId(0);
} elseif(!($staff=$this->getStaff()) || !$staff->isAvailable()) {
// Ticket has no assigned staff - if auto-claim is enabled then
// try assigning it to the last respondent (if available)
// otherwise leave the ticket unassigned.
if ($cfg->autoClaimTickets() //Auto claim is enabled.
&& ($lastrep=$this->getLastRespondent())
&& $lastrep->isAvailable()) {
$this->setStaffId($lastrep->getId()); //direct assignment;
} else {
$this->setStaffId(0); //unassign - last respondent is not available.
}
}
// Reopen if closed AND reopenable
// We're also checking autorespond flag because we don't want to
// reopen closed tickets on auto-reply from end user. This is not to
// confused with autorespond on new message setting
if ($autorespond && $this->isClosed() && $this->isReopenable())
// 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 ($autorespond && (Email::getIdByEmail($user->getEmail())))
elseif ($autorespond && ($dept=$this->getDept()))
$autorespond=$dept->autoRespONNewMessage();
if(!$autorespond
|| !$cfg->autoRespONNewMessage()
|| !$message) return; //no autoresp or alerts.
$dept = $this->getDept();
$email = $dept->getAutoRespEmail();
Peter Rotich
committed
//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():''));
'inreplyto'=>$message->getEmailMessageId(),
'thread'=>$message);
$email->sendAutoReply($user, $msg['subj'], $msg['body'],
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
function onActivity($vars, $alert=true) {
global $cfg, $thisstaff;
//TODO: do some shit
if (!$alert // Check if alert is enabled
|| !$cfg->alertONNewActivity()
|| !($dept=$this->getDept())
|| !($email=$cfg->getAlertEmail())
|| !($tpl = $dept->getTemplate())
|| !($msg=$tpl->getNoteAlertMsgTemplate()))
return;
// Alert recipients
$recipients=array();
//Last respondent.
if ($cfg->alertLastRespondentONNewActivity())
$recipients[] = $this->getLastRespondent();
// Assigned staff / team
if ($cfg->alertAssignedONNewActivity()) {
if (isset($vars['assignee'])
&& $vars['assignee'] instanceof Staff)
$recipients[] = $vars['assignee'];
elseif ($this->isOpen() && ($assignee = $this->getStaff()))
$recipients[] = $assignee;
if ($team = $this->getTeam())
$recipients = array_merge($recipients, $team->getMembers());
}
// Dept manager
if ($cfg->alertDeptManagerONNewActivity() && $dept && $dept->getManagerId())
$recipients[] = $dept->getManager();
$options = array();
$staffId = $thisstaff ? $thisstaff->getId() : 0;
if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) {
$options = array(
'inreplyto' => $vars['threadentry']->getEmailMessageId(),
'references' => $vars['threadentry']->getEmailReferences(),
'thread' => $vars['threadentry']);
// Activity details
if (!$vars['comments'])
$vars['comments'] = $vars['threadentry'];
// Staff doing the activity
$staffId = $vars['threadentry']->getStaffId() ?: $staffId;
}
$msg = $this->replaceVars($msg->asArray(),
array(
'note' => $vars['threadentry'], // For compatibility
'activity' => $vars['activity'],
'comments' => $vars['comments']));
$isClosed = $this->isClosed();
$sentlist=array();
foreach ($recipients as $k=>$staff) {
if (!is_object($staff)
// Don't bother vacationing staff.
|| !$staff->isAvailable()
// No need to alert the poster!
|| $staffId == $staff->getId()
// No duplicates.
|| isset($sentlist[$staff->getEmail()])
// Make sure staff has access to ticket
|| ($isClosed && !$this->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) {
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;
$comments = $comments ?: _S('Ticket assignment');
$assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');
Peter Rotich
committed
//Log an internal note - no alerts on the internal note.
$note = $this->logNote(
sprintf(_S('Ticket Assigned to %s'), $assignee->getName()),
$comments, $assigner, false);
//See if we need to send alerts
if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts!
$dept = $this->getDept();
if(!$dept
|| !($tpl = $dept->getTemplate())
|| !($email = $dept->getAlertEmail()))
//recipients
$recipients=array();
if ($assignee instanceof Staff) {
if ($cfg->alertStaffONAssignment())
} elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) {
if ($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembers()))
$recipients = array_merge($recipients, $members);
elseif ($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead()))
if ($recipients
&& ($msg=$tpl->getAssignedAlertMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments,
'assignee' => $assignee,
'assigner' => $assigner
'inreplyto'=>$note->getEmailMessageId(),
'references'=>$note->getEmailReferences(),
'thread'=>$note);
Peter Rotich
committed
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);
function onOverdue($whine=true, $comments="") {
if($whine && ($sla=$this->getSLA()) && !$sla->alertOnOverdue())
$whine = false;
if(!$whine
|| !$cfg->alertONOverdueTicket()
|| !($dept = $this->getDept()))
return true;
//Get the message template
if(($tpl = $dept->getTemplate())
&& ($msg=$tpl->getOverdueAlertMsgTemplate())
&& ($email = $dept->getAlertEmail())) {
Peter Rotich
committed
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments));
//recipients
$recipients=array();
//Assigned staff or team... if any
if($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
if($this->getStaffId())
$recipients[]=$this->getStaff();
elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
$recipients=array_merge($recipients, $members);
} elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
//Only alerts dept members if the ticket is NOT assigned.
if ($members = $dept->getMembersForAlerts())
$recipients = array_merge($recipients, $members);
}
//Always alert dept manager??
if($cfg->alertDeptManagerONOverdueTicket() && $dept && ($manager=$dept->getManager()))
$recipients[]= $manager;
$sentlist=array();
Peter Rotich
committed
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);
Peter Rotich
committed
Peter Rotich
committed
// TemplateVariable interface
function asVar() {
return $this->getNumber();
if($tag && is_callable(array($this, 'get'.ucfirst($tag))))
return call_user_func(array($this, 'get'.ucfirst($tag)));
switch(mb_strtolower($tag)) {
case 'phone_number':
return $this->getPhoneNumber();
break;
case 'auth_token':
return $this->getOldAuthToken();
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());
if ($due = $this->getEstDueDate())
return new FormattedDate($due);
if ($this->isClosed())
return new FormattedDate($this->getCloseDate());
case 'last_update':
return new FormattedDate($this->last_update);
case 'user':
return $this->getOwner();
default:
if (isset($this->_answers[$tag]))
// The answer object is retrieved here which will
// automatically invoke the toString() method when the
// answer is coerced into text
return $this->_answers[$tag];
static function getVarScope() {
$base = array(
'assigned' => __('Assigned agent and/or team'),
'class' => 'FormattedDate', 'desc' => __('Date Closed'),
),
'create_date' => array(
'class' => 'FormattedDate', 'desc' => __('Date created'),
'class' => 'Dept', 'desc' => __('Department'),
'class' => 'FormattedDate', 'desc' => __('Due Date'),
'email' => __('Default email address of ticket owner'),
'name' => array(
'class' => 'PersonsName', 'desc' => __('Name of ticket owner'),
),
'number' => __('Ticket number'),
'phone' => __('Phone number of ticket owner'),
'class' => 'Priority', 'desc' => __('Priority'),
),
'recipients' => array(
'class' => 'UserList', 'desc' => __('List of all recipient names'),
'source' => __('Source'),
'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'),
'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;
}
//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) {
Peter Rotich
committed
Peter Rotich
committed
if($this->isOverdue())
return true;
$sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1, updated=NOW() '
.' WHERE ticket_id='.db_input($this->getId());
if(!db_query($sql) || !db_affected_rows())
return false;
$this->logEvent('overdue');
Peter Rotich
committed
if(!$this->isOverdue())
Peter Rotich
committed
//NOTE: Previously logged overdue event is NOT annuled.
$sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() ';
Peter Rotich
committed
if($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime())
Peter Rotich
committed
//Clear SLA if est. due date is in the past
if($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
Peter Rotich
committed
$sql.=', sla_id=0 ';
$sql.=' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
Peter Rotich
committed
//Dept Tranfer...with alert.. done by staff
function transfer($deptId, $comments, $alert = true) {
Peter Rotich
committed
Peter Rotich
committed
if (!$this->checkStaffPerm($thisstaff, TicketModel::PERM_TRANSFER))
$currentDept = $this->getDeptName(); //Current department
if(!$deptId || !$this->setDeptId($deptId))
return false;
Peter Rotich
committed
// Reopen ticket if closed
if($this->isClosed()) $this->reopen();
$this->reload();
// Set SLA of the new department
if(!$this->getSLAId() || $this->getSLA()->isTransient())
$this->selectSLAId();
Peter Rotich
committed
// Make sure the new department allows assignment to the
// currently assigned agent (if any)
if ($this->isAssigned()
&& ($staff=$this->getStaff())
&& $dept->assignMembersOnly()
&& !$dept->isMember($staff)) {
$this->setStaffId(0);
}
/*** log the transfer comments as internal note - with alerts disabled - ***/
$title=sprintf(_S('Ticket transferred from %1$s to %2$s'),
$currentDept, $this->getDeptName());
Peter Rotich
committed
$comments=$comments?$comments:$title;
$note = $this->logNote($title, $comments, $thisstaff, false);
Peter Rotich
committed
return true; //no alerts!!
if (($email = $dept->getAlertEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg=$tpl->getTransferAlertMsgTemplate())) {
Peter Rotich
committed
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments, 'staff' => $thisstaff));
Peter Rotich
committed
//recipients
$recipients=array();
//Assigned staff or team... if any
if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
if($this->getStaffId())
$recipients[]=$this->getStaff();
elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
$recipients = array_merge($recipients, $members);
} elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
//Only alerts dept members if the ticket is NOT assigned.
if(($members=$dept->getMembersForAlerts()))
$recipients = array_merge($recipients, $members);
}
//Always alert dept manager??
if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()))
$recipients[]= $manager;
Peter Rotich
committed
'inreplyto'=>$note->getEmailMessageId(),
'references'=>$note->getEmailReferences(),
'thread'=>$note);
Peter Rotich
committed
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);
function claim() {
global $thisstaff;
if (!$thisstaff || !$this->isOpen() || $this->isAssigned())
return false;
$dept = $this->getDept();
if ($dept->assignMembersOnly() && !$dept->isMember($thisstaff))
return false;
$comments = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName());
return $this->assignToStaff($thisstaff->getId(), $comments, false);
}
function assignToStaff($staff, $note, $alert=true) {
if(!is_object($staff) && !($staff=Staff::lookup($staff)))
return false;
Peter Rotich
committed
if (!$staff->isAvailable() || !$this->setStaffId($staff->getId()))
$this->logEvent('assigned');
return true;
}
function assignToTeam($team, $note, $alert=true) {
if(!is_object($team) && !($team=Team::lookup($team)))
return false;
if (!$team->isActive() || !$this->setTeamId($team->getId()))
return false;
//Clear - staff if it's a closed ticket
// staff_id is overloaded -> assigned to & closed by.
if($this->isClosed())
$this->setStaffId(0);
$this->logEvent('assigned');
return true;
}
//Assign ticket to staff or team - overloaded ID.
function assign($assignId, $note, $alert=true) {
global $thisstaff;
$rv=0;
Peter Rotich
committed
$id=preg_replace("/[^0-9]/", "", $assignId);
if($assignId[0]=='t') {
$rv=$this->assignToTeam($id, $note, $alert);
} elseif($assignId[0]=='s' || is_numeric($assignId)) {
$alert=($alert && $thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!!
//We don't care if a team is already assigned to the ticket - staff assignment takes precedence
$rv=$this->assignToStaff($id, $note, $alert);
}
return $rv;
}
Peter Rotich
committed
//unassign primary assignee
function unassign() {
if(!$this->isAssigned()) //We can't release what is not assigned buddy!
return true;
//We can only unassigned OPEN tickets.
if($this->isClosed())
return false;
//Unassign staff (if any)
if($this->getStaffId() && !$this->setStaffId(0))
return false;
//unassign team (if any)
if($this->getTeamId() && !$this->setTeamId(0))
return false;
$this->reload();
return true;
}
Peter Rotich
committed
function release() {
return $this->unassign();
}
//Change ownership
function changeOwner($user) {
global $thisstaff;
if (!$user
|| ($user->getId() == $this->getOwnerId())
|| !($this->checkStaffPerm($thisstaff,
TicketModel::PERM_EDIT)))
return false;
$sql ='UPDATE '.TICKET_TABLE.' SET updated = NOW() '
.', user_id = '.db_input($user->getId())
.' WHERE ticket_id = '.db_input($this->getId());
if (!db_query($sql) || !db_affected_rows())
return false;
$this->ht['user_id'] = $user->getId();
$this->user = null;
$this->collaborators = null;
$this->recipients = null;
//Log an internal note
$note = sprintf(_S('%s changed ticket ownership to %s'),
$thisstaff->getName(), $user->getName());
//Remove the new owner from list of collaborators
$c = Collaborator::lookup(array(
'user_id' => $user->getId(),
'thread_id' => $this->getThreadId()));
if ($c && $c->delete())
$note.= ' '._S('(removed as collaborator)');
$this->logNote('Ticket ownership changed', $note);
return true;
}
function postMessage($vars, $origin='', $alerts=true) {
if ($origin)
$vars['origin'] = $origin;
$vars['ip_address'] = $vars['ip'];
elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
$vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
$errors = array();
if(!($message = $this->getThread()->addMessage($vars, $errors)))
return null;
$this->setLastMessage($message);
//Add email recipients as collaborators...
if ($vars['recipients']
&& (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail()))
//Only add if we have a matched local address
&& $vars['to-email-id']) {
//New collaborators added by other collaborators are disable --
// requires staff approval.
$info = array(
'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0);
$collabs = array();
foreach ($vars['recipients'] as $recipient) {
// Skip virtual delivered-to addresses
if (strcasecmp($recipient['source'], 'delivered-to') === 0)
continue;
if (($user=User::fromVars($recipient)))
if ($c=$this->addCollaborator($user, $info, $errors))
(string) $c,
$recipient['source']
? " ".sprintf(_S('via %s'), $recipient['source'])
: ''
);
}
//TODO: Can collaborators add others?
if ($collabs) {
//TODO: Change EndUser to name of user.
$this->logNote(_S('Collaborators added by end user'),
implode("<br>", $collabs), _S('End User'), false);
Loading
Loading full blame...