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(
'reverse' => 'TicketLock.ticket',
'list' => false,
'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,
),
'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;
}
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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]);
}
}
}
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;
.' ,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() {
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'];
}
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 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(
'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;
'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());
function setStatus($status, $comments='', &$errors=array()) {
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()) {