-
Peter Rotich authored
Introduce the concept of tasks. Tasks are to do list that can be attached to any object e.g Ticket.
Peter Rotich authoredIntroduce the concept of tasks. Tasks are to do list that can be attached to any object e.g Ticket.
class.ticket.php 106.53 KiB
<?php
/*********************************************************************
class.ticket.php
The most important class! Don't play with fire please.
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
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.staff.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.sla.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.task.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')
),
'collaborators' => array(
'reverse' => 'TicketCollaborator.ticket',
'null' => true,
),
'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,
),
'staff' => array(
'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() {
return Format::datetime(max(
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;
}
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 {
var $id;
var $number;
var $ht;
var $lastMsgId;
var $status;
var $dept; //Dept obj
var $sla; // SLA obj
var $staff; //Staff obj
var $client; //Client Obj
var $team; //Team obj
var $topic; //Topic obj
var $tlock; //TicketLock obj
var $thread; //Thread obj.
function Ticket($id) {
$this->id = 0;
$this->load($id);
}
function load($id=0) {
if (!$id && !($id=$this->getId()))
return false;
$sql='SELECT ticket.*, thread.id as thread_id, lock_id, dept.name as dept_name '
.' ,count(distinct attach.attach_id) as attachments'
.' ,count(distinct task.id) as tasks'
.' FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=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 '.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;
if (!($res=db_query($sql)) || !db_num_rows($res))
return false;
$this->ht = db_fetch_array($res);
$this->id = $this->ht['ticket_id'];
$this->number = $this->ht['number'];
$this->_answers = array();
$this->loadDynamicData();
//Reset the sub classes (initiated ondemand)...good for reloads.
$this->status= null;
$this->staff = null;
$this->client = null;
$this->team = null;
$this->dept = null;
$this->sla = null;
$this->tlock = null;
$this->stats = null;
$this->topic = null;
$this->thread = null;
$this->collaborators = null;
return true;
}
function loadDynamicData() {
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;
}
}
}
return $this->_answers;
}
function reload() {
return $this->load();
}
function hasState($state) {
return (strcasecmp($this->getState(), $state)==0);
}
function isOpen() {
return $this->hasState('open');
}
function isReopened() {
return ($this->getReopenDate());
}
function isReopenable() {
return $this->getStatus()->isReopenable();
}
function isClosed() {
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() {
return ($this->ht['isoverdue']);
}
function isAnswered() {
return ($this->ht['isanswered']);
}
function isLocked() {
return null !== $this->getLock();
}
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))
return false;
//Ticket Owner
if ($user->getId() == $this->getUserId())
return true;
//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(
'userId' => $user->getId(),
'ticketId' => $this->getId())))
return true;
return false;
}
//Getters
function getId() {
return $this->id;
}
function getNumber() {
return $this->number;
}
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);
return $this->owner;
}
function getEmail(){
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);
}
function getName(){
if ($o = $this->getOwner())
return $o->getName();
return null;
}
function getSubject() {
return (string) $this->_answers['subject'];
}
/* Help topic title - NOT object -> $topic */
function getHelpTopic() {
if(!$this->ht['helptopic'] && ($topic=$this->getTopic()))
$this->ht['helptopic'] = $topic->getFullName();
return $this->ht['helptopic'];
}
function getCreateDate() {
return $this->ht['created'];
}
function getOpenDate() {
return $this->getCreateDate();
}
function getReopenDate() {
return $this->ht['reopened'];
}
function getUpdateDate() {
return $this->ht['updated'];
}
function getEffectiveDate() {
return $this->ht['lastupdate'];
}
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();
}
function getCloseDate() {
return $this->ht['closed'];
}
function getStatusId() {
return $this->ht['status_id'];
}
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();
}
function getDeptId() {
return $this->ht['dept_id'];
}
function getDeptName() {
if(!$this->ht['dept_name'] && ($dept = $this->getDept()))
$this->ht['dept_name'] = $dept->getName();
return $this->ht['dept_name'];
}
function getPriorityId() {
global $cfg;
if (($a = $this->_answers['priority'])
&& ($b = $a->getValue()))
return $b->getId();
return $cfg->getDefaultPriorityId();
}
function getPriority() {
if (($a = $this->_answers['priority']) && ($b = $a->getValue()))
return $b->getDesc();
return '';
}
function getPhoneNumber() {
return (string)$this->getOwner()->getPhoneNumber();
}
function getSource() {
return $this->ht['source'];
}
function getIP() {
return $this->ht['ip_address'];
}
function getHashtable() {
return $this->ht;
}
function getUpdateInfo() {
global $cfg;
$info=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')):'',
);
return $info;
}
function getLock() {
return $this->tlock;
}
function acquireLock($staffId, $lockTime) {
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.
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!
return $this->tlock;
}
function getDept() {
global $cfg;
if(!$this->dept)
if(!($this->dept = Dept::lookup($this->getDeptId())))
$this->dept = $cfg->getDefaultDept();
return $this->dept;
}
function getUserId() {
return $this->getOwnerId();
}
function getUser() {
if(!isset($this->user) && $this->getOwner())
$this->user = new EndUser($this->getOwner());
return $this->user;
}
function getStaffId() {
return $this->ht['staff_id'];
}
function getStaff() {
if(!$this->staff && $this->getStaffId())
$this->staff= Staff::lookup($this->getStaffId());
return $this->staff;
}
function getTeamId() {
return $this->ht['team_id'];
}
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 '';
}
function getAssignees() {
$assignees=array();
if($staff=$this->getStaff())
$assignees[] = $staff->getName();
if($team=$this->getTeam())
$assignees[] = $team->getName();
return $assignees;
}
function getAssigned($glue='/') {
$assignees = $this->getAssignees();
return $assignees?implode($glue, $assignees):'';
}
function getTopicId() {
return $this->ht['topic_id'];
}
function getTopic() {
if(!$this->topic && $this->getTopicId())
$this->topic = Topic::lookup($this->getTopicId());
return $this->topic;
}
function getSLAId() {
return $this->ht['sla_id'];
}
function getSLA() {
if(!$this->sla && $this->getSLAId())
$this->sla = SLA::lookup($this->getSLAId());
return $this->sla;
}
function getLastRespondent() {
if (!isset($this->lastrespondent)) {
$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);
$this->lastrespondent = Staff::lookup($id);
}
return $this->lastrespondent;
}
function getLastMessageDate() {
return $this->ht['lastmessage'];
}
function getLastMsgDate() {
return $this->getLastMessageDate();
}
function getLastResponseDate() {
return $this->ht['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());
if (!$this->last_message)
$this->last_message = $this->getThread()->getLastMessage();
}
return $this->last_message;
}
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;
}
function getThreadCount() {
return $this->getNumMessages() + $this->getNumResponses();
}
function getNumMessages() {
return $this->getThread()->getNumMessages();
}
function getNumResponses() {
return $this->getThread()->getNumResponses();
}
function getNumNotes() {
return $this->getThread()->getNumNotes();
}
function getMessages() {
return $this->getThreadEntries('M');
}
function getResponses() {
return $this->getThreadEntries('R');
}
function getNotes() {
return $this->getThreadEntries('N');
}
function getClientThread() {
return $this->getThreadEntries(array('M', 'R'));
}
function getThreadEntry($id) {
return $this->getThread()->getEntry($id);
}
function getThreadEntries($type, $order='') {
return $this->getThread()->getEntries(
array( 'type' => $type, 'order' => $order));
}
//Collaborators
function getNumCollaborators() {
return count($this->getCollaborators());
}
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()) {
if ($criteria)
return Collaborator::forTicket($this->getId(), $criteria);
if (!isset($this->collaborators))
$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())
return null;
$vars = array_merge(array(
'ticketId' => $this->getId(),
'userId' => $user->getId()), $vars);
if (!($c=Collaborator::add($vars, $errors)))
return null;
$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()
&& $c->remove())
$collabs[] = $c;
}
$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());
}
//DeptId can NOT be 0. No orphans please!
function setDeptId($deptId) {
//Make sure it's a valid department//
if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId())
return false;
$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());
}
//Set staff ID...assign/unassign/release (id can be 0)
function setStaffId($staffId) {
if(!is_numeric($staffId)) return false;
$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;
$this->staff = null;
$this->ht['staff_id'] = $staffId;
return true;
}
function setSLAId($slaId) {
if ($slaId == $this->getSLAId()) return true;
$rv = db_query(
'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)) {
$slaId = $trump;
} elseif ($this->getDept() && $this->getDept()->getSLAId()) {
$slaId = $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)
function setTeamId($teamId) {
if(!is_numeric($teamId)) return false;
$sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId)
.' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
//Status helper.
function setStatus($status, $comments='', &$errors=array()) {
global $thisstaff;
if (!$thisstaff || !($role=$thisstaff->getRole($this->getDeptId())))
return false;
if ($status && is_numeric($status))
$status = TicketStatus::lookup($status);
if (!$status || !$status instanceof TicketStatus)
return false;
// Double check permissions
switch ($status->getState()) {
case 'closed':
if (!($role->canCloseTickets()))
return false;
break;
case 'deleted':
// XXX: intercept deleted status and do hard delete
if ($role->canDeleteTickets())
return $this->delete($comments);
// Agent doesn't have permission to delete tickets
return false;
break;
}
if ($this->getStatusId() == $status->getId())
return true;
$sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
' ,status_id='.db_input($status->getId());
//TODO: move this up.
$ecb = null;
switch($status->getState()) {
case 'closed':
if ($this->getMissingRequiredFields()) {
$errors['err'] = sprintf(__(
'This ticket is missing data on %s one or more required fields %s and cannot be closed'),
'', '');
return false;
}
$sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL ';
if ($thisstaff)
$sql.=', staff_id='.db_input($thisstaff->getId());
$ecb = function($t) {
$t->reload();
$t->logEvent('closed');
$t->deleteDrafts();
};
break;
case 'open':
// TODO: check current status if it allows for reopening
if ($this->isClosed()) {
$sql .= ',closed=NULL, lastupdate=NOW(), reopened=NOW() ';
$ecb = function ($t) {
$t->logEvent('reopened', 'closed');
};
}
// If the ticket is not open then clear answered flag
if (!$this->isOpen())
$sql .= ', isanswered = 0 ';
break;
default:
return false;
}
$sql.=' WHERE ticket_id='.db_input($this->getId());
if (!db_query($sql) || !db_affected_rows())
return false;
// Log status change b4 reload
$note = sprintf(__('Status changed from %s to %s by %s'),
$this->getStatus(),
$status,
$thisstaff ?: 'SYSTEM');
$alert = false;
if ($comments) {
$note .= sprintf('<hr>%s', $comments);
// Send out alerts if comments are included
$alert = true;
}
$this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
// Log events via callback
if ($ecb) $ecb($this);
return true;
}
function setState($state, $alerts=false) {
switch(strtolower($state)) {
case 'open':
return $this->setStatus('open');
break;
case 'closed':
return $this->setStatus('closed');
break;
case 'answered':
return $this->setAnsweredState(1);
break;
case 'unanswered':
return $this->setAnsweredState(0);
break;
case 'overdue':
return $this->markOverdue();
break;
case 'notdue':
return $this->clearOverdue();
break;
case 'unassined':
return $this->unassign();
}
return false;
}
function setAnsweredState($isanswered) {
$sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered)
.' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
function reopen() {
global $cfg;
if (!$this->isClosed())
return false;
// Set status to open based on current closed status settings
// If the closed status doesn't have configured "reopen" status then use the
// the default ticket status.
if (!($status=$this->getStatus()->getReopenStatus()))
$status = $cfg->getDefaultTicketStatusId();
return $status ? $this->setStatus($status, 'Reopened') : false;
}
function onNewTicket($message, $autorespond=true, $alertstaff=true) {
global $cfg;
//Log stuff here...
if(!$autorespond && !$alertstaff) return true; //No alerts to send.
/* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/
$this->reload(); //get the new goodies.
if(!$cfg
|| !($dept=$this->getDept())
|| !($tpl = $dept->getTemplate())
|| !($email=$dept->getAutoRespEmail())) {
return false; //bail out...missing stuff.
}
$options = array(
'inreplyto'=>$message->getEmailMessageId(),
'references'=>$message->getEmailReferences(),
'thread'=>$message);
//Send auto response - if enabled.
if($autorespond
&& $cfg->autoRespONNewTicket()
&& $dept->autoRespONNewTicket()
&& ($msg=$tpl->getAutoRespMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
array('message' => $message,
'recipient' => $this->getOwner(),
'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
);
$email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'],
null, $options);
}
//Send alert to out sleepy & idle staff.
if ($alertstaff
&& $cfg->alertONNewTicket()
&& ($email=$cfg->getAlertEmail())
&& ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(), array('message' => $message));
$recipients=$sentlist=array();
//Exclude the auto responding email just incase it's from staff member.
if ($message->isAutoReply())
$sentlist[] = $this->getEmail();
//Alert admin??
if($cfg->alertAdminONNewTicket()) {
$alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
$email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options);
$sentlist[]=$cfg->getAdminEmail();
}
//Only alerts dept members if the ticket is NOT assigned.
if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) {
if(($members=$dept->getMembersForAlerts()))
$recipients=array_merge($recipients, $members);
}
if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager()))
$recipients[]= $manager;
// Account manager
if ($cfg->alertAcctManagerONNewMessage()
&& ($org = $this->getOwner()->getOrganization())
&& ($acct_manager = $org->getAccountManager())) {
if ($acct_manager instanceof Team)
$recipients = array_merge($recipients, $acct_manager->getMembers());
else
$recipients[] = $acct_manager;
}
foreach( $recipients as $k=>$staff) {
if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
function onOpenLimit($sendNotice=true) {
global $ost, $cfg;
//Log the limit notice as a warning for admin.
$msg=sprintf(_S('Maximum open tickets (%1$d) reached for %2$s'),
$cfg->getMaxOpenTickets(), $this->getEmail());
$ost->logWarning(sprintf(_S('Maximum Open Tickets Limit (%s)'),$this->getEmail()),
$msg);
if(!$sendNotice || !$cfg->sendOverLimitNotice())
return true;
//Send notice to user.
if(($dept = $this->getDept())
&& ($tpl=$dept->getTemplate())
&& ($msg=$tpl->getOverlimitMsgTemplate())
&& ($email=$dept->getAutoRespEmail())) {
$msg = $this->replaceVars($msg->asArray(),
array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
$email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']);
}
$user = $this->getOwner();
//Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
$alert=sprintf(__('Maximum open tickets reached for %s.'), $this->getEmail())."\n"
.sprintf(__('Open tickets: %d'), $user->getNumOpenTickets())."\n"
.sprintf(__('Max allowed: %d'), $cfg->getMaxOpenTickets())
."\n\n".__("Notice sent to the user.");
$ost->alertAdmin(__('Overlimit Notice'), $alert);
return true;
}
function onResponse() {
db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1, lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId()));
$this->reload();
}
/*
* Notify collaborators on response or new message
*
*/
function notifyCollaborators($entry, $vars = array()) {
global $cfg;
if (!$entry instanceof ThreadEntry
|| !($recipients=$this->getRecipients())
|| !($dept=$this->getDept())
|| !($tpl=$dept->getTemplate())
|| !($msg=$tpl->getActivityNoticeMsgTemplate())
|| !($email=$dept->getEmail()))
return;
//Who posted the entry?
$uid = 0;
if ($entry instanceof Message) {
$poster = $entry->getUser();
// Skip the person who sent in the message
$uid = $entry->getUserId();
} else {
$poster = $entry->getStaff();
// Skip the ticket owner
$uid = $this->getUserId();
}
$vars = array_merge($vars, array(
'message' => (string) $entry,
'poster' => $poster ?: _S('A collaborator'),
)
);
$msg = $this->replaceVars($msg->asArray(), $vars);
$attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
$options = array('inreplyto' => $entry->getEmailMessageId(),
'thread' => $entry);
foreach ($recipients as $recipient) {
if ($uid == $recipient->getUserId()) continue;
$options['references'] = $entry->getEmailReferencesForUser($recipient);
$notice = $this->replaceVars($msg, array('recipient' => $recipient));
$email->send($recipient->getEmail(), $notice['subj'], $notice['body'], $attachments,
$options);
}
return;
}
function onMessage($message, $autorespond=true) {
global $cfg;
db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastupdate=NOW(),lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
// Auto-assign to closing staff or last respondent
// If the ticket is closed and auto-claim is not enabled then put the
// ticket back to unassigned pool.
if ($this->isClosed() && !$cfg->autoClaimTickets()) {
$this->setStaffId(0);
} elseif(!($staff=$this->getStaff()) || !$staff->isAvailable()) {
// Ticket has no assigned staff - if auto-claim is enabled then
// try assigning it to the last respondent (if available)
// otherwise leave the ticket unassigned.
if ($cfg->autoClaimTickets() //Auto claim is enabled.
&& ($lastrep=$this->getLastRespondent())
&& $lastrep->isAvailable()) {
$this->setStaffId($lastrep->getId()); //direct assignment;
} else {
$this->setStaffId(0); //unassign - last respondent is not available.
}
}
// Reopen if closed AND reopenable
if ($this->isClosed() && $this->isReopenable())
$this->reopen();
// Figure out the user
if ($this->getOwnerId() == $message->getUserId())
$user = new TicketOwner(
User::lookup($message->getUserId()), $this);
else
$user = Collaborator::lookup(array(
'userId'=>$message->getUserId(), 'ticketId'=>$this->getId()));
/********** double check auto-response ************/
if (!$user)
$autorespond=false;
elseif ($autorespond && (Email::getIdByEmail($user->getEmail())))
$autorespond=false;
elseif ($autorespond && ($dept=$this->getDept()))
$autorespond=$dept->autoRespONNewMessage();
if(!$autorespond
|| !$cfg->autoRespONNewMessage()
|| !$message) return; //no autoresp or alerts.
$this->reload();
$dept = $this->getDept();
$email = $dept->getAutoRespEmail();
//If enabled...send confirmation to user. ( New Message AutoResponse)
if($email
&& ($tpl=$dept->getTemplate())
&& ($msg=$tpl->getNewMessageAutorepMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
array(
'recipient' => $user,
'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
$options = array(
'inreplyto'=>$message->getEmailMessageId(),
'references' => $message->getEmailReferencesForUser($user),
'thread'=>$message);
$email->sendAutoReply($user->getEmail(), $msg['subj'], $msg['body'],
null, $options);
}
}
function onAssign($assignee, $comments, $alert=true) {
global $cfg, $thisstaff;
if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign?
//Assignee must be an object of type Staff or Team
if(!$assignee || !is_object($assignee)) return false;
$this->reload();
$comments = $comments ?: _S('Ticket assignment');
$assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');
//Log an internal note - no alerts on the internal note.
$note = $this->logNote(
sprintf(_S('Ticket Assigned to %s'), $assignee->getName()),
$comments, $assigner, false);
//See if we need to send alerts
if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts!
$dept = $this->getDept();
if(!$dept
|| !($tpl = $dept->getTemplate())
|| !($email = $cfg->getAlertEmail()))
return true;
//recipients
$recipients=array();
if ($assignee instanceof Staff) {
if ($cfg->alertStaffONAssignment())
$recipients[] = $assignee;
} elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) {
if ($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembers()))
$recipients = array_merge($recipients, $members);
elseif ($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead()))
$recipients[] = $lead;
}
//Get the message template
if ($recipients
&& ($msg=$tpl->getAssignedAlertMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments,
'assignee' => $assignee,
'assigner' => $assigner
));
//Send the alerts.
$sentlist=array();
$options = array(
'inreplyto'=>$note->getEmailMessageId(),
'references'=>$note->getEmailReferences(),
'thread'=>$note);
foreach( $recipients as $k=>$staff) {
if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
function onOverdue($whine=true, $comments="") {
global $cfg;
if($whine && ($sla=$this->getSLA()) && !$sla->alertOnOverdue())
$whine = false;
//check if we need to send alerts.
if(!$whine
|| !$cfg->alertONOverdueTicket()
|| !($dept = $this->getDept()))
return true;
//Get the message template
if(($tpl = $dept->getTemplate())
&& ($msg=$tpl->getOverdueAlertMsgTemplate())
&& ($email=$cfg->getAlertEmail())) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments));
//recipients
$recipients=array();
//Assigned staff or team... if any
if($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
if($this->getStaffId())
$recipients[]=$this->getStaff();
elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
$recipients=array_merge($recipients, $members);
} elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
//Only alerts dept members if the ticket is NOT assigned.
if ($members = $dept->getMembersForAlerts())
$recipients = array_merge($recipients, $members);
}
//Always alert dept manager??
if($cfg->alertDeptManagerONOverdueTicket() && $dept && ($manager=$dept->getManager()))
$recipients[]= $manager;
$sentlist=array();
foreach( $recipients as $k=>$staff) {
if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
//ticket obj as variable = ticket number.
function asVar() {
return $this->getNumber();
}
function getVar($tag) {
global $cfg;
if($tag && is_callable(array($this, 'get'.ucfirst($tag))))
return call_user_func(array($this, 'get'.ucfirst($tag)));
switch(mb_strtolower($tag)) {
case 'phone':
case 'phone_number':
return $this->getPhoneNumber();
break;
case 'auth_token':
return $this->getAuthToken();
break;
case 'client_link':
return sprintf('%s/view.php?t=%s',
$cfg->getBaseUrl(), $this->getNumber());
break;
case 'staff_link':
return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId());
break;
case 'create_date':
return Format::datetime($this->getCreateDate(), true, 'UTC');
break;
case 'due_date':
$duedate ='';
if($this->getEstDueDate())
$duedate = Format::datetime($this->getEstDueDate(), true, 'UTC');
return $duedate;
break;
case 'close_date':
$closedate ='';
if($this->isClosed())
$closedate = Format::datetime($this->getCloseDate(), true, 'UTC');
return $closedate;
break;
case 'user':
return $this->getOwner();
break;
default:
if (isset($this->_answers[$tag]))
// The answer object is retrieved here which will
// automatically invoke the toString() method when the
// answer is coerced into text
return $this->_answers[$tag];
}
return false;
}
//Replace base variables.
function replaceVars($input, $vars = array()) {
global $ost;
$vars = array_merge($vars, array('ticket' => $this));
return $ost->replaceTemplateVariables($input, $vars);
}
function markUnAnswered() {
return (!$this->isAnswered() || $this->setAnsweredState(0));
}
function markAnswered() {
return ($this->isAnswered() || $this->setAnsweredState(1));
}
function markOverdue($whine=true) {
global $cfg;
if($this->isOverdue())
return true;
$sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1, updated=NOW() '
.' WHERE ticket_id='.db_input($this->getId());
if(!db_query($sql) || !db_affected_rows())
return false;
$this->logEvent('overdue');
$this->onOverdue($whine);
return true;
}
function clearOverdue() {
if(!$this->isOverdue())
return true;
//NOTE: Previously logged overdue event is NOT annuled.
$sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() ';
//clear due date if it's in the past
if($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime())
$sql.=', duedate=NULL';
//Clear SLA if est. due date is in the past
if($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
$sql.=', sla_id=0 ';
$sql.=' WHERE ticket_id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
//Dept Tranfer...with alert.. done by staff
function transfer($deptId, $comments, $alert = true) {
global $cfg, $thisstaff;
if(!$thisstaff
|| !($role=$thisstaff->getRole($this->getDeptId()))
|| !$role->canTransferTickets())
return false;
$currentDept = $this->getDeptName(); //Current department
if(!$deptId || !$this->setDeptId($deptId))
return false;
// Reopen ticket if closed
if($this->isClosed()) $this->reopen();
$this->reload();
// Set SLA of the new department
if(!$this->getSLAId() || $this->getSLA()->isTransient())
$this->selectSLAId();
/*** log the transfer comments as internal note - with alerts disabled - ***/
$title=sprintf(_S('Ticket transferred from %1$s to %2$s'),
$currentDept, $this->getDeptName());
$comments=$comments?$comments:$title;
$note = $this->logNote($title, $comments, $thisstaff, false);
$this->logEvent('transferred');
//Send out alerts if enabled AND requested
if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept()))
return true; //no alerts!!
if(($email=$cfg->getAlertEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg=$tpl->getTransferAlertMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $comments, 'staff' => $thisstaff));
//recipients
$recipients=array();
//Assigned staff or team... if any
if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
if($this->getStaffId())
$recipients[]=$this->getStaff();
elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
$recipients = array_merge($recipients, $members);
} elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
//Only alerts dept members if the ticket is NOT assigned.
if(($members=$dept->getMembersForAlerts()))
$recipients = array_merge($recipients, $members);
}
//Always alert dept manager??
if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()))
$recipients[]= $manager;
$sentlist=array();
$options = array(
'inreplyto'=>$note->getEmailMessageId(),
'references'=>$note->getEmailReferences(),
'thread'=>$note);
foreach( $recipients as $k=>$staff) {
if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
return true;
}
function assignToStaff($staff, $note, $alert=true) {
if(!is_object($staff) && !($staff=Staff::lookup($staff)))
return false;
if (!$staff->isAvailable() || !$this->setStaffId($staff->getId()))
return false;
$this->onAssign($staff, $note, $alert);
$this->logEvent('assigned');
return true;
}
function assignToTeam($team, $note, $alert=true) {
if(!is_object($team) && !($team=Team::lookup($team)))
return false;
if (!$team->isActive() || !$this->setTeamId($team->getId()))
return false;
//Clear - staff if it's a closed ticket
// staff_id is overloaded -> assigned to & closed by.
if($this->isClosed())
$this->setStaffId(0);
$this->onAssign($team, $note, $alert);
$this->logEvent('assigned');
return true;
}
//Assign ticket to staff or team - overloaded ID.
function assign($assignId, $note, $alert=true) {
global $thisstaff;
$rv=0;
$id=preg_replace("/[^0-9]/", "", $assignId);
if($assignId[0]=='t') {
$rv=$this->assignToTeam($id, $note, $alert);
} elseif($assignId[0]=='s' || is_numeric($assignId)) {
$alert=($alert && $thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!!
//We don't care if a team is already assigned to the ticket - staff assignment takes precedence
$rv=$this->assignToStaff($id, $note, $alert);
}
return $rv;
}
//unassign primary assignee
function unassign() {
if(!$this->isAssigned()) //We can't release what is not assigned buddy!
return true;
//We can only unassigned OPEN tickets.
if($this->isClosed())
return false;
//Unassign staff (if any)
if($this->getStaffId() && !$this->setStaffId(0))
return false;
//unassign team (if any)
if($this->getTeamId() && !$this->setTeamId(0))
return false;
$this->reload();
return true;
}
function release() {
return $this->unassign();
}
//Change ownership
function changeOwner($user) {
global $thisstaff;
if (!$user
|| ($user->getId() == $this->getOwnerId())
|| !($role=$thisstaff->getRole($this->getDeptId()))
|| !$role->canEditTickets())
return false;
$sql ='UPDATE '.TICKET_TABLE.' SET updated = NOW() '
.', user_id = '.db_input($user->getId())
.' WHERE ticket_id = '.db_input($this->getId());
if (!db_query($sql) || !db_affected_rows())
return false;
$this->ht['user_id'] = $user->getId();
$this->user = null;
$this->collaborators = null;
$this->recipients = null;
//Log an internal note
$note = sprintf(_S('%s changed ticket ownership to %s'),
$thisstaff->getName(), $user->getName());
//Remove the new owner from list of collaborators
$c = Collaborator::lookup(array('userId' => $user->getId(),
'ticketId' => $this->getId()));
if ($c && $c->remove())
$note.= ' '._S('(removed as collaborator)');
$this->logNote('Ticket ownership changed', $note);
return true;
}
//Insert message from client
function postMessage($vars, $origin='', $alerts=true) {
global $cfg;
$vars['origin'] = $origin;
if(isset($vars['ip']))
$vars['ip_address'] = $vars['ip'];
elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
$vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
$errors = array();
if(!($message = $this->getThread()->addMessage($vars, $errors)))
return null;
$this->setLastMessage($message);
//Add email recipients as collaborators...
if ($vars['recipients']
&& (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail()))
//Only add if we have a matched local address
&& $vars['to-email-id']) {
//New collaborators added by other collaborators are disable --
// requires staff approval.
$info = array(
'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0);
$collabs = array();
foreach ($vars['recipients'] as $recipient) {
// Skip virtual delivered-to addresses
if (strcasecmp($recipient['source'], 'delivered-to') === 0)
continue;
if (($user=User::fromVars($recipient)))
if ($c=$this->addCollaborator($user, $info, $errors))
$collabs[] = sprintf('%s%s',
(string) $c,
$recipient['source']
? " ".sprintf(_S('via %s'), $recipient['source'])
: ''
);
}
//TODO: Can collaborators add others?
if ($collabs) {
//TODO: Change EndUser to name of user.
$this->logNote(_S('Collaborators added by end user'),
implode("<br>", $collabs), _S('End User'), false);
}
}
if(!$alerts) return $message; //Our work is done...
// Do not auto-respond to bounces and other auto-replies
$autorespond = isset($vars['flags'])
? !$vars['flags']['bounce'] && !$vars['flags']['auto-reply']
: true;
if ($autorespond && $message->isAutoReply())
$autorespond = false;
$this->onMessage($message, $autorespond); //must be called b4 sending alerts to staff.
if ($autorespond && $cfg && $cfg->notifyCollabsONNewMessage())
$this->notifyCollaborators($message, array('signature' => ''));
$dept = $this->getDept();
$variables = array(
'message' => $message,
'poster' => ($vars['poster'] ? $vars['poster'] : $this->getName())
);
$options = array(
'inreplyto' => $message->getEmailMessageId(),
'references' => $message->getEmailReferences(),
'thread'=>$message);
//If enabled...send alert to staff (New Message Alert)
if($cfg->alertONNewMessage()
&& ($email = $cfg->getAlertEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg = $tpl->getNewMessageAlertMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(), $variables);
//Build list of recipients and fire the alerts.
$recipients=array();
//Last respondent.
if($cfg->alertLastRespondentONNewMessage() || $cfg->alertAssignedONNewMessage())
$recipients[]=$this->getLastRespondent();
//Assigned staff if any...could be the last respondent
if($this->isAssigned() && ($staff=$this->getStaff()))
$recipients[]=$staff;
//Dept manager
if($cfg->alertDeptManagerONNewMessage() && $dept && ($manager=$dept->getManager()))
$recipients[]=$manager;
// Account manager
if ($cfg->alertAcctManagerONNewMessage()
&& ($org = $this->getOwner()->getOrganization())
&& ($acct_manager = $org->getAccountManager())) {
if ($acct_manager instanceof Team)
$recipients = array_merge($recipients, $acct_manager->getMembers());
else
$recipients[] = $acct_manager;
}
$sentlist=array(); //I know it sucks...but..it works.
foreach( $recipients as $k=>$staff) {
if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
$sentlist[] = $staff->getEmail();
}
}
return $message;
}
function postCannedReply($canned, $msgId, $alert=true) {
global $ost, $cfg;
if((!is_object($canned) && !($canned=Canned::lookup($canned))) || !$canned->isEnabled())
return false;
$files = array();
foreach ($canned->attachments->getAll() as $file)
$files[] = $file['id'];
if ($cfg->isHtmlThreadEnabled())
$response = new HtmlThreadEntryBody(
$this->replaceVars($canned->getHtml()));
else
$response = new TextThreadEntryBody(
$this->replaceVars($canned->getPlainText()));
$info = array('msgId' => $msgId,
'poster' => __('SYSTEM (Canned Reply)'),
'response' => $response,
'cannedattachments' => $files);
$errors = array();
if(!($response=$this->postReply($info, $errors, false)))
return null;
$this->markUnAnswered();
if(!$alert) return $response;
$dept = $this->getDept();
if(($email=$dept->getEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg=$tpl->getAutoReplyMsgTemplate())) {
if($dept && $dept->isPublic())
$signature=$dept->getSignature();
else
$signature='';
$msg = $this->replaceVars($msg->asArray(),
array(
'response' => $response,
'signature' => $signature,
'recipient' => $this->getOwner(),
));
$attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array();
$options = array(
'inreplyto'=>$response->getEmailMessageId(),
'references'=>$response->getEmailReferences(),
'thread'=>$response);
$email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments,
$options);
}
return $response;
}
/* public */
function postReply($vars, &$errors, $alert = true) {
global $thisstaff, $cfg;
if(!$vars['poster'] && $thisstaff)
$vars['poster'] = $thisstaff;
if(!$vars['staffId'] && $thisstaff)
$vars['staffId'] = $thisstaff->getId();
if(!($response = $this->getThread()->addResponse($vars, $errors)))
return null;
// Set status - if checked.
if ($vars['reply_status_id']
&& $vars['reply_status_id'] != $this->getStatusId())
$this->setStatus($vars['reply_status_id']);
if($thisstaff && $this->isOpen() && !$this->getStaffId()
&& $cfg->autoClaimTickets())
$this->setStaffId($thisstaff->getId()); //direct assignment;
$this->lastrespondent = null;
$this->onResponse(); //do house cleaning..
/* email the user?? - if disabled - then bail out */
if(!$alert) return $response;
$dept = $this->getDept();
if($thisstaff && $vars['signature']=='mine')
$signature=$thisstaff->getSignature();
elseif($vars['signature']=='dept' && $dept && $dept->isPublic())
$signature=$dept->getSignature();
else
$signature='';
$variables = array(
'response' => $response,
'signature' => $signature,
'staff' => $thisstaff,
'poster' => $thisstaff);
$options = array(
'inreplyto' => $response->getEmailMessageId(),
'references' => $response->getEmailReferences(),
'thread'=>$response);
if(($email=$dept->getEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg=$tpl->getReplyMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
$variables + array('recipient' => $this->getOwner()));
$attachments = $cfg->emailAttachments()?$response->getAttachments():array();
$email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments,
$options);
}
if($vars['emailcollab'])
$this->notifyCollaborators($response,
array('signature' => $signature));
return $response;
}
//Activity log - saved as internal notes WHEN enabled!!
function logActivity($title, $note) {
return $this->logNote($title, $note, 'SYSTEM', false);
}
// History log -- used for statistics generation (pretty reports)
function logEvent($state, $annul=null, $staff=null) {
global $thisstaff;
if ($staff === null) {
if ($thisstaff) $staff=$thisstaff->getUserName();
else $staff='SYSTEM'; # XXX: Security Violation ?
}
# Annul previous entries if requested (for instance, reopening a
# ticket will annul an 'closed' entry). This will be useful to
# easily prevent repeated statistics.
if ($annul) {
db_query('UPDATE '.TICKET_EVENT_TABLE.' SET annulled=1'
.' WHERE ticket_id='.db_input($this->getId())
.' AND state='.db_input($annul));
}
return db_query('INSERT INTO '.TICKET_EVENT_TABLE
.' SET ticket_id='.db_input($this->getId())
.', staff_id='.db_input($this->getStaffId())
.', team_id='.db_input($this->getTeamId())
.', dept_id='.db_input($this->getDeptId())
.', topic_id='.db_input($this->getTopicId())
.', timestamp=NOW(), state='.db_input($state)
.', staff='.db_input($staff))
&& db_affected_rows() == 1;
}
//Insert Internal Notes
function logNote($title, $note, $poster='SYSTEM', $alert=true) {
$errors = array();
//Unless specified otherwise, assume HTML
if ($note && is_string($note))
$note = new HtmlThreadEntryBody($note);
return $this->postNote(
array(
'title' => $title,
'note' => $note,
),
$errors,
$poster,
$alert
);
}
function postNote($vars, &$errors, $poster, $alert=true) {
global $cfg, $thisstaff;
//Who is posting the note - staff or system?
$vars['staffId'] = 0;
$vars['poster'] = 'SYSTEM';
if($poster && is_object($poster)) {
$vars['staffId'] = $poster->getId();
$vars['poster'] = $poster->getName();
}elseif($poster) { //string
$vars['poster'] = $poster;
}
if(!($note=$this->getThread()->addNote($vars, $errors)))
return null;
$alert = $alert && (
isset($vars['flags'])
// No alerts for bounce and auto-reply emails
? !$vars['flags']['bounce'] && !$vars['flags']['auto-reply']
: true
);
// Get assigned staff just in case the ticket is closed.
$assignee = $this->getStaff();
if ($vars['note_status_id']
&& ($status=TicketStatus::lookup($vars['note_status_id']))) {
if ($this->setStatus($status))
$this->reload();
}
// If alerts are not enabled then return a success.
if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept()))
return $note;
if(($email=$cfg->getAlertEmail())
&& ($tpl = $dept->getTemplate())
&& ($msg=$tpl->getNoteAlertMsgTemplate())) {
$msg = $this->replaceVars($msg->asArray(),
array('note' => $note));
// Alert recipients
$recipients=array();
//Last respondent.
if ($cfg->alertLastRespondentONNewNote())
$recipients[] = $this->getLastRespondent();
// Assigned staff / team
if ($cfg->alertAssignedONNewNote()) {
if ($assignee && $assignee instanceof Staff)
$recipients[] = $assignee;
if ($team = $this->getTeam())
$recipients = array_merge($recipients, $team->getMembers());
}
// Dept manager
if ($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId())
$recipients[] = $dept->getManager();
$options = array(
'inreplyto'=>$note->getEmailMessageId(),
'references'=>$note->getEmailReferences(),
'thread'=>$note);
$isClosed = $this->isClosed();
$sentlist=array();
foreach( $recipients as $k=>$staff) {
if(!is_object($staff)
// Don't bother vacationing staff.
|| !$staff->isAvailable()
// No duplicates.
|| isset($sentlist[$staff->getEmail()])
// No need to alert the poster!
|| $note->getStaffId() == $staff->getId()
// Make sure staff has access to ticket
|| ($isClosed && !$this->checkStaffAccess($staff))
)
continue;
$alert = $this->replaceVars($msg, array('recipient' => $staff));
$email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options);
$sentlist[$staff->getEmail()] = 1;
}
}
return $note;
}
//Print ticket... export the ticket thread as PDF.
function pdfExport($psize='Letter', $notes=false) {
global $thisstaff;
require_once(INCLUDE_DIR.'class.pdf.php');
if (!is_string($psize)) {
if ($_SESSION['PAPER_SIZE'])
$psize = $_SESSION['PAPER_SIZE'];
elseif (!$thisstaff || !($psize = $thisstaff->getDefaultPaperSize()))
$psize = 'Letter';
}
$pdf = new Ticket2PDF($this, $psize, $notes);
$name='Ticket-'.$this->getNumber().'.pdf';
Http::download($name, 'application/pdf', $pdf->Output($name, 'S'));
//Remember what the user selected - for autoselect on the next print.
$_SESSION['PAPER_SIZE'] = $psize;
exit;
}
function delete($comments='') {
global $ost, $thisstaff;
//delete just orphaned ticket thread & associated attachments.
// Fetch thread prior to removing ticket entry
$t = $this->getThread();
$sql = 'DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1';
if(!db_query($sql) || !db_affected_rows())
return false;
$t->delete();
foreach (DynamicFormEntry::forTicket($this->getId()) as $form)
$form->delete();
$this->deleteDrafts();
// Log delete
$log = sprintf(__('Ticket #%1$s deleted by %2$s'),
$this->getNumber(),
$thisstaff ? $thisstaff->getName() : __('SYSTEM'));
if ($comments)
$log .= sprintf('<hr>%s', $comments);
$ost->logDebug(
sprintf( __('Ticket #%s deleted'), $this->getNumber()),
$log);
return true;
}
function deleteDrafts() {
Draft::deleteForNamespace('ticket.%.' . $this->getId());
}
function update($vars, &$errors) {
global $cfg, $thisstaff;
if (!$cfg
|| !$thisstaff
|| !($role=$thisstaff->getRole($this->getDeptId()))
|| !$role->canEditTickets())
return false;
$fields=array();
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Help topic selection is required'));
$fields['slaId'] = array('type'=>'int', 'required'=>0, 'error'=>__('Select a valid SLA'));
$fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY'));
$fields['note'] = array('type'=>'text', 'required'=>1, 'error'=>__('A reason for the update is required'));
$fields['user_id'] = array('type'=>'int', 'required'=>0, 'error'=>__('Invalid user-id'));
if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
$errors['err'] = __('Missing or invalid data - check the errors and try again');
if($vars['duedate']) {
if($this->isClosed())
$errors['duedate']=__('Due date can NOT be set on a closed ticket');
elseif(!$vars['time'] || strpos($vars['time'],':')===false)
$errors['time']=__('Select a time from the list');
elseif(strtotime($vars['duedate'].' '.$vars['time'])===false)
$errors['duedate']=__('Invalid due date');
elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time())
$errors['duedate']=__('Due date must be in the future');
}
// Validate dynamic meta-data
$forms = DynamicFormEntry::forTicket($this->getId());
foreach ($forms as $form) {
// Don't validate deleted forms
if (!in_array($form->getId(), $vars['forms']))
continue;
$form->setSource($_POST);
if (!$form->isValid(function($f) {
return $f->isVisibleToStaff() && $f->isEditableToStaff();
})) {
$errors = array_merge($errors, $form->errors());
}
}
if ($errors)
return false;
$sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() '
.' ,topic_id='.db_input($vars['topicId'])
.' ,sla_id='.db_input($vars['slaId'])
.' ,source='.db_input($vars['source'])
.' ,duedate='.($vars['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))):'NULL');
if($vars['user_id'])
$sql.=', user_id='.db_input($vars['user_id']);
if($vars['duedate']) { //We are setting new duedate...
$sql.=' ,isoverdue=0';
}
$sql.=' WHERE ticket_id='.db_input($this->getId());
if(!db_query($sql) || !db_affected_rows())
return false;
if(!$vars['note'])
$vars['note']=sprintf(_S('Ticket details updated by %s'), $thisstaff->getName());
$this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff);
// Decide if we need to keep the just selected SLA
$keepSLA = ($this->getSLAId() != $vars['slaId']);
// Update dynamic meta-data
foreach ($forms as $f) {
// Drop deleted forms
$idx = array_search($f->getId(), $vars['forms']);
if ($idx === false) {
$f->delete();
}
else {
$f->set('sort', $idx);
$f->save();
}
}
// Reload the ticket so we can do further checking
$this->reload();
// Reselect SLA if transient
if (!$keepSLA
&& (!$this->getSLA() || $this->getSLA()->isTransient()))
$this->selectSLAId();
// Update estimated due date in database
$estimatedDueDate = $this->getEstDueDate();
$this->updateEstDueDate();
// Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue.
if($this->isOverdue()
&& (!$estimatedDueDate //Duedate + SLA cleared
|| Misc::db2gmtime($estimatedDueDate) > Misc::gmtime() //New due date in the future.
)) {
$this->clearOverdue();
}
Signal::send('model.updated', $this);
return true;
}
/*============== Static functions. Use Ticket::function(params); =============nolint*/
function getIdByNumber($number, $email=null) {
if(!$number)
return 0;
$sql ='SELECT ticket.ticket_id FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
.' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
.' WHERE ticket.`number`='.db_input($number);
if($email)
$sql .= ' AND email.address = '.db_input($email);
if(($res=db_query($sql)) && db_num_rows($res))
list($id)=db_fetch_row($res);
return $id;
}
function lookup($id) { //Assuming local ID is the only lookup used!
return ($id
&& is_numeric($id)
&& ($ticket= new Ticket($id))
&& $ticket->getId()==$id)
?$ticket:null;
}
function lookupByNumber($number, $email=null) {
return self::lookup(self:: getIdByNumber($number, $email));
}
static function isTicketNumberUnique($number) {
return 0 == db_num_rows(db_query(
'SELECT ticket_id FROM '.TICKET_TABLE.' WHERE `number`='.db_input($number)));
}
function getIdByMessageId($mid, $email) {
if(!$mid || !$email)
return 0;
$sql='SELECT ticket.ticket_id FROM '.TICKET_TABLE. ' ticket '.
' LEFT JOIN '.TICKET_THREAD_TABLE.' msg USING(ticket_id) '.
' INNER JOIN '.TICKET_EMAIL_INFO_TABLE.' emsg ON (msg.id = emsg.message_id) '.
' WHERE email_mid='.db_input($mid).' AND email='.db_input($email);
$id=0;
if(($res=db_query($sql)) && db_num_rows($res))
list($id)=db_fetch_row($res);
return $id;
}
/* Quick staff's tickets stats */
function getStaffStats($staff) {
global $cfg;
/* Unknown or invalid staff */
if(!$staff || (!is_object($staff) && !($staff=Staff::lookup($staff))) || !$staff->isStaff())
return null;
$where = array('(ticket.staff_id='.db_input($staff->getId()) .' AND
status.state="open")');
$where2 = '';
if(($teams=$staff->getTeams()))
$where[] = ' ( ticket.team_id IN('.implode(',', db_input(array_filter($teams)))
.') AND status.state="open")';
if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tickets.
$where[] = 'ticket.dept_id IN('.implode(',', db_input($depts)).') ';
if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets()))
$where2 =' AND (ticket.staff_id=0 OR ticket.team_id=0)';
$where = implode(' OR ', $where);
if ($where) $where = 'AND ( '.$where.' ) ';
$sql = 'SELECT \'open\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'open\') '
.'WHERE ticket.isanswered = 0 '
. $where . $where2
.'UNION SELECT \'answered\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'open\') '
.'WHERE ticket.isanswered = 1 '
. $where . ($cfg->showAnsweredTickets() ? $where2 : '')
.'UNION SELECT \'overdue\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'open\') '
.'WHERE ticket.isoverdue =1 '
. $where
.'UNION SELECT \'assigned\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'open\') '
.'WHERE ticket.staff_id = ' . db_input($staff->getId()) . ' '
. $where
.'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'closed\' ) '
.'WHERE 1 '
. $where;
$res = db_query($sql);
$stats = array();
while($row = db_fetch_row($res)) {
$stats[$row[0]] = $row[1];
}
return $stats;
}
/* Quick client's tickets stats
@email - valid email.
*/
function getUserStats($user) {
if(!$user || !($user instanceof EndUser))
return null;
$sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed '
.' FROM '.TICKET_TABLE.' ticket '
.' LEFT JOIN '.TICKET_TABLE.' open
ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') '
.' LEFT JOIN '.TICKET_TABLE.' closed
ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')'
.' WHERE ticket.user_id = '.db_input($user->getId());
return db_fetch_array(db_query($sql));
}
/*
* The mother of all functions...You break it you fix it!
*
* $autorespond and $alertstaff overrides config settings...
*/
static function create($vars, &$errors, $origin, $autorespond=true,
$alertstaff=true) {
global $ost, $cfg, $thisclient, $_FILES;
// Don't enforce form validation for email
$field_filter = function($type) use ($origin) {
return function($f) use ($origin, $type) {
// Ultimately, only offer validation errors for web for
// non-internal fields. For email, no validation can be
// performed. For other origins, validate as usual
switch (strtolower($origin)) {
case 'email':
return false;
case 'staff':
// Required 'Contact Information' fields aren't required
// when staff open tickets
return $f->isVisibleToStaff();
case 'web':
return $f->isVisibleToUsers();
default:
return true;
}
};
};
$reject_ticket = function($message) use (&$errors) {
global $ost;
$errors = array(
'errno' => 403,
'err' => __('This help desk is for use by authorized users only'));
$ost->logWarning(_S('Ticket Denied'), $message, false);
return 0;
};
Signal::send('ticket.create.before', null, $vars);
// Create and verify the dynamic form entry for the new ticket
$form = TicketForm::getNewInstance();
$form->setSource($vars);
// If submitting via email or api, ensure we have a subject and such
if (!in_array(strtolower($origin), array('web', 'staff'))) {
foreach ($form->getFields() as $field) {
$fname = $field->get('name');
if ($fname && isset($vars[$fname]) && !$field->value)
$field->value = $field->parse($vars[$fname]);
}
}
if (!$form->isValid($field_filter('ticket')))
$errors += $form->errors();
// Unpack dynamic variables into $vars for filter application
$vars += $form->getFilterData();
// Unpack the basic user information
if ($vars['uid'] && ($user = User::lookup($vars['uid']))) {
$vars['email'] = $user->getEmail();
$vars['name'] = $user->getName();
// Add in user and organization data for filtering
$vars += $user->getFilterData();
if ($org = $user->getOrganization()) {
$vars += $org->getFilterData();
}
}
else {
$interesting = array('name', 'email');
$user_form = UserForm::getUserForm()->getForm($vars);
// Add all the user-entered info for filtering
foreach ($user_form->getFields() as $f) {
$vars['field.'.$f->get('id')] = $f->toString($f->getClean());
if (in_array($f->get('name'), $interesting))
$vars[$f->get('name')] = $vars['field.'.$f->get('id')];
}
// Add in organization data if one exists for this email domain
list($mailbox, $domain) = explode('@', $vars['email'], 2);
if ($org = Organization::forDomain($domain)) {
$vars += $org->getFilterData();
}
}
//Check for 403
if ($vars['email']
&& Validator::is_email($vars['email'])) {
//Make sure the email address is not banned
if (TicketFilter::isBanned($vars['email'])) {
return $reject_ticket(sprintf(_S('Banned email - %s'), $vars['email']));
}
//Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
if ($cfg->getMaxOpenTickets() > 0
&& strcasecmp($origin, 'staff')
&& ($_user=TicketUser::lookupByEmail($vars['email']))
&& ($openTickets=$_user->getNumOpenTickets())
&& ($openTickets>=$cfg->getMaxOpenTickets()) ) {
$errors = array('err' => __("You've reached the maximum open tickets allowed."));
$ost->logWarning(sprintf(_S('Ticket denied - %s'), $vars['email']),
sprintf(_S('Max open tickets (%1$d) reached for %2$s'),
$cfg->getMaxOpenTickets(), $vars['email']),
false);
return 0;
}
}
if ($vars['topicId']) {
if (($__topic=Topic::lookup($vars['topicId']))
&& $__form = $__topic->getForm()
) {
$__form = $__form->instanciate();
$__form->setSource($vars);
$vars += $__form->getFilterData();
}
}
//Init ticket filters...
$ticket_filter = new TicketFilter($origin, $vars);
// Make sure email contents should not be rejected
if ($ticket_filter
&& ($filter=$ticket_filter->shouldReject())) {
return $reject_ticket(
sprintf(_S('Ticket rejected (%s) by filter "%s"'),
$vars['email'], $filter->getName()));
}
$id=0;
$fields=array();
$fields['message'] = array('type'=>'*', 'required'=>1, 'error'=>__('Message content is required'));
switch (strtolower($origin)) {
case 'web':
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Select a help topic'));
break;
case 'staff':
$fields['deptId'] = array('type'=>'int', 'required'=>0, 'error'=>__('Department selection is required'));
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Help topic selection is required'));
$fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY'));
case 'api':
$fields['source'] = array('type'=>'string', 'required'=>1, 'error'=>__('Indicate ticket source'));
break;
case 'email':
$fields['emailId'] = array('type'=>'int', 'required'=>1, 'error'=>__('Unknown system email'));
break;
default:
# TODO: Return error message
$errors['err']=$errors['origin'] = __('Invalid ticket origin given');
}
if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
$errors['err'] =__('Missing or invalid data - check the errors and try again');
//Make sure the due date is valid
if($vars['duedate']) {
if(!$vars['time'] || strpos($vars['time'],':')===false)
$errors['time']=__('Select a time from the list');
elseif(strtotime($vars['duedate'].' '.$vars['time'])===false)
$errors['duedate']=__('Invalid due date');
elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time())
$errors['duedate']=__('Due date must be in the future');
}
if (!$errors) {
# Perform ticket filter actions on the new ticket arguments
if ($ticket_filter) $ticket_filter->apply($vars);
// Allow vars to be changed in ticket filter and applied to the user
// account created or detected
if (!$user && $vars['email'])
$user = User::lookupByEmail($vars['email']);
if (!$user) {
// Reject emails if not from registered clients (if
// configured)
if (strcasecmp($origin, 'email') === 0
&& !$cfg->acceptUnregisteredEmail()) {
list($mailbox, $domain) = explode('@', $vars['email'], 2);
// Users not yet created but linked to an organization
// are still acceptable
if (!Organization::forDomain($domain)) {
return $reject_ticket(
sprintf(_S('Ticket rejected (%s) (unregistered client)'),
$vars['email']));
}
}
$user_form = UserForm::getUserForm()->getForm($vars);
if (!$user_form->isValid($field_filter('user'))
|| !($user=User::fromVars($user_form->getClean())))
$errors['user'] = __('Incomplete client information');
}
}
if ($vars['topicId']) {
if ($topic=Topic::lookup($vars['topicId'])) {
if ($topic_form = $topic->getForm()) {
$TF = $topic_form->getForm($vars);
$topic_form = $topic_form->instanciate();
$topic_form->setSource($vars);
if (!$TF->isValid($field_filter('topic')))
$errors = array_merge($errors, $TF->errors());
}
}
else {
$errors['topicId'] = 'Invalid help topic selected';
}
}
// Any error above is fatal.
if ($errors)
return 0;
Signal::send('ticket.create.validated', null, $vars);
# Some things will need to be unpacked back into the scope of this
# function
if (isset($vars['autorespond']))
$autorespond = $vars['autorespond'];
# Apply filter-specific priority
if ($vars['priorityId'])
$form->setAnswer('priority', null, $vars['priorityId']);
// If the filter specifies a help topic which has a form associated,
// and there was previously either no help topic set or the help
// topic did not have a form, there's no need to add it now as (1)
// validation is closed, (2) there may be a form already associated
// and filled out from the original help topic, and (3) staff
// members can always add more forms now
// OK...just do it.
$statusId = $vars['statusId'];
$deptId = $vars['deptId']; //pre-selected Dept if any.
$source = ucfirst($vars['source']);
// Apply email settings for emailed tickets. Email settings should
// trump help topic settins if the email has an associated help
// topic
if ($vars['emailId'] && ($email=Email::lookup($vars['emailId']))) {
$deptId = $deptId ?: $email->getDeptId();
$priority = $form->getAnswer('priority');
if (!$priority || !$priority->getIdValue())
$form->setAnswer('priority', null, $email->getPriorityId());
if ($autorespond)
$autorespond = $email->autoRespond();
if (!isset($topic)
&& ($T = $email->getTopic())
&& ($T->isActive())) {
$topic = $T;
}
$email = null;
$source = 'Email';
}
if (!isset($topic)) {
// This may return NULL, no big deal
$topic = $cfg->getDefaultTopic();
}
// Intenal mapping magic...see if we need to override anything
if (isset($topic)) {
$deptId = $deptId ?: $topic->getDeptId();
$statusId = $statusId ?: $topic->getStatusId();
$priority = $form->getAnswer('priority');
if (!$priority || !$priority->getIdValue())
$form->setAnswer('priority', null, $topic->getPriorityId());
if ($autorespond)
$autorespond = $topic->autoRespond();
//Auto assignment.
if (!isset($vars['staffId']) && $topic->getStaffId())
$vars['staffId'] = $topic->getStaffId();
elseif (!isset($vars['teamId']) && $topic->getTeamId())
$vars['teamId'] = $topic->getTeamId();
//set default sla.
if (isset($vars['slaId']))
$vars['slaId'] = $vars['slaId'] ?: $cfg->getDefaultSLAId();
elseif ($topic && $topic->getSLAId())
$vars['slaId'] = $topic->getSLAId();
}
// Auto assignment to organization account manager
if (($org = $user->getOrganization())
&& $org->autoAssignAccountManager()
&& ($code = $org->getAccountManagerId())) {
if (!isset($vars['staffId']) && $code[0] == 's')
$vars['staffId'] = substr($code, 1);
elseif (!isset($vars['teamId']) && $code[0] == 't')
$vars['teamId'] = substr($code, 1);
}
// Last minute checks
$priority = $form->getAnswer('priority');
if (!$priority || !$priority->getIdValue())
$form->setAnswer('priority', null, $cfg->getDefaultPriorityId());
$deptId = $deptId ?: $cfg->getDefaultDeptId();
$statusId = $statusId ?: $cfg->getDefaultTicketStatusId();
$topicId = isset($topic) ? $topic->getId() : 0;
$ipaddress = $vars['ip'] ?: $_SERVER['REMOTE_ADDR'];
$source = $source ?: 'Web';
//We are ready son...hold on to the rails.
$number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber();
$sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
.' ,lastupdate= NOW() '
.' ,lastmessage= NOW()'
.' ,user_id='.db_input($user->getId())
.' ,`number`='.db_input($number)
.' ,dept_id='.db_input($deptId)
.' ,topic_id='.db_input($topicId)
.' ,status_id='.db_input($statusId)
.' ,ip_address='.db_input($ipaddress)
.' ,source='.db_input($source);
if (isset($vars['emailId']) && $vars['emailId'])
$sql.=', email_id='.db_input($vars['emailId']);
//Make sure the origin is staff - avoid firebug hack!
if($vars['duedate'] && !strcasecmp($origin,'staff'))
$sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time'])));
if(!db_query($sql)
|| !($id=db_insert_id())
|| !($thread=TicketThread::create($id))
|| !($ticket =Ticket::lookup($id)))
return null;
/* -------------------- POST CREATE ------------------------ */
// Save the (common) dynamic form
// Ensure we have a subject
$subject = $form->getAnswer('subject');
if ($subject && !$subject->getValue()) {
if ($topic) {
$form->setAnswer('subject', $topic->getFullName());
}
}
$form->setTicketId($id);
$form->save();
// Save the form data from the help-topic form, if any
if ($topic_form) {
$topic_form->setTicketId($id);
$topic_form->save();
}
$ticket->loadDynamicData();
$dept = $ticket->getDept();
// Add organizational collaborators
if ($org && $org->autoAddCollabs()) {
$pris = $org->autoAddPrimaryContactsAsCollabs();
$members = $org->autoAddMembersAsCollabs();
$settings = array('isactive' => true);
$collabs = array();
foreach ($org->allMembers() as $u) {
if ($members || ($pris && $u->isPrimaryContact())) {
if ($c = $ticket->addCollaborator($u, $settings, $errors)) {
$collabs[] = (string) $c;
}
}
}
//TODO: Can collaborators add others?
if ($collabs) {
//TODO: Change EndUser to name of user.
$ticket->logNote(sprintf(_S('Collaborators for %s organization added'),
$org->getName()),
implode("<br>", $collabs), $org->getName(), false);
}
}
//post the message.
$vars['title'] = $vars['subject']; //Use the initial subject as title of the post.
$vars['userId'] = $ticket->getUserId();
$message = $ticket->postMessage($vars , $origin, false);
// Configure service-level-agreement for this ticket
$ticket->selectSLAId($vars['slaId']);
// Assign ticket to staff or team (new ticket by staff)
if($vars['assignId']) {
$ticket->assign($vars['assignId'], $vars['note']);
}
else {
// Auto assign staff or team - auto assignment based on filter
// rules. Both team and staff can be assigned
if ($vars['staffId'])
$ticket->assignToStaff($vars['staffId'], _S('Auto Assignment'));
if ($vars['teamId'])
$ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'));
}
// Update the estimated due date in the database
$ticket->updateEstDueDate();
/********** double check auto-response ************/
//Override auto responder if the FROM email is one of the internal emails...loop control.
if($autorespond && (Email::getIdByEmail($ticket->getEmail())))
$autorespond=false;
# Messages that are clearly auto-responses from email systems should
# not have a return 'ping' message
if (isset($vars['flags']) && $vars['flags']['bounce'])
$autorespond = false;
if ($autorespond && $message->isAutoReply())
$autorespond = false;
//post canned auto-response IF any (disables new ticket auto-response).
if ($vars['cannedResponseId']
&& $ticket->postCannedReply($vars['cannedResponseId'], $message->getId(), $autorespond)) {
$ticket->markUnAnswered(); //Leave the ticket as unanswred.
$autorespond = false;
}
//Check department's auto response settings
// XXX: Dept. setting doesn't affect canned responses.
if($autorespond && $dept && !$dept->autoRespONNewTicket())
$autorespond=false;
//Don't send alerts to staff when the message is a bounce
// this is necessary to avoid possible loop (especially on new ticket)
if ($alertstaff && $message->isBounce())
$alertstaff = false;
/***** See if we need to send some alerts ****/
$ticket->onNewTicket($message, $autorespond, $alertstaff);
/************ check if the user JUST reached the max. open tickets limit **********/
if($cfg->getMaxOpenTickets()>0
&& ($user=$ticket->getOwner())
&& ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
$ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff')));
}
/* Start tracking ticket lifecycle events */
$ticket->logEvent('created');
// Fire post-create signal (for extra email sending, searching)
Signal::send('model.created', $ticket);
/* Phew! ... time for tea (KETEPA) */
return $ticket;
}
/* routine used by staff to open a new ticket */
static function open($vars, &$errors) {
global $thisstaff, $cfg;
if (!$thisstaff || !$thisstaff->canCreateTickets()) return false;
if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other')))
$errors['source']=sprintf(__('Invalid source given - %s'),Format::htmlchars($vars['source']));
if (!$vars['uid']) {
//Special validation required here
if (!$vars['email'] || !Validator::is_email($vars['email']))
$errors['email'] = __('Valid email address is required');
if (!$vars['name'])
$errors['name'] = __('Name is required');
}
if (!$thisstaff->canAssignTickets())
unset($vars['assignId']);
//TODO: Deny action based on selected department.
$create_vars = $vars;
$tform = TicketForm::objects()->one()->getForm($create_vars);
$create_vars['cannedattachments']
= $tform->getField('message')->getWidget()->getAttachments()->getClean();
if(!($ticket=Ticket::create($create_vars, $errors, 'staff', false)))
return false;
$vars['msgId']=$ticket->getLastMsgId();
// Effective role for the department
$role = $thisstaff->getRole($ticket->getDeptId());
// post response - if any
$response = null;
if($vars['response'] && $role->canPostReply()) {
$vars['response'] = $ticket->replaceVars($vars['response']);
// $vars['cannedatachments'] contains the attachments placed on
// the response form.
if(($response=$ticket->postReply($vars, $errors, false))) {
//Only state supported is closed on response
if(isset($vars['ticket_state']) && $role->canCloseTickets())
$ticket->setState($vars['ticket_state']);
}
}
// Not assigned...save optional note if any
if (!$vars['assignId'] && $vars['note']) {
$ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false);
}
else {
// Not assignment and no internal note - log activity
$ticket->logActivity(_S('New Ticket by Agent'),
sprintf(_S('Ticket created by agent - %s'), $thisstaff->getName()));
}
$ticket->reload();
if(!$cfg->notifyONNewStaffTicket()
|| !isset($vars['alertuser'])
|| !($dept=$ticket->getDept()))
return $ticket; //No alerts.
//Send Notice to user --- if requested AND enabled!!
if(($tpl=$dept->getTemplate())
&& ($msg=$tpl->getNewTicketNoticeMsgTemplate())
&& ($email=$dept->getEmail())) {
$message = (string) $ticket->getLastMessage();
if($response) {
$message .= ($cfg->isHtmlThreadEnabled()) ? "<br><br>" : "\n\n";
$message .= $response->getBody();
}
if($vars['signature']=='mine')
$signature=$thisstaff->getSignature();
elseif($vars['signature']=='dept' && $dept && $dept->isPublic())
$signature=$dept->getSignature();
else
$signature='';
$attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array();
$msg = $ticket->replaceVars($msg->asArray(),
array(
'message' => $message,
'signature' => $signature,
'response' => ($response) ? $response->getBody() : '',
'recipient' => $ticket->getOwner(), //End user
'staff' => $thisstaff,
)
);
$references = $ticket->getLastMessage()->getEmailMessageId();
if (isset($response))
$references = array($response->getEmailMessageId(), $references);
$options = array(
'references' => $references,
'thread' => $ticket->getLastMessage()
);
$email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments,
$options);
}
return $ticket;
}
function checkOverdue() {
$sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 '
.' INNER JOIN '.TICKET_STATUS_TABLE.' status
ON (status.id=T1.status_id AND status.state="open") '
.' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) '
.' WHERE isoverdue=0 '
.' AND ((reopened is NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),T1.created))>=T2.grace_period*3600) '
.' OR (reopened is NOT NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),reopened))>=T2.grace_period*3600) '
.' OR (duedate is NOT NULL AND duedate<NOW()) '
.' ) ORDER BY T1.created LIMIT 50'; //Age upto 50 tickets at a time?
if(($res=db_query($sql)) && db_num_rows($res)) {
while(list($id)=db_fetch_row($res)) {
if(($ticket=Ticket::lookup($id)) && $ticket->markOverdue())
$ticket->logActivity(_S('Ticket Marked Overdue'),
_S('Ticket flagged as overdue by the system.'));
}
} else {
//TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period.
}
}
}
?>