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.flags & 1 = 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->getThread()->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;
$this->logEvent('collab', array('add' => array($c->toString())));
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->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(),
'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>