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'),
'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'),
);
function getId() {
return $this->ticket_id;
}
function getEffectiveDate() {
strtotime($this->thread->lastmessage),
strtotime($this->closed),
strtotime($this->reopened),
strtotime($this->created)
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}
extends VerySimpleModel {
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);
$join = array(
'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'),
'list' => true,
);
// This may be necessary if the model has already been inspected
if (static::$meta instanceof ModelMeta)
static::$meta->addJoin('cdata+'.$form->id, $join);
else {
static::$meta['joins']['cdata+'.$form->id] = array(
'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'),
'list' => true,
);
}
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';
class Ticket extends TicketModel
implements RestrictedAccess, Threadable {
'select_related' => array('topic', 'staff', 'user', 'team', 'dept', 'sla', 'thread',
'user__default_email'),
Peter Rotich
committed
var $owner; // TicketOwner
var $_user; // EndUser
var $_answers;
var $collaborators;
var $active_collaborators;
var $recipients;
var $lastrespondent;
Peter Rotich
committed
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;
return strcasecmp($this->getState(), $state) == 0;
function isReopenable() {
return $this->getStatus()->isReopenable();
}
return $this->hasState('closed');
}
function isCloseable() {
if ($this->isClosed())
return true;
$warning = null;
if ($this->getMissingRequiredFields()) {
$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');
return $this->isOpen() && ($this->getStaffId() || $this->getTeamId());
Peter Rotich
committed
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))
if ($user->getId() == $this->getUserId())
// 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->_answers['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() {
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() {
$this->est_duedate = $this->getEstDueDate();
$this->save();
}
function getEstDueDate() {
// Real due date
if ($duedate = $this->getDueDate()) {
return $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 (($a = $this->_answers['priority'])
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();
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())
: '',
'time' => $this->getDueDate()?(Format::date($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() {
function getAssigneeId() {
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;
if ($this->team)
return $this->team;
return null;
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() {
$this->lastrespondent = Staff::objects()
->filter(array(
'staff_id' => static::objects()
->filter(array(
'thread__entries__type' => 'R',
'thread__entries__staff_id__gt' => 0
->values_flat('thread__entries__staff_id')
->order_by('-thread__entries__id')
->limit(1)
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() {
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 getAssignmentForm($source=null, $options=array()) {
if (!$source)
$source = array('assignee' => array($this->getAssigneeId()));
$options += array('dept' => $this->getDept());
return AssignmentForm::instantiate($source, $options);
}
function getTransferForm($source=null) {
if (!$source)
$source = array('dept' => array($this->getDeptId()));
return TransferForm::instantiate($source);
}
function getDynamicFields($criteria=array()) {
$fields = DynamicFormField::objects()->filter(array(
'id__in' => $this->entries
->filter($criteria)
->values_flat('answers__field_id')));
return ($fields && count($fields)) ? $fields : array();
}
function hasClientEditableFields() {
$forms = DynamicFormEntry::forTicket($this->getId());
foreach ($forms as $form) {
foreach ($form->getFields() as $field) {
if ($field->isEditableToUsers())
return true;
}
}
}
function getMissingRequiredFields() {
return $this->getDynamicFields(array(
'answers__field__flags__hasbit' => DynamicFormField::FLAG_ENABLED,
'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED,
'answers__value__isnull' => true,
));
}
function getMissingRequiredField() {
$fields = $this->getMissingRequiredFields();
function addCollaborator($user, $vars, &$errors, $event=true) {
if (!$user || $user->getId() == $this->getOwnerId())
if ($c = $this->getThread()->addCollaborator($user, $vars, $errors, $event)) {
$this->collaborators = null;
$this->recipients = 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()
$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,
));
}
$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>
$authtoken = sprintf('%s%dx%s',
($user->getId() == $this->getOwnerId() ? 'o' : 'c'),
$algo,
Base32::encode(pack('VV',$user->getId(), $this->getId())));
switch($algo) {
case 1:
$authtoken .= substr(base64_encode(
md5($user->getId().$this->getCreateDate().$this->getId().SECRET_SALT, true)), 8);
break;
default:
return null;
}
return $authtoken;
}
function sendAccessLink($user) {
global $ost;
if (!($email = $ost->getConfig()->getDefaultEmail())
|| !($content = Page::lookupByType('access-link')))
return;
$vars = array(
'url' => $ost->getConfig()->getBaseUrl(),
'ticket' => $this,
'user' => $user,
'recipient' => $user,
);
$lang = $user->getLanguage(UserAccount::LANG_MAILOUTS);
$msg = $ost->replaceTemplateVariables(array(
'subj' => $content->getLocalName($lang),
'body' => $content->getLocalBody($lang),
), $vars);
$email->send($user, Format::striptags($msg['subj']),
$msg['body']);
}
/* -------------------- Setters --------------------- */
function setLastMsgId($msgid) {
return $this->lastMsgId=$msgid;
}
function setLastMessage($message) {
$this->last_message = $message;