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');
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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(
'reverse' => 'TicketLock.ticket',
'list' => false,
'null' => true,
),
'dept' => array(
'constraint' => array('dept_id' => 'Dept.dept_id'),
),
'staff' => array(
'constraint' => array('staff_id' => 'StaffModel.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,
),
'cdata' => array(
'reverse' => 'TicketCData.ticket',
'list' => false,
),
)
);
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;
}
}
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';
class Ticket {
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
function load($id=0) {
if(!$id && !($id=$this->getId()))
return false;
.' ,IF(sla.id IS NULL, NULL, '
.'DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate '
.' ,count(distinct attach.attach_id) as attachments'
.' FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '
.' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
.' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock
ON ( ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) '
.' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
ON ( ticket.ticket_id=attach.ticket_id) '
.' WHERE ticket.ticket_id='.db_input($id)
.' GROUP BY ticket.ticket_id';
//echo $sql;
if(!($res=db_query($sql)) || !db_num_rows($res))
return false;
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->getField()->get('name'))
?: 'field.' . $answer->getField()->get('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 checkStaffAccess($staff) {
if(!is_object($staff) && !($staff=Staff::lookup($staff)))
return false;
// Staff has access to the department.
if (!$staff->showAssignedOnly()
&& $staff->canAccessDept($this->getDeptId()))
return true;
// Only consider assignment if the ticket is open
if (!$this->isOpen())
return false;
// Check ticket access based on direct or team assignment
if ($staff->getId() == $this->getStaffId()
|| ($this->getTeamId()
&& $staff->isTeamMember($this->getTeamId())
))
return true;
// No access bro!
return false;
function checkUserAccess($user) {
if (!$user || !($user instanceof EndUser))
//Ticket Owner
if ($user->getId() == $this->getUserId())
//Collaborator?
// 1) If the user was authorized via this ticket.
if ($user->getTicketId() == $this->getId()
&& !strcasecmp($user->getRole(), 'collaborator'))
return true;
// 2) Query the database to check for expanded access...
if (Collaborator::lookup(array(
'userId' => $user->getId(),
'ticketId' => $this->getId())))
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();
function getAuthToken() {
# 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'];
Peter Rotich
committed
function getDueDate() {
return $this->ht['duedate'];
function getSLADueDate() {
return $this->ht['sla_duedate'];
}
function getEstDueDate() {
//Real due date
if(($duedate=$this->getDueDate()))
return $duedate;
//return sla due date (If ANY)
return $this->getSLADueDate();
}
Peter Rotich
committed
function getCloseDate() {
return $this->ht['closed'];
function getStatusId() {
return $this->ht['status_id'];
}
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->getName();
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() {
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 = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock..
//load and return the newly created lock if any!
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 '.TICKET_THREAD_TABLE.' resp '
.' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) '
.' WHERE resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 '
.' AND resp.thread_type="R"'
.' ORDER BY resp.created DESC LIMIT 1';
if(!($res=db_query($sql)) || !db_num_rows($res))
return null;
Peter Rotich
committed
list($id)=db_fetch_row($res);
return Staff::lookup($id);
}
function getLastMessageDate() {
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 = Message::lookup(
$this->getLastMsgId(), $this->getId());
if (!$this->last_message)
$this->last_message = Message::lastByTicketId($this->getId());
}
return $this->last_message;
}
function getThread() {
if(!$this->thread)
$this->thread = Thread::lookup($this);
return $this->thread;
}
function getThreadCount() {
return $this->getNumMessages() + $this->getNumResponses();
}
function getNumMessages() {
return $this->getThread()->getNumMessages();
return $this->getThread()->getNumResponses();
return $this->getThread()->getNumNotes();
return $this->getThreadEntries('M');
function getResponses() {
return $this->getThreadEntries('R');
return $this->getThreadEntries('N');
return $this->getThreadEntries(array('M', 'R'));
function getThreadEntry($id) {
return $this->getThread()->getEntry($id);
function getThreadEntries($type, $order='') {
return $this->getThread()->getEntries($type, $order);
//Collaborators
function getNumCollaborators() {
function getNumActiveCollaborators() {
if (!isset($this->ht['active_collaborators']))
$this->ht['active_collaborators'] = count($this->getActiveCollaborators());
return $this->ht['active_collaborators'];
}
function getActiveCollaborators() {
return $this->getCollaborators(array('isactive'=>1));
}
function getCollaborators($criteria=array()) {
return Collaborator::forTicket($this->getId(), $criteria);
$this->collaborators = Collaborator::forTicket($this->getId());
return $this->collaborators;
}
//UserList of recipients (owner + collaborators)
function getRecipients() {
if (!isset($this->recipients)) {
$list = new UserList();
$list->add($this->getOwner());
if ($collabs = $this->getActiveCollaborators()) {
foreach ($collabs as $c)
$list->add($c);
}
$this->recipients = $list;
}
return $this->recipients;
}
function addCollaborator($user, $vars, &$errors) {
if (!$user || $user->getId()==$this->getOwnerId())
$vars = array_merge(array(
'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()
&& $c->remove())
$this->logNote(_S('Collaborators Removed'),
implode("<br>", $collabs), $thisstaff, false);
}
//statuses
$cids = null;
if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
$sql='UPDATE '.TICKET_COLLABORATOR_TABLE
.' SET updated=NOW(), isactive=1 '
.' WHERE ticket_id='.db_input($this->getId())
.' AND id IN('.implode(',', db_input($cids)).')';
db_query($sql);
}
$sql='UPDATE '.TICKET_COLLABORATOR_TABLE
.' SET updated=NOW(), isactive=0 '
.' WHERE ticket_id='.db_input($this->getId());
if($cids)
$sql.=' AND id NOT IN('.implode(',', db_input($cids)).')';
db_query($sql);
unset($this->ht['active_collaborators']);
$this->collaborators = null;
return true;
}
/* -------------------- Setters --------------------- */
function setLastMsgId($msgid) {
return $this->lastMsgId=$msgid;
}
function setLastMessage($message) {
$this->last_message = $message;
$this->setLastMsgId($message->getId());
}
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;
return db_query(
'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId)
.' WHERE ticket_id='.db_input($this->getId()))
&& db_affected_rows();
}
/**
* Selects the appropriate service-level-agreement plan for this ticket.
* When tickets are transfered between departments, the SLA of the new
* department should be applied to the ticket. This would be useful,
* for instance, if the ticket is transferred to a different department
* which has a shorter grace period, the ticket should be considered
* overdue in the shorter window now that it is owned by the new
* department.
*
* $trump - if received, should trump any other possible SLA source.
* This is used in the case of email filters, where the SLA
* specified in the filter should trump any other SLA to be
* considered.
*/
function selectSLAId($trump=null) {
global $cfg;
# XXX Should the SLA be overridden if it was originally set via an
# email filter? This method doesn't consider such a case
if ($trump && is_numeric($trump)) {
} 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());
function setStatus($status, $comments='') {
if ($status && is_numeric($status))
$status = TicketStatus::lookup($status);
if (!$status || !$status instanceof TicketStatus)
return false;
// XXX: intercept deleted status and do hard delete
if (!strcasecmp($status->getState(), 'deleted'))
return $this->delete($comments);
if ($this->getStatusId() == $status->getId())
return true;
$sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
' ,status_id='.db_input($status->getId());
$ecb = null;
switch($status->getState()) {
$sql.=', closed=NOW(), duedate=NULL ';
if ($thisstaff)
$sql.=', staff_id='.db_input($thisstaff->getId());
$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, 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
$note = sprintf(__('Status changed from %s to %s by %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();