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 Ticket extends VerySimpleModel
implements RestrictedAccess, Threadable, Searchable {
static $meta = array(
'table' => TICKET_TABLE,
'pk' => array('ticket_id'),
'select_related' => array('topic', 'staff', 'user', 'team', 'dept',
'sla', 'thread', 'user__default_email', 'status'),
'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'),
'constraint' => array('sla_id' => 'Sla.id'),
'constraint' => array('staff_id' => 'Staff.staff_id'),
'tasks' => array(
'reverse' => 'Task.ticket',
),
'team' => array(
'constraint' => array('team_id' => 'Team.team_id'),
'null' => true,
),
'topic' => array(
'constraint' => array('topic_id' => 'Topic.topic_id'),
'null' => true,
),
'thread' => array(
'list' => false,
'null' => true,
),
'cdata' => array(
'reverse' => 'TicketCData.ticket',
'list' => false,
),
'entries' => array(
'constraint' => array(
"'T'" => 'DynamicFormEntry.object_type',
'ticket_id' => 'DynamicFormEntry.object_id',
),
const PERM_CREATE = 'ticket.create';
const PERM_EDIT = 'ticket.edit';
const PERM_ASSIGN = 'ticket.assign';
const PERM_TRANSFER = 'ticket.transfer';
const PERM_REFER = 'ticket.refer';
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_RELEASE => array(
'title' =>
/* @trans */ 'Release',
'desc' =>
/* @trans */ 'Ability to release ticket assignment'),
/* @trans */ 'Ability to transfer tickets between departments'),
self::PERM_REFER => array(
'title' =>
/* @trans */ 'Refer',
'desc' =>
/* @trans */ 'Ability to manage ticket referrals'),
/* @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'),
);
// Ticket Sources
static protected $sources = array(
'Phone' =>
/* @trans */ 'Phone',
'Email' =>
/* @trans */ 'Email',
'Web' =>
/* @trans */ 'Web',
'API' =>
/* @trans */ 'API',
Peter Rotich
committed
var $owner; // TicketOwner
var $_user; // EndUser
var $_answers;
var $collaborators;
var $active_collaborators;
var $recipients;
var $lastrespondent;
var $lastuserrespondent;
Peter Rotich
committed
function loadDynamicData($force=false) {
if (!isset($this->_answers) || $force) {
foreach (DynamicFormEntryAnswer::objects()
->filter(array(
'entry__object_id' => $this->getId(),
'entry__object_type' => 'T'
)) as $answer
) {
$tag = mb_strtolower($answer->field->name)
?: 'field.' . $answer->field->id;
$this->_answers[$tag] = $answer;
function getAnswer($field, $form=null) {
// TODO: Prefer CDATA ORM relationship if already loaded
$this->loadDynamicData();
return $this->_answers[$field];
}
function getId() {
return $this->ticket_id;
}
return strcasecmp($this->getState(), $state) == 0;
return ($this->getStatus()->isReopenable() && $this->getDept()->allowsReopen()
&& ($this->getTopic() ? $this->getTopic()->allowsReopen() : true));
return $this->hasState('closed');
}
if ($this->isClosed())
return true;
$warning = null;
if (self::getMissingRequiredFields($this)) {
$warning = sprintf(
__( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
__('This ticket'),
'', '');
} elseif (($num=$this->getNumOpenTasks())) {
$warning = sprintf(__('%1$s has %2$d open tasks and cannot be closed'),
__('This ticket'), $num);
} elseif ($cfg->requireTopicToClose() && !$this->getTopicId()) {
$warning = sprintf(
__( '%1$s is missing a %2$s and cannot be closed'),
__('This ticket'), __('Help Topic'), '');
}
return $warning ?: true;
}
function isArchived() {
return $this->hasState('archived');
}
function isDeleted() {
return $this->hasState('deleted');
function isAssigned($to=null) {
if (!$this->isOpen())
return false;
return ($this->getStaffId() || $this->getTeamId());
switch (true) {
case $to instanceof Staff:
return ($to->getId() == $this->getStaffId() ||
$to->isTeamMember($this->getTeamId()));
break;
case $to instanceof Team:
return ($to->getId() == $this->getTeamId());
break;
}
return false;
Peter Rotich
committed
function getRole($staff) {
if (!$staff instanceof Staff)
return null;
return $staff->getRole($this->getDept(), $this->isAssigned($staff));
}
function checkStaffPerm($staff, $perm=null) {
if ((!$staff instanceof Staff) && !($staff=Staff::lookup($staff)))
// check department access first
if (!$staff->canAccessDept($this->getDept())
// check assignment
&& !$this->isAssigned($staff)
// check referral
&& !$this->thread->isReferred($staff))
// At this point staff has view access unless a specific permission is
// requested
if ($perm === null)
// Permission check requested -- get role if any
if (!($role=$this->getRole($staff)))
// Check permission based on the effective role
return $role->hasPerm($perm);
function checkUserAccess($user) {
if (!$user || !($user instanceof EndUser))
if ($user->getId() == $this->getUserId())
// Organization
if ($user->canSeeOrgTickets()
&& ($U = $this->getUser())
&& ($U->getOrgId() == $user->getOrgId())
) {
// The owner of this ticket is in the same organization as the
// user in question, and the organization is configured to allow
// the user in question to see other tickets in the
// organization.
return true;
}
// 1) If the user was authorized via this ticket.
if ($user->getTicketId() == $this->getId()
&& !strcasecmp($user->getUserType(), 'collaborator')
) {
// 2) Query the database to check for expanded access...
if (Collaborator::lookup(array(
'user_id' => $user->getId(),
'thread_id' => $this->getThreadId()))
) {
function getNumber() {
Peter Rotich
committed
return $this->number;
Peter Rotich
committed
}
function getOwner() {
if (!isset($this->owner)) {
$this->owner = new TicketOwner(new EndUser($this->user), $this);
}
function getEmail() {
if ($o = $this->getOwner()) {
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->getAnswer('subject');
}
/* Help topic title - NOT object -> $topic */
function getHelpTopic() {
if ($this->topic)
return $this->topic->getFullName();
Peter Rotich
committed
function getCreateDate() {
}
function getOpenDate() {
return $this->getCreateDate();
}
function getReopenDate() {
Peter Rotich
committed
function getUpdateDate() {
Peter Rotich
committed
function getDueDate() {
$dt = new DateTime($datetime ?: $this->getReopenDate() ?: $this->getCreateDate());
return $dt
->add(new DateInterval('PT' . $sla->getGracePeriod() . 'H'))
->format('Y-m-d H:i:s');
}
function updateEstDueDate($clearOverdue=true) {
$DueDate = $this->getEstDueDate();
$this->est_duedate = $this->getSLADueDate();
// Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue.
if ($this->isOverdue()
&& $clearOverdue
&& (!$DueDate // Duedate + SLA cleared
|| Misc::db2gmtime($DueDate) > Misc::gmtime() //New due date in the future.
)) {
$this->isoverdue = 0;
}
return $this->save();
}
function getEstDueDate() {
// Real due date or sla due date (If ANY)
return $this->getDueDate() ?: $this->getSLADueDate();
Peter Rotich
committed
function getCloseDate() {
/**
* setStatusId
*
* Forceably set the ticket status ID to the received status ID. No
* checks are made. Use ::setStatus() to change the ticket status
*/
// XXX: Use ::setStatus to change the status. This can be used as a
// fallback if the logic in ::setStatus fails.
function setStatusId($id) {
$this->status_id = $id;
return $this->save();
Peter Rotich
committed
function getStatus() {
return $this->status;
}
function getState() {
Peter Rotich
committed
function getDeptId() {
Peter Rotich
committed
function getDeptName() {
if ($this->dept instanceof Dept)
return $this->dept->getFullName();
if (($priority = $this->getPriority()))
return $priority->getId();
return $cfg->getDefaultPriorityId();
Peter Rotich
committed
if (($a = $this->getAnswer('priority')))
return $a->getValue();
return null;
Peter Rotich
committed
return (string)$this->getOwner()->getPhoneNumber();
$sources = $this->getSources();
return $sources[$this->source] ?: $this->source;
Peter Rotich
committed
function getHashtable() {
return $this->ht;
}
function getUpdateInfo() {
'topicId' => $this->getTopicId(),
'slaId' => $this->getSLAId(),
'user_id' => $this->getOwnerId(),
'duedate' => Misc::db2gmtime($this->getDueDate()),
Peter Rotich
committed
function getLock() {
$lock = $this->lock;
if ($lock && !$lock->isExpired())
return $lock;
Peter Rotich
committed
function acquireLock($staffId, $lockTime=null) {
global $cfg;
if (!isset($lockTime))
$lockTime = $cfg->getLockTime();
Peter Rotich
committed
if (!$staffId or !$lockTime) //Lockig disabled?
// 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
// No lock on the ticket or it is expired
$this->lock = Lock::acquire($staffId, $lockTime); //Create a new lock..
// load and return the newly created lock if any!
return $this->lock;
Peter Rotich
committed
function releaseLock($staffId=false) {
if (!($lock = $this->getLock()))
return false;
if ($staffId && $lock->staff_id != $staffId)
return false;
$this->lock = null;
return $this->save();
Peter Rotich
committed
function getDept() {
Peter Rotich
committed
return $this->dept ?: $cfg->getDefaultDept();
function getUserId() {
return $this->getOwnerId();
}
if (!isset($this->_user) && $this->user) {
$this->_user = new EndUser($this->user);
}
return $this->_user;
Peter Rotich
committed
function getStaffId() {
Peter Rotich
committed
function getStaff() {
Peter Rotich
committed
function getTeamId() {
Peter Rotich
committed
function getTeam() {
if (!($assignee=$this->getAssignee()))
return null;
$id = '';
if ($assignee instanceof Staff)
$id = 's'.$assignee->getId();
elseif ($assignee instanceof Team)
$id = 't'.$assignee->getId();
return $id;
if (!$this->isOpen() || !$this->isAssigned())
return false;
Peter Rotich
committed
$assignees = array();
if ($staff = $this->getStaff())
Peter Rotich
committed
function getAssigned($glue='/') {
$assignees = $this->getAssignees();
return $assignees ? implode($glue, $assignees) : '';
Peter Rotich
committed
function getTopic() {
Peter Rotich
committed
}
function getSLA() {
return $this->sla;
}
function getLastRespondent() {
if (!$this->thread || !$this->thread->entries)
return $this->lastrespondent = false;
$this->lastrespondent = Staff::objects()
'staff_id' => $this->thread->entries
'type' => 'R',
'staff_id__gt' => 0,
->values_flat('staff_id')
->order_by('-id')
function getLastUserRespondent() {
if (!isset($this->$lastuserrespondent)) {
if (!$this->thread || !$this->thread->entries)
return $this->$lastuserrespondent = false;
$this->$lastuserrespondent = User::objects()
->filter(array(
'id' => $this->thread->entries
->filter(array(
'user_id__gt' => 0,
))
->values_flat('user_id')
->order_by('-id')
->limit(1)
))
->first()
?: false;
}
return $this->$lastuserrespondent;
}
return $this->thread->lastmessage;
}
function getLastMsgDate() {
return $this->getLastMessageDate();
}
function getLastResponseDate() {
return $this->thread->lastresponse;
}
function getLastRespDate() {
return $this->getLastResponseDate();
}
function getLastMsgId() {
return $this->lastMsgId;
}
function getLastMessage() {
if (!isset($this->last_message)) {
if ($this->getLastMsgId())
$this->last_message = MessageThreadEntry::lookup(
$this->getLastMsgId(), $this->getThreadId());
$this->last_message = $this->getThread()->getLastMessage();
}
return $this->last_message;
// FIXME: Implement this after merging Tasks
return count($this->tasks);
function getNumOpenTasks() {
return count($this->tasks->filter(array(
'flags__hasbit' => TaskModel::ISOPEN)));
}
if ($this->thread)
return $this->thread->id;
function getThread() {
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;
// MailingList of participants (owner + collaborators)
function getRecipients($who='all', $whitelist=array(), $active=true) {
$list = new MailingList();
switch (strtolower($who)) {
case 'user':
$list->addTo($this->getOwner());
break;
case 'all':
$list->addTo($this->getOwner());
// Fall-trough
case 'collabs':
if (($collabs = $active ? $this->getActiveCollaborators() :
$this->getCollaborators())) {
foreach ($collabs as $c)
if (!$whitelist || in_array($c->getUserId(),
$whitelist))
$list->addCc($c);
}
break;
default:
return null;
function getCollaborators() {
return $this->getThread()->getCollaborators();
}
function getNumCollaborators() {
return $this->getThread()->getNumCollaborators();
}
function getActiveCollaborators() {
return $this->getThread()->getActiveCollaborators();
}
function getNumActiveCollaborators() {
return $this->getThread()->getNumActiveCollaborators();
}
function getAssignmentForm($source=null, $options=array()) {
$prompt = $assignee = '';
// Possible assignees
$assignees = null;
switch (strtolower($options['target'])) {
case 'agents':
$assignees = array();
foreach ($dept->getAssignees() as $member)
$assignees['s'.$member->getId()] = $member;
if (!$source && $this->isOpen() && $this->staff)
$assignee = sprintf('s%d', $this->staff->getId());
$prompt = __('Select an Agent');
break;
case 'teams':
$assignees = array();
if (($teams = Team::getActiveTeams()))
foreach ($teams as $id => $name)
$assignees['t'.$id] = $name;
if (!$source && $this->isOpen() && $this->team)
$prompt = __('Select a Team');
break;
}
// Default to current assignee if source is not set
$source = array('assignee' => array($assignee));
$form = AssignmentForm::instantiate($source, $options);
if (isset($assignees))
$form->setAssignees($assignees);
if (($refer = $form->getField('refer'))) {
if ($assignee) {
$visibility = new VisibilityConstraint(
new Q(array()), VisibilityConstraint::HIDDEN);
$refer->set('visibility', $visibility);
} else {
$refer->configure('desc', sprintf(__('Maintain referral access to %s'),
$this->getAssigned()));
}
}
// Field configurations
if ($f=$form->getField('assignee')) {
if ($prompt)
$f->configure('prompt', $prompt);
$f->configure('dept', $dept);
}
function getReferralForm($source=null, $options=array()) {
$form = ReferralForm::instantiate($source, $options);
$dept = $this->getDept();
// Agents
$staff = Staff::objects()->filter(array(
'isactive' => 1,
))
->filter(Q::not(array('dept_id' => $dept->getId())));
$staff = Staff::nsort($staff);
$agents = array();
foreach ($staff as $s)
$agents[$s->getId()] = $s;
$form->setChoices('agent', $agents);
// Teams
$form->setChoices('team', Team::getActiveTeams());
// Depts
$form->setChoices('dept', Dept::getDepartments());
function getClaimForm($source=null, $options=array()) {
global $thisstaff;
$id = sprintf('s%d', $thisstaff->getId());
if(!$source)
$source = array('assignee' => array($id));
$form = ClaimForm::instantiate($source, $options);
$form->setAssignees(array($id => $thisstaff->getName()));
$source = array('dept' => array($this->getDeptId()),
'refer' => false);
return TransferForm::instantiate($source);
}
function getField($fid) {
if (is_numeric($fid))
return $this->getDymanicFieldById($fid);
// Special fields
switch ($fid) {
case 'priority':
return TicketForm::getInstance()->getField('priority');
break;
case 'sla':
return ChoiceField::init(array(
'id' => $fid,
'name' => "{$fid}_id",
'label' => __('SLA Plan'),