Newer
Older
if(!$email || !Validator::is_email($email))
return null;
if (!$user = User::lookup(array('emails__address'=>$email)))
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...
Peter Rotich
committed
*/
function create($vars, &$errors, $origin, $autorespond=true, $alertstaff=true) {
global $ost, $cfg, $thisclient, $_FILES;
// Don't enforce form validation for email
$field_filter = function($f) use ($origin) {
// 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 'web':
return !$f->get('private');
default:
return true;
}
};
//Check for 403
if ($vars['email'] && Validator::is_email($vars['email'])) {
//Make sure the email address is not banned
if(TicketFilter::isBanned($vars['email'])) {
$errors['err']='Ticket denied. Error #403';
$ost->logWarning('Ticket denied', 'Banned email - '.$vars['email']);
return 0;
}
//Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
Peter Rotich
committed
if($cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff')
&& ($client=Client::lookupByEmail($vars['email']))
&& ($openTickets=$client->getNumOpenTickets())
&& ($openTickets>=$cfg->getMaxOpenTickets()) ) {
$errors['err']="You've reached the maximum open tickets allowed.";
Peter Rotich
committed
$ost->logWarning('Ticket denied -'.$vars['email'],
sprintf('Max open tickets (%d) reached for %s ',
$cfg->getMaxOpenTickets(), $vars['email']));
// Create and verify the dynamic form entry for the new ticket
$form = TicketForm::getNewInstance();
// If submitting via email, ensure we have a subject and such
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))
$errors += $form->errors();
// Unpack dynamic variables into $vars for filter application
foreach ($form->getFields() as $f)
$vars['field.'.$f->get('id')] = $f->toString($f->getClean());
// Unpack the basic user information
$interesting = array('name', 'email');
$user_form = UserForm::getUserForm()->getForm($vars);
foreach ($user_form->getFields() as $f)
if (in_array($f->get('name'), $interesting))
$vars[$f->get('name')] = $f->toString($f->getClean());
//Init ticket filters...
$ticket_filter = new TicketFilter($origin, $vars);
// Make sure email contents should not be rejected
Peter Rotich
committed
if($ticket_filter
&& ($filter=$ticket_filter->shouldReject())) {
$errors['err']='Ticket denied. Error #403';
Peter Rotich
committed
$ost->logWarning('Ticket denied',
sprintf('Ticket rejected ( %s) by filter "%s"',
$vars['email'], $filter->getName()));
$id=0;
$fields=array();
$fields['message'] = array('type'=>'text', 'required'=>1, 'error'=>'Message required');
switch (strtolower($origin)) {
case 'web':
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Select help topic');
break;
case 'staff':
$fields['deptId'] = array('type'=>'int', 'required'=>1, 'error'=>'Dept. required');
$fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Topic required');
$fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY');
case 'api':
$fields['source'] = array('type'=>'string', 'required'=>1, 'error'=>'Indicate source');
break;
case 'email':
$fields['emailId'] = array('type'=>'int', 'required'=>1, 'error'=>'Email unknown');
break;
default:
# TODO: Return error message
$errors['err']=$errors['origin'] = 'Invalid origin given';
Peter Rotich
committed
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 time';
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) {
if ($vars['uid'] && ($user = User::lookup($vars['uid']))) {
$vars['email'] = $user->getEmail();
$vars['name'] = $user->getName();
}
# 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) {
$user_form = UserForm::getUserForm()->getForm($vars);
if (!$user_form->isValid($field_filter)
|| !($user=User::fromVars($user_form->getClean())))
$errors['user'] = 'Incomplete client information';
}
//Any error above is fatal.
if($errors) return 0;
# Some things will need to be unpacked back into the scope of this
# function
if (isset($vars['autorespond'])) $autorespond=$vars['autorespond'];
// OK...just do it.
$deptId=$vars['deptId']; //pre-selected Dept if any.
$source=ucfirst($vars['source']);
$topic=NULL;
// Intenal mapping magic...see if we need to override anything
if(isset($vars['topicId']) && ($topic=Topic::lookup($vars['topicId']))) { //Ticket created via web by user/or staff
$deptId=$deptId?$deptId:$topic->getDeptId();
$priority = $form->getAnswer('priority');
if (!$priority || !$priority->getIdValue())
$form->setAnswer('priority', null, $topic->getPriorityId());
if($autorespond) $autorespond=$topic->autoRespond();
$source=$vars['source']?$vars['source']:'Web';
//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']?$vars['slaId']:$cfg->getDefaultSLAId();
elseif($topic && $topic->getSLAId())
$vars['slaId'] = $topic->getSLAId();
}elseif($vars['emailId'] && !$vars['deptId'] && ($email=Email::lookup($vars['emailId']))) { //Emailed Tickets
$deptId=$email->getDeptId();
$priority = $form->getAnswer('priority');
if (!$priority || !$priority->getIdValue())
$form->setAnswer('priority', null, $email->getPriorityId());
if($autorespond) $autorespond=$email->autoRespond();
$email=null;
$source='Email';
}
//Last minute checks
$priority = $form->getAnswer('priority');
if (!$priority || !$priority->getIdValue())
$form->setAnswer('priority', null, $cfg->getDefaultPriorityId());
$deptId=$deptId?$deptId:$cfg->getDefaultDeptId();
$topicId=$vars['topicId']?$vars['topicId']:0;
$ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR'];
Peter Rotich
committed
//We are ready son...hold on to the rails.
$extId=Ticket::genExtRandID();
$sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
.' ,lastmessage= NOW()'
.' ,user_id='.db_input($user->id)
.' ,ticketID='.db_input($extId)
.' ,dept_id='.db_input($deptId)
.' ,topic_id='.db_input($topicId)
Peter Rotich
committed
.' ,ip_address='.db_input($ipaddress)
.' ,source='.db_input($source);
//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()) || !($ticket =Ticket::lookup($id)))
return null;
/* -------------------- POST CREATE ------------------------ */
Peter Rotich
committed
if(!$cfg->useRandomIds()) {
//Sequential ticketIDs support really..really suck arse.
$extId=$id; //To make things really easy we are going to use autoincrement ticket_id.
Peter Rotich
committed
db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1');
//TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable]
// Save the (common) dynamic form
$form->setTicketId($id);
$form->save();
$dept = $ticket->getDept();
unset($vars['cannedattachments']); //Ticket::open() might have it set as part of open & respond.
$vars['title'] = $vars['subject']; //Use the initial subject as title of the post.
$message = $ticket->postMessage($vars , $origin, false);
// Configure service-level-agreement for this ticket
$ticket->selectSLAId($vars['slaId']);
//Auto assign staff or team - auto assignment based on filter rules.
if($vars['staffId'] && !$vars['assignId'])
$ticket->assignToStaff($vars['staffId'], 'Auto Assignment');
$ticket->assignToTeam($vars['teamId'], 'Auto Assignment');
/********** 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 ($autorespond && $message && $message->isAutoResponse())
$autorespond=false;
//Don't auto respond to mailer daemons.
if( $autorespond &&
(strpos(strtolower($vars['email']),'mailer-daemon@')!==false
|| strpos(strtolower($vars['email']),'postmaster@')!==false)) {
$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.
//Check department's auto response settings
// XXX: Dept. setting doesn't affect canned responses.
if($autorespond && $dept && !$dept->autoRespONNewTicket())
$autorespond=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
&& ($client=$ticket->getClient())
&& ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
$ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff')));
}
Peter Rotich
committed
/* Start tracking ticket lifecycle events */
$ticket->logEvent('created');
/* Phew! ... time for tea (KETEPA) */
function open($vars, &$errors) {
Peter Rotich
committed
global $thisstaff, $cfg;
if(!$thisstaff || !$thisstaff->canCreateTickets()) return false;
if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other')))
$errors['source']='Invalid source - '.Format::htmlchars($vars['source']);
if (!$vars['uid']) {
//Special validation required here
if (!$vars['email'] || !Validator::is_email($vars['email']))
$errors['email'] = 'Valid email required';
if (!$vars['name'])
$errors['name'] = 'Name required';
}
if(!($ticket=Ticket::create($vars, $errors, 'staff', false, (!$vars['assignId']))))
return false;
$vars['msgId']=$ticket->getLastMsgId();
$response = null;
if($vars['response'] && $thisstaff->canPostReply()) {
$vars['response'] = $ticket->replaceVars($vars['response']);
if(($response=$ticket->postReply($vars, $errors, false))) {
//Only state supported is closed on response
if(isset($vars['ticket_state']) && $thisstaff->canCloseTickets())
$ticket->setState($vars['ticket_state']);
}
}
Peter Rotich
committed
if($vars['assignId'] && $thisstaff->canAssignTickets()) { //Assign ticket to staff or team.
$ticket->assign($vars['assignId'], $vars['note']);
} elseif($vars['note']) { //Not assigned...save optional note if any
$ticket->logNote('New Ticket', $vars['note'], $thisstaff, false);
} else { //Not assignment and no internal note - log activity
$ticket->logActivity('New Ticket by Staff','Ticket created by staff -'.$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())) {
Peter Rotich
committed
$message = $vars['message'];
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='';
Peter Rotich
committed
$attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array();
$msg = $ticket->replaceVars($msg->asArray(), array(
'message' => $message,
'signature' => $signature,
'response' => ($response) ? $response->getBody() : '',
if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator())))
$msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body'];
$references = $ticket->getLastMessage()->getEmailMessageId();
if (isset($response))
$references = array($response->getEmailMessageId(), $references);
$options = array('references' => $references);
$email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments,
$options);
Peter Rotich
committed
Peter Rotich
committed
Peter Rotich
committed
$sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 '
.' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) '
.' WHERE status=\'open\' AND 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('Ticket Marked Overdue', 'Ticket flagged as overdue by the system.');
} else {
//TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period.
Peter Rotich
committed