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_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'),
);
// 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;
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()->allowsReopen());
return $this->hasState('closed');
}
function isCloseable() {
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);
}
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())
// no restrictions
&& !$staff->isAccessLimited()
// 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();
Peter Rotich
committed
function getHashtable() {
return $this->ht;
}
function getUpdateInfo() {
return array(
'source' => $this->getSource(),
'topicId' => $this->getTopicId(),
'slaId' => $this->getSLAId(),
'user_id' => $this->getOwnerId(),
'duedate' => $this->getDueDate()
? Format::date($this->getDueDate(), true,
$cfg->getDateFormat(true))
: '',
'time' => $this->getDueDate()
? Format::time($this->getDueDate(), true, 'HH:mm')
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')
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;
//UserList of recipients (owner + collaborators)
function getRecipients($excludeBcc=false) {
if ($excludeBcc && isset($this->recipients)) {
$list = new UserList();
if ($collabs = $this->getThread()->getActiveCollaborators()) {
$list->add($this->getOwner());
foreach ($collabs as $c) {
if (get_class($c) == 'Collaborator' && !$c->isCc()) //skip bcc
continue;
else
}
}
$this->recipients = $list;
}
//I think we need to rebuild each time since it
//would be incomplete if called after an exclude bcc call
else {
$list = new UserList();
$list->add($this->getOwner());
if ($collabs = $this->getThread()->getActiveCollaborators()) {
foreach ($collabs as $c) {
$list->add($c);
}
}
$this->recipients = $list;
function getAssignmentForm($source=null, $options=array()) {
$prompt = $assignee = '';
// Possible assignees
$assignees = array();
switch (strtolower($options['target'])) {
case 'agents':
$dept = $this->getDept();
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':
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 ($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()));
}
}
if ($prompt && ($f=$form->getField('assignee')))
$f->configure('prompt', $prompt);
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');
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
break;
case 'sla':
return ChoiceField::init(array(
'id' => $fid,
'name' => "{$fid}_id",
'label' => __('SLA Plan'),
'default' => $this->getSLAId(),
'choices' => SLA::getSLAs()
));
break;
case 'topic':
return ChoiceField::init(array(
'id' => $fid,
'name' => "{$fid}_id",
'label' => __('Help Topic'),
'default' => $this->getTopicId(),
'choices' => Topic::getHelpTopics()
));
break;
case 'source':
return ChoiceField::init(array(
'id' => $fid,
'name' => 'source',
'label' => __('Ticket Source'),
'default' => $this->getSource(),
'choices' => Ticket::getSources()
));
break;
case 'duedate':
$hint = sprintf(__('Setting a %s will override %s'),
__('Due Date'), __('SLA Plan'));
return DateTimeField::init(array(
'id' => $fid,
'name' => $fid,
'default' => Misc::db2gmtime($this->getDueDate()),
'label' => __('Due Date'),
'hint' => $hint,
'configuration' => array(