diff --git a/include/api.tickets.php b/include/api.tickets.php index 7346f2b2943bbc5c0451d62fcc7952677034b6d5..1cc93d995fc52a563b6c8ce826137b4f198522c5 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -12,9 +12,9 @@ class TicketApiController extends ApiController { $supported = array( "alert", "autorespond", "source", "topicId", "name", "email", "subject", "phone", "phone_ext", - "attachments" => array("*" => + "attachments" => array("*" => array("name", "type", "data", "encoding") - ), + ), "message", "ip", "priorityId" ); @@ -24,6 +24,37 @@ class TicketApiController extends ApiController { return $supported; } + /* + Validate data - overwrites parent's validator for additional validations. + */ + function validate(&$data, $format) { + global $ost; + + //Call parent to Validate the structure + if(!parent::validate($data, $format)) + $this->exerr(400, 'Unexpected or invalid data received'); + + //Nuke attachments IF API files are not allowed. + if(!$ost->getConfig()->allowAPIAttachments()) + $data['attachments'] = array(); + + //Validate attachments: Do error checking... soft fail - set the error and pass on the request. + if($data['attachments'] && is_array($data['attachments'])) { + foreach($data['attachments'] as &$attachment) { + if(!$ost->isFileTypeAllowed($attachment)) + $data['error'] = 'Invalid file type (ext) for '.Format::htmlchars($attachment['name']); + elseif ($attachment['encoding'] && !strcasecmp($attachment['encoding'], 'base64')) { + if(!($attachment['data'] = base64_decode($attachment['data'], true))) + $attachment['error'] = sprintf('%s: Poorly encoded base64 data', Format::htmlchars($attachment['name'])); + } + } + unset($attachment); + } + + return true; + } + + function create($format) { if(!($key=$this->requireApiKey()) || !$key->canCreateTickets()) @@ -62,7 +93,7 @@ class TicketApiController extends ApiController { return $this->exerr(403, 'Ticket denied'); else return $this->exerr( - 400, + 400, "Unable to create new ticket: validation errors:\n" .Format::array_implode(": ", "\n", $errors) ); @@ -70,11 +101,6 @@ class TicketApiController extends ApiController { return $this->exerr(500, "Unable to create new ticket: unknown error"); } - - # Save attachment(s) - if($data['attachments']) - $ticket->importAttachments($data['attachments'], $ticket->getLastMsgId(), 'M'); - return $ticket; } @@ -97,7 +123,7 @@ class PipeApiController extends TicketApiController { //Overwrite grandparent's (ApiController) response method. function response($code, $resp) { - //Use postfix exit codes - instead of HTTP + //Use postfix exit codes - instead of HTTP switch($code) { case 201: //Success $exitcode = 0; @@ -119,8 +145,8 @@ class PipeApiController extends TicketApiController { $exitcode = 69; break; case 500: //Server error. - default: //Temp (unknown) failure - retry - $exitcode = 75; + default: //Temp (unknown) failure - retry + $exitcode = 75; } //echo "$code ($exitcode):$resp"; diff --git a/include/class.api.php b/include/class.api.php index 6fc0445e5ef66c2da6eaf925af5027c52a6c0fdb..d22be61100f5dff66663107377f0b9abffe37df2 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -192,6 +192,7 @@ class ApiController { * work will be done for XML requests */ function getRequest($format) { + global $ost; $input = (substr(php_sapi_name(), 0, 3) == 'cli')?'php://stdin':'php://input'; @@ -219,7 +220,8 @@ class ApiController { if (!($data = $parser->parse($stream))) $this->exerr(400, $parser->lastError()); - $this->validate($data, $this->getRequestStructure($format)); + //Validate structure of the request. + $this->validate($data, $format); return $data; } @@ -239,19 +241,33 @@ class ApiController { * expected. It is assumed that the functions actually implementing the * API will further validate the contents of the request */ - function validate($data, $structure, $prefix="") { + function validateRequestStructure($data, $structure, $prefix="") { + foreach ($data as $key=>$info) { if (is_array($structure) and is_array($info)) { $search = (isset($structure[$key]) && !is_numeric($key)) ? $key : "*"; if (isset($structure[$search])) { - $this->validate($info, $structure[$search], "$prefix$key/"); + $this->validateRequestStructure($info, $structure[$search], "$prefix$key/"); continue; } } elseif (in_array($key, $structure)) { continue; } - $this->exerr(400, "$prefix$key: Unexpected data received"); + return $this->exerr(400, "$prefix$key: Unexpected data received"); } + + return true; + } + + /** + * Validate request. + * + */ + function validate(&$data, $format) { + return $this->validateRequestStructure( + $data, + $this->getRequestStructure($format) + ); } /** diff --git a/include/class.config.php b/include/class.config.php index a445f952d277c94850d8b4b6de81ff58b165838d..e6bed4657f69b4c432e617a750042ad3271b3611 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -2,7 +2,7 @@ /********************************************************************* class.config.php - osTicket config info manager. + osTicket config info manager. Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket @@ -17,17 +17,17 @@ require_once(INCLUDE_DIR.'class.email.php'); class Config { - + var $id = 0; var $config = array(); - var $defaultDept; //Default Department + var $defaultDept; //Default Department var $defaultSLA; //Default SLA - var $defaultEmail; //Default Email + var $defaultEmail; //Default Email var $alertEmail; //Alert Email var $defaultSMTPEmail; //Default SMTP Email - function Config($id) { + function Config($id) { $this->load($id); } @@ -39,11 +39,11 @@ class Config { $sql='SELECT *, (TIME_TO_SEC(TIMEDIFF(NOW(), UTC_TIMESTAMP()))/3600) as db_tz_offset ' .' FROM '.CONFIG_TABLE .' WHERE id='.db_input($id); - + if(!($res=db_query($sql)) || !db_num_rows($res)) return false; - + $this->config = db_fetch_array($res); $this->id = $this->config['id']; @@ -100,7 +100,7 @@ class Config { return null; } - + function getDBTZoffset() { return $this->config['db_tz_offset']; } @@ -135,15 +135,15 @@ class Config { function getConfigInfo() { return $this->config; } - + function getTitle() { return $this->config['helpdesk_title']; } - + function getUrl() { - return $this->config['helpdesk_url']; + return $this->config['helpdesk_url']; } - + function getBaseUrl() { //Same as above with no trailing slash. return rtrim($this->getUrl(),'/'); } @@ -171,11 +171,11 @@ class Config { function showNotesInline(){ return $this->config['show_notes_inline']; } - + function getClientTimeout() { return $this->getClientSessionTimeout(); } - + function getClientSessionTimeout() { return $this->config['client_session_timeout']*60; } @@ -191,8 +191,8 @@ class Config { function getStaffTimeout() { return $this->getStaffSessionTimeout(); } - - function getStaffSessionTimeout() { + + function getStaffSessionTimeout() { return $this->config['staff_session_timeout']*60; } @@ -218,7 +218,7 @@ class Config { $this->defaultDept=Dept::lookup($this->getDefaultDeptId()); return $this->defaultDept; - } + } function getDefaultEmailId() { return $this->config['default_email_id']; @@ -280,7 +280,7 @@ class Config { } function getDefaultTemplate() { - + if(!$this->defaultTemplate && $this->getDefaultTemplateId()) $this->defaultTemplate = Template::lookup($this->getDefaultTemplateId()); @@ -319,7 +319,7 @@ class Config { function clickableURLS() { return ($this->config['clickable_urls']); } - + function enableStaffIPBinding() { return ($this->config['staff_ip_binding']); } @@ -335,12 +335,12 @@ class Config { function isEmailPollingEnabled() { return ($this->config['enable_mail_polling']); } - + function allowPriorityChange() { return ($this->config['allow_priority_change']); } - + function useEmailPriority() { return ($this->config['use_email_priority']); } @@ -352,7 +352,7 @@ class Config { function getReplySeparator() { return $this->config['reply_separator']; } - + function stripQuotedReply() { return ($this->config['strip_quoted_reply']); } @@ -360,7 +360,7 @@ class Config { function saveEmailHeaders() { return true; //No longer an option...hint: big plans for headers coming!! } - + function useRandomIds() { return ($this->config['random_ticket_ids']); } @@ -369,7 +369,7 @@ class Config { function autoRespONNewTicket() { return ($this->config['ticket_autoresponder']); } - + function autoRespONNewMessage() { return ($this->config['message_autoresponder']); } @@ -385,11 +385,11 @@ class Config { function alertLastRespondentONNewMessage() { return ($this->config['message_alert_laststaff']); } - + function alertAssignedONNewMessage() { return ($this->config['message_alert_assigned']); } - + function alertDeptManagerONNewMessage() { return ($this->config['message_alert_dept_manager']); } @@ -417,7 +417,7 @@ class Config { function alertAdminONNewTicket() { return ($this->config['ticket_alert_admin']); } - + function alertDeptManagerONNewTicket() { return ($this->config['ticket_alert_dept_manager']); } @@ -433,11 +433,11 @@ class Config { function alertAssignedONTransfer() { return ($this->config['transfer_alert_assigned']); } - + function alertDeptManagerONTransfer() { return ($this->config['transfer_alert_dept_manager']); } - + function alertDeptMembersONTransfer() { return ($this->config['transfer_alert_dept_members']); } @@ -486,7 +486,7 @@ class Config { function showAnsweredTickets() { return ($this->config['show_answered_tickets']); } - + function hideStaffName() { return ($this->config['hide_staff_name']); } @@ -494,10 +494,10 @@ class Config { function sendOverLimitNotice() { return ($this->config['overlimit_notice_active']); } - + /* Error alerts sent to admin email when enabled */ function alertONSQLError() { - return ($this->config['send_sql_errors']); + return ($this->config['send_sql_errors']); } function alertONLoginError() { return ($this->config['send_login_errors']); @@ -507,7 +507,7 @@ class Config { return ($this->config['send_mailparse_errors']); } - + /* Attachments */ function getAllowedFileTypes() { @@ -529,21 +529,27 @@ class Config { function allowAttachmentsOnlogin() { return ($this->allowOnlineAttachments() && $this->config['allow_online_attachments_onlogin']); } - + function allowEmailAttachments() { return ($this->allowAttachments() && $this->config['allow_email_attachments']); } + //TODO: change db field to allow_api_attachments - which will include email/json/xml attachments + // terminology changed on the UI + function allowAPIAttachments() { + return $this->allowEmailAttachments(); + } + /* Needed by upgrader on 1.6 and older releases upgrade - not not remove */ function getUploadDir() { return $this->config['upload_dir']; } - + function updateSettings($vars, &$errors) { if(!$vars || $errors) return false; - + switch(strtolower($vars['t'])) { case 'system': return $this->updateSystemSettings($vars, $errors); @@ -702,10 +708,10 @@ class Config { $f['default_email_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default email required'); $f['alert_email_id']=array('type'=>'int', 'required'=>1, 'error'=>'Selection required'); $f['admin_email']=array('type'=>'email', 'required'=>1, 'error'=>'System admin email required'); - + if($vars['strip_quoted_reply'] && !$vars['reply_separator']) $errors['reply_separator']='Reply separator required to strip quoted reply.'; - + if($vars['admin_email'] && Email::getIdByEmail($vars['admin_email'])) //Make sure admin email is not also a system email. $errors['admin_email']='Email already setup as system email'; @@ -724,7 +730,7 @@ class Config { .' WHERE id='.db_input($this->getId()); - + return (db_query($sql)); } @@ -732,16 +738,16 @@ class Config { if($vars['allow_attachments']) { - + if(!ini_get('file_uploads')) $errors['err']='The \'file_uploads\' directive is disabled in php.ini'; - + if(!is_numeric($vars['max_file_size'])) - $errors['max_file_size']='Maximum file size required'; - + $errors['max_file_size']='Maximum file size required'; + if(!$vars['allowed_filetypes']) $errors['allowed_filetypes']='Allowed file extentions required'; - + if(!($maxfileuploads=ini_get('max_file_uploads'))) $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS; @@ -842,7 +848,7 @@ class Config { } if($errors) return false; - + $sql= 'UPDATE '.CONFIG_TABLE.' SET updated=NOW() ' .',ticket_alert_active='.db_input($vars['ticket_alert_active']) .',ticket_alert_admin='.db_input(isset($vars['ticket_alert_admin'])?1:0) diff --git a/include/class.faq.php b/include/class.faq.php index f902dd0f0f4c1412896a319b95fee45730e2ede8..447719fbda62e1eac0fd3fe68cc66cbefc069352 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -170,7 +170,7 @@ class FAQ { } //Upload new attachments IF any. - if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments']))) $this->uploadAttachments($files); $this->reload(); @@ -282,7 +282,7 @@ class FAQ { if(($faq=self::lookup($id))) { $faq->updateTopics($vars['topics']); - if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments']))) $faq->uploadAttachments($files); $faq->reload(); diff --git a/include/class.file.php b/include/class.file.php index 27a8881608ef4bda37a3c2a8be0fb838973626ef..6908dd8eec6efb4e2a24a0618fd1945471859878 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -210,6 +210,53 @@ class AttachmentFile { return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null; } + + /* + Method formats http based $_FILE uploads - plus basic validation. + @restrict - make sure file type & size are allowed. + */ + function format($files, $restrict=false) { + global $ost; + + if(!$files || !is_array($files)) + return null; + + //Reformat $_FILE for the sane. + $attachments = array(); + foreach($files as $k => $a) { + if(is_array($a)) + foreach($a as $i => $v) + $attachments[$i][$k] = $v; + } + + //Basic validation. + foreach($attachments as $i => &$file) { + //skip no file upload "error" - why PHP calls it an error is beyond me. + if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) { + unset($attachments[$i]); + continue; + } + + if($file['error']) //PHP defined error! + $file['error'] = 'File upload error #'.$file['error']; + elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name'])) + $file['error'] = 'Invalid or bad upload POST'; + elseif($restrict) { // make sure file type & size are allowed. + if(!$ost->isFileTypeAllowed($file)) + $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']); + elseif($ost->getConfig()->getMaxFileSize() + && $file['size']>$ost->getConfig()->getMaxFileSize()) + $file['error'] = sprintf('File %s (%s) is too big. Maximum of %s allowed', + Format::htmlchars($file['name']), + Format::file_size($file['size']), + Format::file_size($ost->getConfig()->getMaxFileSize())); + } + } + unset($file); + + return array_filter($attachments); + } + /** * Removes files and associated meta-data for files which no ticket, * canned-response, or faq point to any more. diff --git a/include/class.format.php b/include/class.format.php index 1ec32b733f5c4c2c671dbdc8773f0cfaca299fbf..7f6cc957b152d2e1b28a033ad481b3166bf7a667 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -34,19 +34,6 @@ class Format { return preg_replace('/\s+/', '_', $filename); } - /* re-arrange $_FILES array for the sane */ - function files($files) { - - foreach($files as $k => $a) { - if(is_array($a)) - foreach($a as $i => $v) - $result[$i][$k] = $v; - } - - return $result?array_filter($result):$files; - } - - /* encode text into desired encoding - taking into accout charset when available. */ function encode($text, $charset=null, $encoding='utf-8') { @@ -109,24 +96,50 @@ class Format { return Format::html($html,array('safe'=>1,'balance'=>1)); } + function sanitize($text, $striptags= true) { + + //balance and neutralize unsafe tags. + $text = Format::safe_html($text); + + //If requested - strip tags with decoding disabled. + return $striptags?Format::striptags($text, false):$text; + } + function htmlchars($var) { + return Format::htmlencode($var); + } + + function htmlencode($var) { $flags = ENT_COMPAT | ENT_QUOTES; if (phpversion() >= '5.4.0') $flags |= ENT_HTML401; + return is_array($var) - ? array_map(array('Format','htmlchars'),$var) + ? array_map(array('Format','htmlencode'), $var) : htmlentities($var, $flags, 'UTF-8'); } + function htmldecode($var) { + + if(is_array($var)) + return array_map(array('Format','htmldecode'), $var); + + $flags = ENT_COMPAT; + if (phpversion() >= '5.4.0') + $flags |= ENT_HTML401; + + return html_entity_decode($var, $flags, 'UTF-8'); + } + function input($var) { - return Format::htmlchars($var); + return Format::htmlencode($var); } //Format text for display.. function display($text) { global $cfg; - $text=Format::htmlchars($text); //take care of html special chars + //make urls clickable. if($cfg && $cfg->clickableURLS() && $text) $text=Format::clickableurls($text); @@ -140,14 +153,12 @@ class Format { return nl2br($text); } - function striptags($var) { - $flags = ENT_COMPAT; - if (phpversion() >= '5.4.0') - $flags |= ENT_HTML401; - return is_array($var) - ? array_map(array('Format','striptags'),$var) - //strip all tags ...no mercy! - : strip_tags(html_entity_decode($var, $flags, 'UTF-8')); + function striptags($var, $decode=true) { + + if(is_array($var)) + return array_map(array('Format','striptags'), $var, array_fill(0, count($var), $decode)); + + return strip_tags($decode?Format::htmldecode($var):$var); } //make urls clickable. Mainly for display diff --git a/include/class.mailer.php b/include/class.mailer.php index debf2f849a5b09f9a1dd7466602999f42737dc8d..b4ec97c243718f1b86b3a669c8bc901a6c947096 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -94,7 +94,8 @@ class Mailer { //do some cleanup $to = preg_replace("/(\r\n|\r|\n)/s",'', trim($to)); $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject)); - $body = preg_replace("/(\r\n|\r)/s", "\n", trim($message)); + //We're decoding html entities here becasuse we only support plain text for now - html support comming. + $body = Format::htmldecode(preg_replace("/(\r\n|\r)/s", "\n", trim($message))); /* Message ID - generated for each outgoing email */ $messageId = sprintf('<%s%d-%s>', Misc::randCode(6), time(), diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 5253dfe818eae2e1d8c02757e8db647352cbb598..1bcf6d690cdbc244c233ae65b90ad334ed79185b 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -29,20 +29,20 @@ class MailFetcher { var $charset = 'UTF-8'; var $encodings =array('UTF-8','WINDOWS-1251', 'ISO-8859-5', 'ISO-8859-1','KOI8-R'); - + function MailFetcher($email, $charset='UTF-8') { - + if($email && is_numeric($email)) //email_id $email=Email::lookup($email); if(is_object($email)) $this->ht = $email->getMailAccountInfo(); elseif(is_array($email) && $email['host']) //hashtable of mail account info - $this->ht = $email; + $this->ht = $email; else $this->ht = null; - + $this->charset = $charset; if($this->ht) { @@ -59,12 +59,12 @@ class MailFetcher { $this->srvstr=sprintf('{%s:%d/%s', $this->getHost(), $this->getPort(), $this->getProtocol()); if(!strcasecmp($this->getEncryption(), 'SSL')) $this->srvstr.='/ssl'; - + $this->srvstr.='/novalidate-cert}'; } - //Set timeouts + //Set timeouts if(function_exists('imap_timeout')) imap_timeout(1,20); } @@ -92,7 +92,7 @@ class MailFetcher { function getUsername() { return $this->ht['username']; } - + function getPassword() { return $this->ht['password']; } @@ -112,7 +112,7 @@ class MailFetcher { } /* Core */ - + function connect() { return ($this->mbox && $this->ping())?$this->mbox:$this->open(); } @@ -123,7 +123,7 @@ class MailFetcher { /* Default folder is inbox - TODO: provide user an option to fetch from diff folder/label */ function open($box='INBOX') { - + if($this->mbox) $this->close(); @@ -157,7 +157,7 @@ class MailFetcher { function createMailbox($folder) { if(!$folder) return false; - + return imap_createmailbox($this->mbox, imap_utf7_encode($this->srvstr.trim($folder))); } @@ -187,23 +187,23 @@ class MailFetcher { $text=imap_qprint($text); break; } - + return $text; } //Convert text to desired encoding..defaults to utf8 - function mime_encode($text, $charset=null, $encoding='utf-8') { //Thank in part to afterburner + function mime_encode($text, $charset=null, $encoding='utf-8') { //Thank in part to afterburner return Format::encode($text, $charset, $encoding); } - + //Generic decoder - resulting text is utf8 encoded -> mirrors imap_utf8 function mime_decode($text, $encoding='utf-8') { - + $str = ''; $parts = imap_mime_header_decode($text); foreach ($parts as $part) $str.= $this->mime_encode($part->text, $part->charset, $encoding); - + return $str?$str:imap_utf8($text); } @@ -215,12 +215,12 @@ class MailFetcher { $mimeType = array('TEXT', 'MULTIPART', 'MESSAGE', 'APPLICATION', 'AUDIO', 'IMAGE', 'VIDEO', 'OTHER'); if(!$struct || !$struct->subtype) return 'TEXT/PLAIN'; - + return $mimeType[(int) $struct->type].'/'.$struct->subtype; } function getHeaderInfo($mid) { - + if(!($headerinfo=imap_headerinfo($this->mbox, $mid)) || !$headerinfo->from) return null; @@ -237,7 +237,7 @@ class MailFetcher { //search for specific mime type parts....encoding is the desired encoding. function getPart($mid, $mimeType, $encoding=false, $struct=null, $partNumber=false) { - + if(!$struct && $mid) $struct=@imap_fetchstructure($this->mbox, $mid); @@ -264,7 +264,7 @@ class MailFetcher { $text=''; if($struct && $struct->parts) { while(list($i, $substruct) = each($struct->parts)) { - if($partNumber) + if($partNumber) $prefix = $partNumber . '.'; if(($result=$this->getPart($mid, $mimeType, $encoding, $substruct, $prefix.($i+1)))) $text.=$result; @@ -332,29 +332,28 @@ class MailFetcher { return imap_fetchheader($this->mbox, $mid,FT_PREFETCHTEXT); } - + function getPriority($mid) { return Mail_Parse::parsePriority($this->getHeader($mid)); } function getBody($mid) { - + $body =''; if(!($body = $this->getPart($mid,'TEXT/PLAIN', $this->charset))) { if(($body = $this->getPart($mid,'TEXT/HTML', $this->charset))) { //Convert tags of interest before we striptags $body=str_replace("</DIV><DIV>", "\n", $body); $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body); - $body=Format::html($body); //Balance html tags before stripping. - $body=Format::striptags($body); //Strip tags?? + $body=Format::safe_html($body); //Balance html tags & neutralize unsafe tags. } } return $body; } - //email to ticket - function createTicket($mid) { + //email to ticket + function createTicket($mid) { global $ost; if(!($mailinfo = $this->getHeaderInfo($mid))) @@ -387,7 +386,7 @@ class MailFetcher { if($ost->getConfig()->useEmailPriority()) $vars['priorityId']=$this->getPriority($mid); - + $ticket=null; $newticket=true; //Check the subject line for possible ID. @@ -397,14 +396,14 @@ class MailFetcher { if(!($ticket=Ticket::lookupByExtId($tid, $vars['email']))) $ticket=null; } - + $errors=array(); if($ticket) { - if(!($msgid=$ticket->postMessage($vars, 'Email'))) + if(!($message=$ticket->postMessage($vars, 'Email'))) return false; } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) { - $msgid = $ticket->getLastMsgId(); + $message = $ticket->getLastMessage(); } else { //Report success if the email was absolutely rejected. if(isset($errors['errno']) && $errors['errno'] == 403) @@ -421,19 +420,22 @@ class MailFetcher { } //Save attachments if any. - if($msgid + if($message && $ost->getConfig()->allowEmailAttachments() - && ($struct = imap_fetchstructure($this->mbox, $mid)) - && $struct->parts + && ($struct = imap_fetchstructure($this->mbox, $mid)) + && $struct->parts && ($attachments=$this->getAttachments($struct))) { - + foreach($attachments as $a ) { - $file = array( - 'name' => $a['name'], - 'type' => $a['type'], - 'data' => $this->decode(imap_fetchbody($this->mbox, $mid, $a['index']), $a['encoding']) - ); - $ticket->importAttachments(array($file), $msgid, 'M'); + $file = array('name' => $a['name'], 'type' => $a['type']); + + //Check the file type + if(!$ost->isFileTypeAllowed($file)) + $file['error'] = 'Invalid file type (ext) for '.Format::htmlchars($file['name']); + else //only fetch the body if necessary TODO: Make it a callback. + $file['data'] = $this->decode(imap_fetchbody($this->mbox, $mid, $a['index']), $a['encoding']); + + $message->importAttachment($file); } } @@ -491,11 +493,11 @@ class MailFetcher { /* MailFetcher::run() - Static function called to initiate email polling + Static function called to initiate email polling */ function run() { global $ost; - + if(!$ost->getConfig()->isEmailPollingEnabled()) return; @@ -507,7 +509,7 @@ class MailFetcher { return; } - //Hardcoded error control... + //Hardcoded error control... $MAXERRORS = 5; //Max errors before we start delayed fetch attempts $TIMEOUT = 10; //Timeout in minutes after max errors is reached. diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 342fc5298b59f55a204d86248af0920e7dbe2b0e..3e412675a3ada7ff0d319b15f2e8a89f4b9cade9 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -19,16 +19,16 @@ require_once(PEAR_DIR.'Mail/mimeDecode.php'); require_once(PEAR_DIR.'Mail/RFC822.php'); class Mail_Parse { - + var $mime_message; var $include_bodies; var $decode_headers; var $decode_bodies; - + var $struct; - + function Mail_parse($mimeMessage,$includeBodies=true,$decodeHeaders=TRUE,$decodeBodies=TRUE){ - + $this->mime_message=$mimeMessage; $this->include_bodies=$includeBodies; $this->decode_headers=$decodeHeaders; @@ -42,9 +42,9 @@ class Mail_Parse { 'include_bodies'=> $this->include_bodies, 'decode_headers'=> $this->decode_headers, 'decode_bodies' => $this->decode_bodies); - $this->splitBodyHeader(); + $this->splitBodyHeader(); $this->struct=Mail_mimeDecode::decode($params); - + return (PEAR::isError($this->struct) || !(count($this->struct->headers)>1))?FALSE:TRUE; } @@ -94,7 +94,7 @@ class Mail_Parse { } return $array; } - + function getStruct(){ return $this->struct; @@ -109,8 +109,8 @@ class Mail_Parse { function getError(){ return PEAR::isError($this->struct)?$this->struct->getMessage():''; } - - + + function getFromAddressList(){ return Mail_Parse::parseAddressList($this->struct->headers['from']); } @@ -119,7 +119,7 @@ class Mail_Parse { //Delivered-to incase it was a BBC mail. return Mail_Parse::parseAddressList($this->struct->headers['to']?$this->struct->headers['to']:$this->struct->headers['delivered-to']); } - + function getCcAddressList(){ return $this->struct->headers['cc']?Mail_Parse::parseAddressList($this->struct->headers['cc']):null; } @@ -127,27 +127,27 @@ class Mail_Parse { function getMessageId(){ return $this->struct->headers['message-id']; } - + function getSubject(){ return $this->struct->headers['subject']; } - + function getBody(){ - + $body=''; if(!($body=$this->getPart($this->struct,'text/plain'))) { if(($body=$this->getPart($this->struct,'text/html'))) { //Cleanup the html. - $body=str_replace("</DIV><DIV>", "\n", $body); + $body=str_replace("</DIV><DIV>", "\n", $body); $body=str_replace(array("<br>", "<br />", "<BR>", "<BR />"), "\n", $body); - $body=Format::striptags(Format::html($body)); + $body=Format::safe_html($body); //Balance html tags & neutralize unsafe tags. } } return $body; } - + function getPart($struct,$ctypepart) { - + if($struct && !$struct->parts) { $ctype = @strtolower($struct->ctype_primary.'/'.$struct->ctype_secondary); if($ctype && strcasecmp($ctype,$ctypepart)==0) @@ -164,7 +164,7 @@ class Mail_Parse { return $data; } - + function mime_encode($text, $charset=null, $encoding='utf-8') { return Format::encode($text, $charset, $encoding); } @@ -175,15 +175,15 @@ class Mail_Parse { $part=$this->getStruct(); if($part && $part->disposition - && (!strcasecmp($part->disposition,'attachment') - || !strcasecmp($part->disposition,'inline') + && (!strcasecmp($part->disposition,'attachment') + || !strcasecmp($part->disposition,'inline') || !strcasecmp($part->ctype_primary,'image'))){ - + if(!($filename=$part->d_parameters['filename']) && $part->d_parameters['filename*']) $filename=$part->d_parameters['filename*']; //Do we need to decode? - + $file=array( - 'name' => $filename, + 'name' => $filename, 'type' => strtolower($part->ctype_primary.'/'.$part->ctype_secondary), 'data' => $this->mime_encode($part->body, $part->ctype_parameters['charset']) ); @@ -245,8 +245,9 @@ class EmailDataParser { function EmailDataParser($stream=null) { $this->stream = $stream; } - + function parse($stream) { + global $cfg; $contents =''; if(is_resource($stream)) { @@ -260,7 +261,7 @@ class EmailDataParser { $parser= new Mail_Parse($contents); if(!$parser->decode()) //Decode...returns false on decoding errors return $this->err('Email parse failed ['.$parser->getError().']'); - + $data =array(); //FROM address: who sent the email. if(($fromlist = $parser->getFromAddressList()) && !PEAR::isError($fromlist)) { @@ -293,7 +294,7 @@ class EmailDataParser { break; } } - + $data['subject'] = Format::utf8encode($parser->getSubject()); $data['message'] = Format::utf8encode(Format::stripEmptyLines($parser->getBody())); $data['header'] = $parser->getHeader(); @@ -301,8 +302,8 @@ class EmailDataParser { $data['priorityId'] = $parser->getPriority(); $data['emailId'] = $emailId; - //attachments XXX: worry about encoding?? - $data['attachments'] = $parser->getAttachments(); + if($cfg && $cfg->allowEmailAttachments()) + $data['attachments'] = $parser->getAttachments(); return $data; } diff --git a/include/class.osticket.php b/include/class.osticket.php index c357ba0e619557954045828fe9b33d32a8ad0874..4445aca6cd116d85c7f54e7f6cb3eca956131d89 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -146,33 +146,6 @@ class osTicket { return ($ext && is_array($allowed) && in_array(".$ext", $allowed)); } - /* Function expects a well formatted array - see Format::files() - It's up to the caller to reject the upload on error. - */ - function validateFileUploads(&$files, $checkFileTypes=true) { - - $errors=0; - foreach($files as &$file) { - //skip no file upload "error" - why PHP calls it an error is beyond me. - if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) continue; - - if($file['error']) //PHP defined error! - $file['error'] = 'File upload error #'.$file['error']; - elseif(!$file['tmp_name'] || !is_uploaded_file($file['tmp_name'])) - $file['error'] = 'Invalid or bad upload POST'; - elseif($checkFileTypes && !$this->isFileTypeAllowed($file)) - $file['error'] = 'Invalid file type for '.Format::htmlchars($file['name']); - elseif($file['size']>$this->getConfig()->getMaxFileSize()) - $file['error'] = sprintf('File (%s) is too big. Maximum of %s allowed', - Format::htmlchars($file['name']), - Format::file_size($this->getConfig()->getMaxFileSize())); - - if($file['error']) $errors++; - } - - return (!$errors); - } - /* Replace Template Variables */ function replaceTemplateVariables($input, $vars=array()) { diff --git a/include/class.pdf.php b/include/class.pdf.php index b0650b7796de3c2932cc1f24e0dd6527f5a1f46a..f210149346c7d36d561bdca8bc3fb9ad59771226 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -21,11 +21,11 @@ require (FPDF_DIR . 'fpdf.php'); class Ticket2PDF extends FPDF { - + var $includenotes = false; - + var $pageOffset = 0; - + var $ticket = null; function Ticket2PDF($ticket, $psize='Letter', $notes=false) { @@ -47,7 +47,7 @@ class Ticket2PDF extends FPDF function getTicket() { return $this->ticket; } - + //report header...most stuff are hard coded for now... function Header() { global $cfg; @@ -66,7 +66,7 @@ class Ticket2PDF extends FPDF $this->Cell(0, 5, 'Date & Time based on GMT '.$_SESSION['TZ_OFFSET'], 0, 1, 'R'); $this->Ln(10); } - + //Page footer baby function Footer() { global $thisstaff; @@ -94,10 +94,10 @@ class Ticket2PDF extends FPDF if(function_exists('iconv')) return iconv('UTF-8', 'windows-1252', $text); - + return utf8_encode($text); } - + function _print() { if(!($ticket=$this->getTicket())) @@ -107,7 +107,7 @@ class Ticket2PDF extends FPDF $l = 35; $c = $w-$l; - + $this->SetFont('Arial', 'B', 11); $this->cMargin = 0; $this->SetFont('Arial', 'B', 11); @@ -215,7 +215,11 @@ class Ticket2PDF extends FPDF 'R'=>array(255, 224, 179), 'N'=>array(250, 250, 210)); //Get ticket thread - if(($entries = $ticket->getThread(($this->includenotes)))) { + $types = array('M', 'R'); + if($this->includenotes) + $types[] = 'N'; + + if(($entries = $ticket->getThreadEntries($types))) { foreach($entries as $entry) { $color = $colors[$entry['thread_type']]; @@ -228,11 +232,12 @@ class Ticket2PDF extends FPDF $this->Cell($w/2, 7, $entry['poster'], 'TBR', 1, 'L', true); $this->SetFont(''); $text= $entry['body']; - if($entry['attachments'] - && ($attachments = $ticket->getAttachments($entry['id'], $entry['thread_type']))) { + if($entry['attachments'] + && ($tentry=$ticket->getThreadEntry($entry['id'])) + && ($attachments = $tentry->getAttachments())) { foreach($attachments as $attachment) $files[]= $attachment['name']; - + $text.="\nFiles Attached: [".implode(', ',$files)."]\n"; } $this->WriteText($w*2, $text, 1); @@ -240,6 +245,6 @@ class Ticket2PDF extends FPDF } } - } + } } ?> diff --git a/include/class.thread.php b/include/class.thread.php index cae40fa3b0ab1c419b1ce2443a91c3481e6e57e2..705ba5ec5c623b40844d648b5cef6ccbfc945e5b 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -3,7 +3,7 @@ class.thread.php Ticket thread - TODO: move thread related logic here from class.ticket.php + XXX: Please DO NOT add any ticket related logic! use ticket class. Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket @@ -16,6 +16,188 @@ **********************************************************************/ include_once(INCLUDE_DIR.'class.ticket.php'); +//Ticket thread. +class Thread { + + var $id; // same as ticket ID. + var $ticket; + + function Thread($ticket) { + + $this->ticket = $ticket; + + $this->id = 0; + + $this->load(); + } + + function load() { + + if(!$this->getTicketId()) + return null; + + $sql='SELECT ticket.ticket_id as id ' + .' ,count(DISTINCT attach.attach_id) as attachments ' + .' ,count(DISTINCT message.id) as messages ' + .' ,count(DISTINCT response.id) as responses ' + .' ,count(DISTINCT note.id) as notes ' + .' FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (' + .'ticket.ticket_id=attach.ticket_id) ' + .' LEFT JOIN '.TICKET_THREAD_TABLE.' message ON (' + ."ticket.ticket_id=message.ticket_id AND message.thread_type = 'M') " + .' LEFT JOIN '.TICKET_THREAD_TABLE.' response ON (' + ."ticket.ticket_id=response.ticket_id AND response.thread_type = 'R') " + .' LEFT JOIN '.TICKET_THREAD_TABLE.' note ON ( ' + ."ticket.ticket_id=note.ticket_id AND note.thread_type = 'N') " + .' WHERE ticket.ticket_id='.db_input($this->getTicketId()) + .' GROUP BY ticket.ticket_id'; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht = db_fetch_array($res); + + $this->id = $this->ht['id']; + + return true; + } + + function getId() { + return $this->id; + } + + function getTicketId() { + return $this->getTicket()?$this->getTicket()->getId():0; + } + + function getTicket() { + return $this->ticket; + } + + function getNumAttachments() { + return $this->ht['attachments']; + } + + function getNumMessages() { + return $this->ht['messages']; + } + + function getNumResponses() { + return $this->ht['responses']; + } + + function getNumNotes() { + return $this->ht['notes']; + } + + function getCount() { + return $this->getNumMessages() + $this->getNumResponses(); + } + + function getMessages() { + return $this->getEntries('M'); + } + + function getResponses() { + return $this->getEntries('R'); + } + + function getNotes() { + return $this->getEntries('N'); + } + + function getEntries($type, $order='ASC') { + + if(!$order || !in_array($order, array('DESC','ASC'))) + $order='ASC'; + + $sql='SELECT thread.* ' + .' ,count(DISTINCT attach.attach_id) as attachments ' + .' FROM '.TICKET_THREAD_TABLE.' thread ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (thread.ticket_id=attach.ticket_id + AND thread.id=attach.ref_id + AND thread.thread_type=attach.ref_type) ' + .' WHERE thread.ticket_id='.db_input($this->getTicketId()); + + if($type && is_array($type)) + $sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')'; + elseif($type) + $sql.=' AND thread.thread_type='.db_input($type); + + $sql.=' GROUP BY thread.id ' + .' ORDER BY thread.created '.$order; + + $entries = array(); + if(($res=db_query($sql)) && db_num_rows($res)) + while($rec=db_fetch_array($res)) + $entries[] = $rec; + + return $entries; + } + + function getEntry($id) { + return ThreadEntry::lookup($id, $this->getTicketId()); + } + + function addNote($vars, &$errors) { + + //Add ticket Id. + $vars['ticketId'] = $this->getTicketId(); + + return Note::create($vars, $errors); + } + + function addMessage($vars, &$errors) { + + $vars['ticketId'] = $this->getTicketId(); + $vars['staffId'] = 0; + + return Message::create($vars, $errors); + } + + function addResponse($vars, &$errors) { + + $vars['ticketId'] = $this->getTicketId(); + + return Response::create($vars, $errors); + } + + function deleteAttachments() { + + $deleted=0; + // Clear reference table + $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); + if ($res && db_affected_rows()) + $deleted = AttachmentFile::deleteOrphans(); + + return $deleted; + } + + function delete() { + + $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); + if(!$res || !db_affected_rows()) + return false; + + $this->deleteAttachments(); + + return true; + } + + /* static */ + function lookup($ticket) { + + return ($ticket + && is_object($ticket) + && ($thread = new Thread($ticket)) + && $thread->getId() + )?$thread:null; + } +} + + Class ThreadEntry { var $id; @@ -23,7 +205,10 @@ Class ThreadEntry { var $staff; var $ticket; - + + var $attachments; + + function ThreadEntry($id, $type='', $ticketId=0) { $this->load($id, $type, $ticketId); } @@ -33,12 +218,14 @@ Class ThreadEntry { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT thread.* ' + $sql='SELECT thread.*, info.* ' .' ,count(DISTINCT attach.attach_id) as attachments ' .' FROM '.TICKET_THREAD_TABLE.' thread ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id + .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info + ON (thread.id=info.message_id) ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (thread.ticket_id=attach.ticket_id + AND thread.id=attach.ref_id AND thread.thread_type=attach.ref_type) ' .' WHERE thread.id='.db_input($id); @@ -49,7 +236,7 @@ Class ThreadEntry { $sql.=' AND thread.ticket_id='.db_input($ticketId); $sql.=' GROUP BY thread.id '; - + if(!($res=db_query($sql)) || !db_num_rows($res)) return false; @@ -57,6 +244,7 @@ Class ThreadEntry { $this->id = $this->ht['id']; $this->staff = $this->ticket = null; + $this->attachments = array(); return true; } @@ -122,13 +310,186 @@ Class ThreadEntry { } function getStaff() { - + if(!$this->staff && $this->getStaffId()) $this->staff = Staff::lookup($this->getStaffId()); return $this->staff; } + function getEmailHeader() { + return $this->ht['headers']; + } + + function isAutoResponse() { + return $this->getEmailHeader()?TicketFilter::isAutoResponse($this->getEmailHeader()):false; + } + + //Web uploads - caller is expected to format, validate and set any errors. + function uploadFiles($files) { + + if(!$files || !is_array($files)) + return false; + + $uploaded=array(); + foreach($files as $file) { + if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) + continue; + + if(!$file['error'] + && ($id=AttachmentFile::upload($file)) + && $this->saveAttachment($id)) + $uploaded[]=$id; + else { + if(!$file['error']) + $error = 'Unable to upload file - '.$file['name']; + elseif(is_numeric($file['error'])) + $error ='Error #'.$file['error']; //TODO: Transplate to string. + else + $error = $file['error']; + /* + Log the error as an internal note. + XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!) + XXX: logNote must watch for possible loops + */ + $this->getTicket()->logNote('File Upload Error', $error, 'SYSTEM', false); + } + + } + + return $uploaded; + } + + function importAttachments($attachments) { + + if(!$attachments || !is_array($attachments)) + return null; + + $files = array(); + foreach($attachments as $attachment) + if(($id=$this->importAttachment($attachment))) + $files[] = $id; + + return $files; + } + + /* Emailed & API attachments handler */ + function importAttachment($attachment) { + + if(!$attachment || !is_array($attachment)) + return null; + + $id=0; + if (!$attachment['error'] && ($id=$this->saveAttachment($attachment))) + $files[] = $id; + else { + $error = $attachment['error']; + + if(!$error) + $error = 'Unable to import attachment - '.$attachment['name']; + + $this->getTicket()->logNote('File Import Error', $error, 'SYSTEM', false); + } + + return $id; + } + + /* + Save attachment to the DB. + @file is a mixed var - can be ID or file hashtable. + */ + function saveAttachment($file) { + + if(!($fileId=is_numeric($file)?$file:AttachmentFile::save($file))) + return 0; + + $sql ='INSERT INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' + .' ,file_id='.db_input($fileId) + .' ,ticket_id='.db_input($this->getTicketId()) + .' ,ref_id='.db_input($this->getId()) + .' ,ref_type='.db_input($this->getType()); + + return (db_query($sql) && ($id=db_insert_id()))?$id:0; + } + + function saveAttachments($files) { + $ids=array(); + foreach($files as $file) + if(($id=$this->saveAttachment($file))) + $ids[] = $id; + + return $ids; + } + + function getAttachments() { + + if($this->attachments) + return $this->attachments; + + //XXX: inner join the file table instead? + $sql='SELECT a.attach_id, f.id as file_id, f.size, f.hash as file_hash, f.name ' + .' FROM '.FILE_TABLE.' f ' + .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' + .' WHERE a.ticket_id='.db_input($this->getTicketId()) + .' AND a.ref_id='.db_input($this->getId()) + .' AND a.ref_type='.db_input($this->getType()); + + $this->attachments = array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while($rec=db_fetch_array($res)) + $this->attachments[] = $rec; + } + + return $this->attachments; + } + + function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ') { + + $str=''; + foreach($this->getAttachments() as $attachment ) { + /* The hash can be changed but must match validation in @file */ + $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); + $size = ''; + if($attachment['size']) + $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); + + $str.=sprintf('<a class="Icon file" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', + $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + } + + return $str; + } + + + /* Returns file names with id as key */ + function getFiles() { + + $files = array(); + foreach($this->getAttachments() as $attachment) + $files[$attachment['file_id']] = $attachment['name']; + + return $files; + } + + + /* save email info + * TODO: Refactor it to include outgoing emails on responses. + */ + + function saveEmailInfo($vars) { + + if(!$vars || !$vars['mid']) + return 0; + + $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE + .' SET message_id='.db_input($this->getId()) //TODO: change it to thread_id + .', email_mid='.db_input($vars['mid']) //TODO: change it to mid. + .', headers='.db_input($vars['header']); + + return db_query($sql)?db_insert_id():0; + } + + /* variables */ function asVar() { @@ -164,12 +525,59 @@ Class ThreadEntry { /* static calls */ function lookup($id, $tid=0, $type='') { - return ($id - && is_numeric($id) - && ($e = new ThreadEntry($id, $type, $tid)) + return ($id + && is_numeric($id) + && ($e = new ThreadEntry($id, $type, $tid)) && $e->getId()==$id )?$e:null; } + + //new entry ... we're trusting the caller to check validity of the data. + function create($vars) { + + //Must have... + if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N'))) + return false; + + $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' + .' ,thread_type='.db_input($vars['type']) + .' ,ticket_id='.db_input($vars['ticketId']) + .' ,title='.db_input(Format::sanitize($vars['title'])) + .' ,body='.db_input(Format::sanitize($vars['body'])) + .' ,staff_id='.db_input($vars['staffId']) + .' ,poster='.db_input($vars['poster']) + .' ,source='.db_input($vars['source']); + + if(isset($vars['pid'])) + $sql.=' ,pid='.db_input($vars['pid']); + + if($vars['ip_address']) + $sql.=' ,ip_address='.db_input($vars['ip_address']); + + //echo $sql; + if(!db_query($sql) || !($entry=self::lookup(db_insert_id(), $vars['ticketId']))) + return false; + + /************* ATTACHMENTS *****************/ + + //Upload/save attachments IF ANY + if($vars['files']) //expects well formatted and VALIDATED files array. + $entry->uploadFiles($vars['files']); + + //Emailed or API attachments + if($vars['attachments']) + $entry->importAttachments($vars['attachments']); + + //Canned attachments... + if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) + $entry->saveAttachments($vars['cannedattachments']); + + return $entry; + } + + function add($vars) { + return ($entry=self::create($vars))?$entry->getId():0; + } } /* Message - Ticket thread entry of type message */ @@ -183,11 +591,30 @@ class Message extends ThreadEntry { return $this->getTitle(); } - function lookup($id, $tid, $type='M') { - - return ($id + function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + function add($vars, &$errors) { + + if(!$vars || !is_array($vars) || !$vars['ticketId']) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['message']) + $errors['message'] = 'Message required'; + + if($errors) return false; + + $vars['type'] = 'M'; + $vars['body'] = $vars['message']; + + return ThreadEntry::add($vars); + } + + function lookup($id, $tid=0, $type='M') { + + return ($id && is_numeric($id) - && ($m = new Message($id, $tid)) + && ($m = new Message($id, $tid)) && $m->getId()==$id )?$m:null; } @@ -208,11 +635,33 @@ class Response extends ThreadEntry { return $this->getStaff(); } - function lookup($id, $tid, $type='R') { - - return ($id + function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + function add($vars, &$errors) { + + if(!$vars || !is_array($vars) || !$vars['ticketId']) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['response']) + $errors['response'] = 'Response required'; + + if($errors) return false; + + $vars['type'] = 'R'; + $vars['body'] = $vars['response']; + if(!$vars['pid'] && $vars['msgId']) + $vars['pid'] = $vars['msgId']; + + return ThreadEntry::add($vars); + } + + + function lookup($id, $tid=0, $type='R') { + + return ($id && is_numeric($id) - && ($r = new Response($id, $tid)) + && ($r = new Response($id, $tid)) && $r->getId()==$id )?$r:null; } @@ -229,11 +678,33 @@ class Note extends ThreadEntry { return $this->getBody(); } - function lookup($id, $tid, $type='N') { - - return ($id + /* static */ + function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + function add($vars, &$errors) { + + //Check required params. + if(!$vars || !is_array($vars) || !$vars['ticketId']) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['note']) + $errors['note'] = 'Note required'; + + if($errors) return false; + + //TODO: use array_intersect_key when we move to php 5 to extract just what we need. + $vars['type'] = 'N'; + $vars['body'] = $vars['note']; + + return ThreadEntry::add($vars); + } + + function lookup($id, $tid=0, $type='N') { + + return ($id && is_numeric($id) - && ($n = new Note($id, $tid)) + && ($n = new Note($id, $tid)) && $n->getId()==$id )?$n:null; } diff --git a/include/class.ticket.php b/include/class.ticket.php index 9dd35529defaa69a4fda345c840fb67271046a8a..94733adc14eb52fe26847297fb1d294a94bb7f72 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -34,29 +34,12 @@ include_once(INCLUDE_DIR.'class.canned.php'); class Ticket { var $id; - var $extid; - var $email; - var $status; - var $created; - var $reopened; - var $updated; - var $lastrespdate; - var $lastmsgdate; - var $duedate; - var $priority; - var $priority_id; - var $fullname; - var $staff_id; - var $team_id; - var $dept_id; - var $topic_id; - var $dept_name; - var $subject; - var $helptopic; - var $overdue; + var $number; + + var $ht; var $lastMsgId; - + var $dept; //Dept obj var $sla; // SLA obj var $staff; //Staff obj @@ -64,25 +47,22 @@ class Ticket { var $team; //Team obj var $topic; //Topic obj var $tlock; //TicketLock obj - - function Ticket($id){ + + var $thread; //Thread obj. + + function Ticket($id) { $this->id = 0; $this->load($id); } - + function load($id=0) { if(!$id && !($id=$this->getId())) return false; - //TODO: delete helptopic field in ticket table. - $sql='SELECT ticket.*, lock_id, dept_name, priority_desc ' - .' ,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate ' + .' ,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate ' .' ,count(attach.attach_id) as attachments ' - .' ,count(DISTINCT message.id) as messages ' - .' ,count(DISTINCT response.id) as responses ' - .' ,count(DISTINCT note.id) as notes ' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) ' @@ -92,12 +72,6 @@ class Ticket { .'ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (' .'ticket.ticket_id=attach.ticket_id) ' - .' LEFT JOIN '.TICKET_THREAD_TABLE.' message ON (' - ."ticket.ticket_id=message.ticket_id AND message.thread_type = 'M') " - .' LEFT JOIN '.TICKET_THREAD_TABLE.' response ON (' - ."ticket.ticket_id=response.ticket_id AND response.thread_type = 'R') " - .' LEFT JOIN '.TICKET_THREAD_TABLE.' note ON ( ' - ."ticket.ticket_id=note.ticket_id AND note.thread_type = 'N') " .' WHERE ticket.ticket_id='.db_input($id) .' GROUP BY ticket.ticket_id'; @@ -105,35 +79,12 @@ class Ticket { if(!($res=db_query($sql)) || !db_num_rows($res)) return false; - - $this->ht=db_fetch_array($res); - + + $this->ht = db_fetch_array($res); + $this->id = $this->ht['ticket_id']; - $this->extid = $this->ht['ticketID']; - - $this->email = $this->ht['email']; - $this->fullname = $this->ht['name']; - $this->status = $this->ht['status']; - $this->created = $this->ht['created']; - $this->reopened = $this->ht['reopened']; - $this->updated = $this->ht['updated']; - $this->duedate = $this->ht['duedate']; - $this->closed = $this->ht['closed']; - $this->lastmsgdate = $this->ht['lastmessagedate']; - $this->lastrespdate = $this->ht['lastresponsedate']; - - $this->lock_id = $this->ht['lock_id']; - $this->priority_id = $this->ht['priority_id']; - $this->priority = $this->ht['priority_desc']; - $this->staff_id = $this->ht['staff_id']; - $this->team_id = $this->ht['team_id']; - $this->dept_id = $this->ht['dept_id']; - $this->dept_name = $this->ht['dept_name']; - $this->sla_id = $this->ht['sla_id']; - $this->topic_id = $this->ht['topic_id']; - $this->subject = $this->ht['subject']; - $this->overdue = $this->ht['isoverdue']; - + $this->number = $this->ht['ticketID']; + //Reset the sub classes (initiated ondemand)...good for reloads. $this->staff = null; $this->client = null; @@ -143,14 +94,18 @@ class Ticket { $this->tlock = null; $this->stats = null; $this->topic = null; - + $this->thread = null; + + //REQUIRED: Preload thread obj - checked on lookup! + $this->getThread(); + return true; } - + function reload() { return $this->load(); } - + function isOpen() { return (strcasecmp($this->getStatus(),'Open')==0); } @@ -168,9 +123,9 @@ class Ticket { } function isOverdue() { - return ($this->overdue); + return ($this->ht['isoverdue']); } - + function isAnswered() { return ($this->ht['isanswered']); } @@ -195,10 +150,10 @@ class Ticket { if(!is_object($client) && !($client=Client::lookup($client))) return false; - if(!strcasecmp($client->getEmail(),$this->getEmail())) + if(!strcasecmp($client->getEmail(), $this->getEmail())) return true; - return ($cfg && $cfg->showRelatedTickets() + return ($cfg && $cfg->showRelatedTickets() && $client->getTicketId()==$this->getExtId()); } @@ -208,15 +163,15 @@ class Ticket { } function getExtId() { - return $this->extid; + return $this->getNumber(); } function getNumber() { - return $this->getExtId(); + return $this->number; } - - function getEmail(){ - return $this->email; + + function getEmail() { + return $this->ht['email']; } function getAuthToken() { @@ -224,25 +179,25 @@ class Ticket { return md5($this->getId() . $this->getEmail() . SECRET_SALT); } - function getName(){ - return $this->fullname; + function getName() { + return $this->ht['name']; } function getSubject() { - return $this->subject; + return $this->ht['subject']; } /* Help topic title - NOT object -> $topic */ function getHelpTopic() { - if(!$this->helpTopic && ($topic=$this->getTopic())) - $this->helpTopic = $topic->getName(); - - return $this->helpTopic; + if(!$this->ht['helptopic'] && ($topic=$this->getTopic())) + $this->ht['helptopic'] = $topic->getName(); + + return $this->ht['helptopic']; } - - function getCreateDate(){ - return $this->created; + + function getCreateDate() { + return $this->ht['created']; } function getOpenDate() { @@ -250,15 +205,15 @@ class Ticket { } function getReopenDate() { - return $this->reopened; + return $this->ht['reopened']; } - - function getUpdateDate(){ - return $this->updated; + + function getUpdateDate() { + return $this->ht['updated']; } - function getDueDate(){ - return $this->duedate; + function getDueDate() { + return $this->ht['duedate']; } function getSLADueDate() { @@ -267,7 +222,7 @@ class Ticket { function getEstDueDate() { - //Real due date + //Real due date if(($duedate=$this->getDueDate())) return $duedate; @@ -275,30 +230,34 @@ class Ticket { return $this->getSLADueDate(); } - function getCloseDate(){ - return $this->closed; + function getCloseDate() { + return $this->ht['closed']; } - function getStatus(){ - return $this->status; + function getStatus() { + return $this->ht['status']; } - - function getDeptId(){ - return $this->dept_id; + + function getDeptId() { + return $this->ht['dept_id']; } - - function getDeptName(){ - return $this->dept_name; + + function getDeptName() { + + if(!$this->ht['dept_name'] && ($dept = $this->getDept())) + $this->ht['dept_name'] = $dept->getName(); + + return $this->ht['dept_name']; } function getPriorityId() { - return $this->priority_id; + return $this->ht['priority_id']; } - - function getPriority() { - return $this->priority; + + function getPriority() { //TODO: Make it an obj. + return $this->ht['priority_desc']; } - + function getPhone() { return $this->ht['phone']; } @@ -318,7 +277,7 @@ class Ticket { function getSource() { return $this->ht['source']; } - + function getIP() { return $this->ht['ip_address']; } @@ -341,24 +300,24 @@ class Ticket { 'duedate' => $this->getDueDate()?(Format::userdate('m/d/Y', Misc::db2gmtime($this->getDueDate()))):'', 'time' => $this->getDueDate()?(Format::userdate('G:i', Misc::db2gmtime($this->getDueDate()))):'', ); - + return $info; } function getLockId() { - return $this->lock_id; + return $this->ht['lock_id']; } - - function getLock(){ - + + function getLock() { + if(!$this->tlock && $this->getLockId()) - $this->tlock= TicketLock::lookup($this->getLockId(),$this->getId()); - + $this->tlock= TicketLock::lookup($this->getLockId(), $this->getId()); + return $this->tlock; } - + function acquireLock($staffId, $lockTime) { - + if(!$staffId or !$lockTime) //Lockig disabled? return null; @@ -369,18 +328,18 @@ class Ticket { //Lock already exits...renew it $lock->renew($lockTime); //New clock baby. - + return $lock; } //No lock on the ticket or it is expired - $this->tlock=null; //clear crap - $this->lock_id=TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock.. + $this->tlock = null; //clear crap + $this->ht['lock_id'] = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock.. //load and return the newly created lock if any! return $this->getLock(); } - - function getDept(){ - + + function getDept() { + if(!$this->dept && $this->getDeptId()) $this->dept= Dept::lookup($this->getDeptId()); @@ -394,12 +353,12 @@ class Ticket { return $this->client; } - - function getStaffId(){ - return $this->staff_id; + + function getStaffId() { + return $this->ht['staff_id']; } - function getStaff(){ + function getStaff() { if(!$this->staff && $this->getStaffId()) $this->staff= Staff::lookup($this->getStaffId()); @@ -407,11 +366,11 @@ class Ticket { return $this->staff; } - function getTeamId(){ - return $this->team_id; + function getTeamId() { + return $this->ht['team_id']; } - function getTeam(){ + function getTeam() { if(!$this->team && $this->getTeamId()) $this->team = Team::lookup($this->getTeamId()); @@ -431,11 +390,11 @@ class Ticket { } function getAssignees() { - + $assignees=array(); if($staff=$this->getStaff()) $assignees[] = $staff->getName(); - + if($team=$this->getTeam()) $assignees[] = $team->getName(); @@ -448,10 +407,10 @@ class Ticket { } function getTopicId() { - return $this->topic_id; + return $this->ht['topic_id']; } - function getTopic() { + function getTopic() { if(!$this->topic && $this->getTopicId()) $this->topic = Topic::lookup($this->getTopicId()); @@ -459,9 +418,9 @@ class Ticket { return $this->topic; } - + function getSLAId() { - return $this->sla_id; + return $this->ht['sla_id']; } function getSLA() { @@ -483,7 +442,7 @@ class Ticket { if(!($res=db_query($sql)) || !db_num_rows($res)) return null; - + list($id)=db_fetch_row($res); return Staff::lookup($id); @@ -491,19 +450,7 @@ class Ticket { } function getLastMessageDate() { - - if($this->lastmsgdate) - return $this->lastmsgdate; - - //for old versions...XXX: still needed???? - $sql='SELECT created FROM '.TICKET_THREAD_TABLE - .' WHERE ticket_id='.db_input($this->getId()) - ." AND thread_type = 'M'" - .' ORDER BY created DESC LIMIT 1'; - if(($res=db_query($sql)) && db_num_rows($res)) - list($this->lastmsgdate)=db_fetch_row($res); - - return $this->lastmsgdate; + return $this->ht['lastmessage']; } function getLastMsgDate() { @@ -511,35 +458,28 @@ class Ticket { } function getLastResponseDate() { - - if($this->lastrespdate) - return $this->lastrespdate; - - $sql='SELECT created FROM '.TICKET_THREAD_TABLE - .' WHERE ticket_id='.db_input($this->getId()) - .' AND thread_type="R"' - .' ORDER BY created DESC LIMIT 1'; - if(($res=db_query($sql)) && db_num_rows($res)) - list($this->lastrespdate)=db_fetch_row($res); - - return $this->lastrespdate; + return $this->ht['lastresponse']; } function getLastRespDate() { return $this->getLastResponseDate(); } - + function getLastMsgId() { return $this->lastMsgId; } - function getRelatedTicketsCount(){ + function getLastMessage() { + return Message::lookup($this->getLastMsgId(), $this->getId()); + } + + function getThread() { - $sql='SELECT count(*) FROM '.TICKET_TABLE - .' WHERE email='.db_input($this->getEmail()); + if(!$this->thread) + $this->thread = Thread::lookup($this); - return db_result(db_query($sql)); + return $this->thread; } function getThreadCount() { @@ -547,121 +487,42 @@ class Ticket { } function getNumMessages() { - return $this->ht['messages']; + return $this->getThread()->getNumMessages(); } function getNumResponses() { - return $this->ht['responses']; + return $this->getThread()->getNumResponses(); } function getNumNotes() { - return $this->ht['notes']; + return $this->getThread()->getNumNotes(); } function getMessages() { - return $this->getThreadByType('M'); + return $this->getThreadEntries('M'); } - function getResponses($msgId=0) { - return $this->getThreadByType('R', $msgId); + function getResponses() { + return $this->getThreadEntries('R'); } function getNotes() { - return $this->getThreadByType('N'); + return $this->getThreadEntries('N'); } function getClientThread() { - return $this->getThreadWithoutNotes(); - } - - function getThreadWithNotes() { - return $this->getThread(true); + return $this->getThreadEntries(array('M', 'R')); } - - function getThreadWithoutNotes() { - return $this->getThread(false); - } - - function getThread($includeNotes=false, $order='') { - - $treadtypes=array('M', 'R'); // messages and responses. - if($includeNotes) //Include notes?? - $treadtypes[] = 'N'; - - return $this->getThreadByType($treadtypes, $order); - } - - function getThreadByType($type, $order='ASC') { - - if(!$order || !in_array($order, array('DESC','ASC'))) - $order='ASC'; - - $sql='SELECT thread.* ' - .' ,count(DISTINCT attach.attach_id) as attachments ' - .' FROM '.TICKET_THREAD_TABLE.' thread ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id - AND thread.thread_type=attach.ref_type) ' - .' WHERE thread.ticket_id='.db_input($this->getId()); - - if($type && is_array($type)) - $sql.=" AND thread.thread_type IN('".implode("','", $type)."')"; - elseif($type) - $sql.=' AND thread.thread_type='.db_input($type); - - $sql.=' GROUP BY thread.id ' - .' ORDER BY thread.created '.$order; - - $thread=array(); - if(($res=db_query($sql)) && db_num_rows($res)) - while($rec=db_fetch_array($res)) - $thread[] = $rec; - return $thread; + function getThreadEntry($id) { + return $this->getThread()->getEntry($id); } - function getAttachments($refId=0, $type=null) { - - if($refId && !$type) - return NULL; - - //XXX: inner join the file table instead? - $sql='SELECT a.attach_id, f.id as file_id, f.size, f.hash as file_hash, f.name ' - .' FROM '.FILE_TABLE.' f ' - .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' - .' WHERE a.ticket_id='.db_input($this->getId()); - - if($refId) - $sql.=' AND a.ref_id='.db_input($refId); - - if($type) - $sql.=' AND a.ref_type='.db_input($type); - - $attachments = array(); - if(($res=db_query($sql)) && db_num_rows($res)) { - while($rec=db_fetch_array($res)) - $attachments[] = $rec; - } - - return $attachments; + function getThreadEntries($type, $order='') { + return $this->getThread()->getEntries($type, $order); } - function getAttachmentsLinks($refId, $type, $separator=' ',$target='') { - $str=''; - foreach($this->getAttachments($refId, $type) as $attachment ) { - /* The has here can be changed but must match validation in attachment.php */ - $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); - if($attachment['size']) - $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); - - $str.=sprintf('<a class="Icon file" href="attachment.php?id=%d&h=%s" target="%s">%s</a>%s %s', - $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); - } - - return $str; - } /* -------------------- Setters --------------------- */ function setLastMsgId($msgid) { @@ -671,10 +532,10 @@ class Ticket { function setPriority($priorityId) { //XXX: what happens to SLA priority??? - - if(!$priorityId || $priorityId==$this->getPriorityId()) + + if(!$priorityId || $priorityId==$this->getPriorityId()) return ($priorityId); - + $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() ' .', priority_id='.db_input($priorityId) .' WHERE ticket_id='.db_input($this->getId()); @@ -683,31 +544,33 @@ class Ticket { } //DeptId can NOT be 0. No orphans please! - function setDeptId($deptId){ - + 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_id = $staffId; + + $this->staff = null; + $this->ht['staff_id'] = $staffId; + return true; } @@ -751,9 +614,9 @@ class Ticket { //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()); @@ -763,7 +626,7 @@ class Ticket { //Status helper. function setStatus($status) { - if(strcasecmp($this->getStatus(),$status)==0) + if(strcasecmp($this->getStatus(), $status)==0) return true; //No changes needed. switch(strtolower($status)) { @@ -818,11 +681,11 @@ class Ticket { } //Close the ticket - function close(){ + function close() { global $thisstaff; - + $sql='UPDATE '.TICKET_TABLE.' SET closed=NOW(),isoverdue=0, duedate=NULL, updated=NOW(), status='.db_input('closed'); - if($thisstaff) //Give the closing staff credit. + if($thisstaff) //Give the closing staff credit. $sql.=', staff_id='.db_input($thisstaff->getId()); $sql.=' WHERE ticket_id='.db_input($this->getId()); @@ -837,14 +700,14 @@ class Ticket { } //set status to open on a closed ticket. - function reopen($isanswered=0){ + function reopen($isanswered=0) { $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), reopened=NOW() ' .' ,status='.db_input('open') .' ,isanswered='.db_input($isanswered) .' WHERE ticket_id='.db_input($this->getId()); - //TODO: log reopen event here + //TODO: log reopen event here $this->logEvent('reopened', 'closed'); return (db_query($sql) && db_affected_rows()); @@ -854,46 +717,46 @@ class Ticket { 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. $dept= $this->getDept(); if(!$dept || !($tpl = $dept->getTemplate())) $tpl= $cfg->getDefaultTemplate(); - + if(!$tpl) return false; //bail out...missing stuff. if(!$dept || !($email=$dept->getAutoRespEmail())) $email =$cfg->getDefaultEmail(); //Send auto response - if enabled. - if($autorespond && $email && $cfg->autoRespONNewTicket() - && $dept->autoRespONNewTicket() + if($autorespond && $email && $cfg->autoRespONNewTicket() + && $dept->autoRespONNewTicket() && ($msg=$tpl->getAutoRespMsgTemplate())) { - - $msg = $this->replaceVars($msg, + + $msg = $this->replaceVars($msg, array('message' => $message, 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'') ); if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] ="\n$tag\n\n".$msg['body']; - + $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']); } - + if(!($email=$cfg->getAlertEmail())) $email =$cfg->getDefaultEmail(); - + //Send alert to out sleepy & idle staff. if($alertstaff && $email - && $cfg->alertONNewTicket() + && $cfg->alertONNewTicket() && ($msg=$tpl->getNewTicketAlertMsgTemplate())) { - + $msg = $this->replaceVars($msg, array('message' => $message)); $recipients=$sentlist=array(); @@ -903,26 +766,26 @@ class Ticket { $email->sendAlert($cfg->getAdminEmail(), $msg['subj'], $alert); $sentlist[]=$cfg->getAdminEmail(); } - + //Only alerts dept members if the ticket is NOT assigned. if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) { if(($members=$dept->getMembers())) $recipients=array_merge($recipients, $members); } - + if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager())) $recipients[]= $manager; - - foreach( $recipients as $k=>$staff){ - if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + + foreach( $recipients as $k=>$staff) { + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } - - + + } - + return true; } @@ -937,43 +800,43 @@ class Ticket { //Send notice to user. $dept = $this->getDept(); - + if(!$dept || !($tpl=$dept->getTemplate())) $tpl=$cfg->getDefaultTemplate(); - + if(!$dept || !($email=$dept->getAutoRespEmail())) $email=$cfg->getDefaultEmail(); if($tpl && ($msg=$tpl->getOverlimitMsgTemplate()) && $email) { - - $msg = $this->replaceVars($msg, + + $msg = $this->replaceVars($msg, array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); - + $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']); } $client= $this->getClient(); - + //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think. $alert='Max. open tickets reached for '.$this->getEmail()."\n" .'Open ticket: '.$client->getNumOpenTickets()."\n" .'Max Allowed: '.$cfg->getMaxOpenTickets()."\n\nNotice sent to the user."; - + $ost->alertAdmin('Overlimit Notice', $alert); - + return true; } - function onResponse(){ + function onResponse() { db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1,lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId())); } - function onMessage($autorespond=true, $alert=true){ + function onMessage($autorespond=true, $alert=true) { global $cfg; db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId())); - - //auto-assign to closing staff or last respondent + + //auto-assign to closing staff or last respondent if(!($staff=$this->getStaff()) || !$staff->isAvailable()) { if($cfg->autoAssignReopenedTickets() && ($lastrep=$this->getLastRespondent()) && $lastrep->isAvailable()) { $this->setStaffId($lastrep->getId()); //direct assignment; @@ -1001,7 +864,7 @@ class Ticket { if(!$dept || !($email = $dept->getAutoRespEmail())) $email = $cfg->getDefaultEmail(); - + //If enabled...send confirmation to user. ( New Message AutoResponse) if($email && $tpl && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { @@ -1011,7 +874,7 @@ class Ticket { //Reply separator tag. if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] ="\n$tag\n\n".$msg['body']; - + $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']); } } @@ -1028,7 +891,7 @@ class Ticket { $comments = $comments?$comments:'Ticket assignment'; $assigner = $thisstaff?$thisstaff:'SYSTEM (Auto Assignment)'; - + //Log an internal note - no alerts on the internal note. $this->logNote('Ticket Assigned to '.$assignee->getName(), $comments, $assigner, false); @@ -1060,7 +923,7 @@ class Ticket { //Get the message template if($email && $recipients && $tpl && ($msg=$tpl->getAssignedAlertMsgTemplate())) { - $msg = $this->replaceVars($msg, + $msg = $this->replaceVars($msg, array('comments' => $comments, 'assignee' => $assignee, 'assigner' => $assigner @@ -1069,7 +932,7 @@ class Ticket { //Send the alerts. $sentlist=array(); foreach( $recipients as $k=>$staff) { - if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); @@ -1100,7 +963,7 @@ class Ticket { //Get the message template if($tpl && ($msg=$tpl->getOverdueAlertMsgTemplate()) && $email) { - + $msg = $this->replaceVars($msg, array('comments' => $comments)); //recipients @@ -1121,8 +984,8 @@ class Ticket { $recipients[]= $manager; $sentlist=array(); - foreach( $recipients as $k=>$staff){ - if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(),$sentlist)) continue; + foreach( $recipients as $k=>$staff) { + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace("%{recipient}", $staff->getFirstName(), $msg['body']); $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); @@ -1131,9 +994,9 @@ class Ticket { } return true; - + } - + //ticket obj as variable = ticket number. function asVar() { return $this->getNumber(); @@ -1161,17 +1024,17 @@ class Ticket { break; case 'create_date': return Format::date( - $cfg->getDateTimeFormat(), + $cfg->getDateTimeFormat(), Misc::db2gmtime($this->getCreateDate()), $cfg->getTZOffset(), $cfg->observeDaylightSaving()); break; case 'due_date': $duedate =''; - if($this->getDueDate()) + if($this->getEstDueDate()) $duedate = Format::date( $cfg->getDateTimeFormat(), - Misc::db2gmtime($this->getDueDate()), + Misc::db2gmtime($this->getEstDueDate()), $cfg->getTZOffset(), $cfg->observeDaylightSaving()); @@ -1211,9 +1074,9 @@ class Ticket { } function markOverdue($whine=true) { - + global $cfg; - + if($this->isOverdue()) return true; @@ -1231,7 +1094,7 @@ class Ticket { function clearOverdue() { - if(!$this->isOverdue()) + if(!$this->isOverdue()) return true; $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() '; @@ -1244,11 +1107,11 @@ class Ticket { return (db_query($sql) && db_affected_rows()); } - //Dept Tranfer...with alert.. done by staff + //Dept Tranfer...with alert.. done by staff function transfer($deptId, $comments, $alert = true) { - + global $cfg, $thisstaff; - + if(!$thisstaff || !$thisstaff->canTransferTickets()) return false; @@ -1256,8 +1119,8 @@ class Ticket { if(!$deptId || !$this->setDeptId($deptId)) return false; - - // Reopen ticket if closed + + // Reopen ticket if closed if($this->isClosed()) $this->reopen(); $this->reload(); @@ -1265,14 +1128,14 @@ class Ticket { // Set SLA of the new department if(!$this->getSLAId()) $this->selectSLAId(); - + /*** log the transfer comments as internal note - with alerts disabled - ***/ $title='Ticket transfered from '.$currentDept.' to '.$this->getDeptName(); - $comments=$comments?$comments:$title; + $comments=$comments?$comments:$title; $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!! @@ -1280,16 +1143,16 @@ class Ticket { //Get template. if(!($tpl = $dept->getTemplate())) $tpl= $cfg->getDefaultTemplate(); - + //Email to use! if(!($email=$cfg->getAlertEmail())) $email =$cfg->getDefaultEmail(); - - //Get the message template + + //Get the message template if($tpl && ($msg=$tpl->getTransferAlertMsgTemplate()) && $email) { - + $msg = $this->replaceVars($msg, array('comments' => $comments, 'staff' => $thisstaff)); - //recipients + //recipients $recipients=array(); //Assigned staff or team... if any if($this->isAssigned() && $cfg->alertAssignedONTransfer()) { @@ -1306,11 +1169,11 @@ class Ticket { //Always alert dept manager?? if($cfg->alertDeptManagerONTransfer() && $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 = str_replace('%{recipient}',$staff->getFirstName(), $msg['body']); + foreach( $recipients as $k=>$staff) { + if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; + $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } @@ -1323,7 +1186,7 @@ class Ticket { if(!is_object($staff) && !($staff=Staff::lookup($staff))) return false; - + if(!$this->setStaffId($staff->getId())) return false; @@ -1357,7 +1220,7 @@ class Ticket { global $thisstaff; $rv=0; - $id=preg_replace("/[^0-9]/", "",$assignId); + $id=preg_replace("/[^0-9]/", "", $assignId); if($assignId[0]=='t') { $rv=$this->assignToTeam($id, $note, $alert); } elseif($assignId[0]=='s' || is_numeric($assignId)) { @@ -1368,7 +1231,7 @@ class Ticket { return $rv; } - + //unassign primary assignee function unassign() { @@ -1391,52 +1254,40 @@ class Ticket { return true; } - + function release() { return $this->unassign(); } //Insert message from client - function postMessage($vars, $source='', $alerts=true) { + function postMessage($vars, $origin='', $alerts=true) { global $cfg; - - if(!$vars || !$vars['message']) - return 0; - + //Strip quoted reply...on emailed replies - if(!strcasecmp($source, 'Email') - && $cfg->stripQuotedReply() + if(!strcasecmp($origin, 'Email') + && $cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator()) && strpos($vars['message'], $tag)) - list($vars['message']) = split($tag, $vars['message']); - - # XXX: Refuse auto-response messages? (via email) XXX: No - but kill our auto-responder. - - - $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW()' - .' ,thread_type="M" ' - .' ,ticket_id='.db_input($this->getId()) - # XXX: Put Subject header into the 'title' field - .' ,body='.db_input(Format::striptags($vars['message'])) //Tags/code stripped...meaning client can not send in code..etc - .' ,source='.db_input($source?$source:$_SERVER['REMOTE_ADDR']) - .' ,ip_address='.db_input($_SERVER['REMOTE_ADDR']); - - if(!db_query($sql) || !($msgid=db_insert_id())) - return 0; //bail out.... - - $this->setLastMsgId($msgid); - - if (isset($vars['mid'])) { - $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE - .' SET message_id='.db_input($msgid) - .', email_mid='.db_input($vars['mid']) - .', headers='.db_input($vars['header']); - db_query($sql); - } + if(list($msg) = split($tag, $vars['message'])) + $vars['message'] = $msg; - if(!$alerts) return $msgid; //Our work is done... + if($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->setLastMsgId($message->getId()); + + if (isset($vars['mid'])) + $message->saveEmailInfo($vars); + + if(!$alerts) return $message; //Our work is done... $autorespond = true; - if ($autorespond && $vars['header'] && TicketFilter::isAutoResponse($vars['header'])) + if ($autorespond && $message->isAutoResponse()) $autorespond=false; $this->onMessage($autorespond); //must be called b4 sending alerts to staff. @@ -1452,33 +1303,33 @@ class Ticket { //If enabled...send alert to staff (New Message Alert) if($cfg->alertONNewMessage() && $tpl && $email && ($msg=$tpl->getNewMessageAlertMsgTemplate())) { - $msg = $this->replaceVars($msg, array('message' => $vars['message'])); + $msg = $this->replaceVars($msg, array('message' => $message)); //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; - + $sentlist=array(); //I know it sucks...but..it works. - foreach( $recipients as $k=>$staff){ + foreach( $recipients as $k=>$staff) { if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); $sentlist[] = $staff->getEmail(); } } - - return $msgid; + + return $message; } function postCannedReply($canned, $msgId, $alert=true) { @@ -1492,16 +1343,17 @@ class Ticket { $files[] = $file['id']; $info = array('msgId' => $msgId, + 'poster' => 'SYSTEM (Canned Reply)', 'response' => $this->replaceVars($canned->getResponse()), 'cannedattachments' => $files); $errors = array(); - if(!($respId=$this->postReply($info, $errors, false))) - return false; + if(!($response=$this->postReply($info, $errors, false))) + return null; $this->markUnAnswered(); - if(!$alert) return $respId; + if(!$alert) return $response; $dept = $this->getDept(); @@ -1518,65 +1370,41 @@ class Ticket { else $signature=''; - $msg = $this->replaceVars($msg, array('response' => $info['response'], 'signature' => $signature)); + $msg = $this->replaceVars($msg, array('response' => $response, 'signature' => $signature)); if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] ="\n$tag\n\n".$msg['body']; - $attachments =($cfg->emailAttachments() && $files)?$this->getAttachments($respId, 'R'):array(); + $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array(); $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments); } - return $respId; + return $response; } - /* public */ + /* public */ function postReply($vars, &$errors, $alert = true) { global $thisstaff, $cfg; - if(!$vars['msgId']) - $errors['msgId'] ='Missing messageId - internal error'; - if(!$vars['response']) - $errors['response'] = 'Response message required'; - if($errors) return 0; + if(!$vars['poster'] && $thisstaff) + $vars['poster'] = $thisstaff->getName(); - $poster = $thisstaff?$thisstaff->getName():'SYSTEM (Canned Reply)'; + if(!$vars['staffId'] && $thisstaff) + $vars['staffId'] = $thisstaff->getId(); - $sql='INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' - .' ,thread_type="R"' - .' ,ticket_id='.db_input($this->getId()) - .' ,pid='.db_input($vars['msgId']) - .' ,body='.db_input(Format::striptags($vars['response'])) - .' ,staff_id='.db_input($thisstaff?$thisstaff->getId():0) - .' ,poster='.db_input($poster) - .' ,ip_address='.db_input($thisstaff?$thisstaff->getIP():''); - - if(!db_query($sql) || !($respId=db_insert_id())) - return false; + if(!($response = $this->getThread()->addResponse($vars, $errors))) + return null; //Set status - if checked. if(isset($vars['reply_ticket_status']) && $vars['reply_ticket_status']) $this->setStatus($vars['reply_ticket_status']); - /* We can NOT recover from attachment related failures at this point */ - $attachments = array(); - //Web based upload.. note that we're not "validating" attachments on response. - if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) - $attachments=$this->uploadAttachments($files, $respId, 'R'); - - //Canned attachments... - if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) { - foreach($vars['cannedattachments'] as $fileId) - if($fileId && $this->saveAttachment($fileId, $respId, 'R')) - $attachments[] = $fileId; - } - $this->onResponse(); //do house cleaning.. $this->reload(); /* email the user?? - if disabled - the bail out */ - if(!$alert) return $respId; + if(!$alert) return $response; $dept = $this->getDept(); @@ -1594,24 +1422,24 @@ class Ticket { $signature=$dept->getSignature(); else $signature=''; - - $msg = $this->replaceVars($msg, - array('response' => $vars['response'], 'signature' => $signature, 'staff' => $thisstaff)); + + $msg = $this->replaceVars($msg, + array('response' => $response, 'signature' => $signature, 'staff' => $thisstaff)); if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] ="\n$tag\n\n".$msg['body']; //Set attachments if emailing. - $attachments =($cfg->emailAttachments() && $attachments)?$this->getAttachments($respId,'R'):array(); + $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); //TODO: setup 5 param (options... e.g mid trackable on replies) $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments); } - return $respId; + return $response; } //Activity log - saved as internal notes WHEN enabled!! - function logActivity($title,$note){ + function logActivity($title, $note) { global $cfg; if(!$cfg || !$cfg->logTicketActivity()) @@ -1659,42 +1487,23 @@ class Ticket { $alert); } - function postNote($vars, &$errors, $poster, $alert=true) { + function postNote($vars, &$errors, $poster, $alert=true) { global $cfg, $thisstaff; - if(!$vars || !is_array($vars)) - $errors['err'] = 'Missing or invalid data'; - elseif(!$vars['note']) - $errors['note'] = 'Note required'; - - if($errors) return false; - - $staffId = 0; + //Who is posting the note - staff or system? + $vars['staffId'] = 0; + $vars['poster'] = 'SYSTEM'; if($poster && is_object($poster)) { - $staffId = $poster->getId(); - $poster = $poster->getName(); - } elseif(!$poster) { - $poster ='SYSTEM'; + $vars['staffId'] = $poster->getId(); + $vars['poster'] = $poster->getName(); + }elseif($poster) { //string + $vars['poster'] = $poster; } - //TODO: move to class.thread.php + if(!($note=$this->getThread()->addNote($vars, $errors))) + return null; - $sql= 'INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '. - ',thread_type="N"'. - ',ticket_id='.db_input($this->getId()). - ',title='.db_input(Format::striptags($vars['title']?$vars['title']:'[No Title]')). - ',body='.db_input(Format::striptags($vars['note'])). - ',staff_id='.db_input($staffId). - ',poster='.db_input($poster); - //echo $sql; - if(!db_query($sql) || !($id=db_insert_id())) - return false; - - //Upload attachments IF ANY - TODO: validate attachment types?? - if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) - $attachments = $this->uploadAttachments($files, $id, 'N'); - - //Set state: Error on state change not critical! + //Set state: Error on state change not critical! if(isset($vars['state']) && $vars['state']) { if($this->setState($vars['state'])) $this->reload(); @@ -1702,11 +1511,8 @@ class Ticket { // If alerts are not enabled then return a success. if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept())) - return $id; + return $note; - //Note obj. - $note = Note::lookup($id, $this->getId()); - if(!($tpl = $dept->getTemplate())) $tpl= $cfg->getDefaultTemplate(); @@ -1715,36 +1521,36 @@ class Ticket { if($tpl && ($msg=$tpl->getNoteAlertMsgTemplate()) && $email) { - + $msg = $this->replaceVars($msg, array('note' => $note)); - // Alert recipients + // Alert recipients $recipients=array(); - + //Last respondent. if($cfg->alertLastRespondentONNewNote()) $recipients[]=$this->getLastRespondent(); - + //Assigned staff if any...could be the last respondent if($cfg->alertAssignedONNewNote() && $this->isAssigned() && $this->getStaffId()) $recipients[]=$this->getStaff(); - + //Dept manager if($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId()) $recipients[]=$dept->getManager(); - $attachments =($attachments)?$this->getAttachments($id, 'N'):array(); + $attachments = $note->getAttachments(); $sentlist=array(); foreach( $recipients as $k=>$staff) { if(!$staff || !is_object($staff) || !$staff->getEmail() || !$staff->isAvailable()) continue; - if(in_array($staff->getEmail(),$sentlist) || ($staffId && $staffId==$staff->getId())) continue; - $alert = str_replace('%{recipient}',$staff->getFirstName(), $msg['body']); + if(in_array($staff->getEmail(), $sentlist) || ($staffId && $staffId==$staff->getId())) continue; + $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, $attachments); $sentlist[] = $staff->getEmail(); } } - - return $id; + + return $note; } //Print ticket... export the ticket thread as PDF. @@ -1757,123 +1563,25 @@ class Ticket { exit; } - //online based attached files. - function uploadAttachments($files, $refid, $type, $checkFileTypes=false) { - global $ost; - - $uploaded=array(); - $ost->validateFileUploads($files, $checkFileTypes); //Validator sets errors - if any - foreach($files as $file) { - if(!$file['error'] - && ($id=AttachmentFile::upload($file)) - && $this->saveAttachment($id, $refid, $type)) - $uploaded[]=$id; - elseif($file['error']!=UPLOAD_ERR_NO_FILE) { - - // log file upload errors as interal notes + syslog debug. - if($file['error'] && gettype($file['error'])=='string') - $error = $file['error']; - else - $error ='Error #'.$file['error']; - - $this->logNote('File Upload Error', $error, 'SYSTEM', false); - - $ost->logDebug('File Upload Error (Ticket #'.$this->getExtId().')', $error); - } - - } - - return $uploaded; - } - - /* Wrapper or uploadAttachments - - used on client interface - - file type check is forced - - $_FILES is passed. - */ - function uploadFiles($files, $refid, $type) { - return $this->uploadAttachments(Format::files($files), $refid, $type, true); - } - - /* Emailed & API attachments handler */ - function importAttachments($attachments, $refid, $type, $checkFileTypes=true) { - global $ost; - - if(!$attachments || !is_array($attachments)) return null; - - $files = array(); - foreach ($attachments as &$info) { - //Do error checking... - if ($checkFileTypes && !$ost->isFileTypeAllowed($info)) - $info['error'] = 'Invalid file type (ext) for '.Format::htmlchars($info['name']); - elseif ($info['encoding'] && !strcasecmp($info['encoding'], 'base64')) { - if(!($info['data'] = base64_decode($info['data'], true))) - $info['error'] = sprintf('%s: Poorly encoded base64 data', Format::htmlchars($info['name'])); - } - - if($info['error']) { - $this->logNote('File Import Error', $info['error'], 'SYSTEM', false); - } elseif (($id=$this->saveAttachment($info, $refid, $type))) { - $files[] = $id; - } - } - - return $files; - } - - - /* - Save attachment to the DB. upload/import (above). - - @file is a mixed var - can be ID or file hash. - */ - function saveAttachment($file, $refid, $type) { - - if(!$refid || !$type || !($fileId=is_numeric($file)?$file:AttachmentFile::save($file))) - return 0; - - $sql ='INSERT INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' - .' ,ticket_id='.db_input($this->getId()) - .' ,file_id='.db_input($fileId) - .' ,ref_id='.db_input($refid) - .' ,ref_type='.db_input($type); - - return (db_query($sql) && ($id=db_insert_id()))?$id:0; - } - - - - function deleteAttachments(){ - - $deleted=0; - // Clear reference table - $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getId())); - if ($res && db_affected_rows()) - $deleted = AttachmentFile::deleteOrphans(); - - return $deleted; - } - + function delete() { - function delete(){ - - $sql='DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1'; + $sql = 'DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1'; if(!db_query($sql) || !db_affected_rows()) return false; - db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getId())); - $this->deleteAttachments(); - + //delete just orphaned ticket thread & associated attachments. + $this->getThread()->delete(); + return true; } function update($vars, &$errors) { global $cfg, $thisstaff; - + if(!$cfg || !$thisstaff || !$thisstaff->canEditTickets()) return false; - + $fields=array(); $fields['name'] = array('type'=>'string', 'required'=>1, 'error'=>'Name required'); $fields['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); @@ -1889,7 +1597,7 @@ class Ticket { if(!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] = 'Missing or invalid data - check the errors and try again'; - if($vars['duedate']) { + if($vars['duedate']) { if($this->isClosed()) $errors['duedate']='Duedate can NOT be set on a closed ticket'; elseif(!$vars['time'] || strpos($vars['time'],':')===false) @@ -1899,7 +1607,7 @@ class Ticket { elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time()) $errors['duedate']='Due date must be in the future'; } - + //Make sure phone extension is valid if($vars['phone_ext'] ) { if(!is_numeric($vars['phone_ext']) && !$errors['phone']) @@ -1920,11 +1628,11 @@ class Ticket { .' ,topic_id='.db_input($vars['topicId']) .' ,sla_id='.db_input($vars['slaId']) .' ,duedate='.($vars['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))):'NULL'); - + 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()) @@ -1935,20 +1643,20 @@ class Ticket { $this->logNote('Ticket Updated', $vars['note'], $thisstaff); $this->reload(); - + return true; } - + /*============== Static functions. Use Ticket::function(params); ==================*/ function getIdByExtId($extId, $email=null) { - - if(!$extId || !is_numeric($extId)) + + if(!$extId || !is_numeric($extId)) return 0; $sql ='SELECT ticket_id FROM '.TICKET_TABLE.' ticket ' .' WHERE ticketID='.db_input($extId); - + if($email) $sql.=' AND email='.db_input($email); @@ -1959,9 +1667,14 @@ class Ticket { } - + 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; + return ($id + && is_numeric($id) + && ($ticket= new Ticket($id)) + && $ticket->getId()==$id + && $ticket->getThread()) + ?$ticket:null; } function lookupByExtId($id, $email=null) { @@ -1980,7 +1693,7 @@ class Ticket { return $id; } - function getIdByMessageId($mid,$email) { + function getIdByMessageId($mid, $email) { if(!$mid || !$email) return 0; @@ -1996,7 +1709,7 @@ class Ticket { return $id; } - function getOpenTicketsByEmail($email){ + function getOpenTicketsByEmail($email) { $sql='SELECT count(*) as open FROM '.TICKET_TABLE.' WHERE status='.db_input('open').' AND email='.db_input($email); if(($res=db_query($sql)) && db_num_rows($res)) @@ -2005,10 +1718,10 @@ class Ticket { return $num; } - /* Quick staff's tickets stats */ + /* 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() || $cfg->getDBVersion()) return null; @@ -2017,26 +1730,26 @@ class Ticket { .' ,count(overdue.ticket_id) as overdue, count(assigned.ticket_id) as assigned, 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\' + ON (open.ticket_id=ticket.ticket_id + AND open.status=\'open\' AND open.isanswered=0 '.((!($cfg->showAssignedTickets() || $staff->showAssignedTickets()))? ' AND open.staff_id=0 ':'').') ' .' LEFT JOIN '.TICKET_TABLE.' answered - ON (answered.ticket_id=ticket.ticket_id - AND answered.status=\'open\' + ON (answered.ticket_id=ticket.ticket_id + AND answered.status=\'open\' AND answered.isanswered=1) ' .' LEFT JOIN '.TICKET_TABLE.' overdue - ON (overdue.ticket_id=ticket.ticket_id - AND overdue.status=\'open\' + ON (overdue.ticket_id=ticket.ticket_id + AND overdue.status=\'open\' AND overdue.isoverdue=1) ' .' LEFT JOIN '.TICKET_TABLE.' assigned - ON (assigned.ticket_id=ticket.ticket_id - AND assigned.status=\'open\' + ON (assigned.ticket_id=ticket.ticket_id + AND assigned.status=\'open\' AND assigned.staff_id='.db_input($staff->getId()).')' .' LEFT JOIN '.TICKET_TABLE.' closed - ON (closed.ticket_id=ticket.ticket_id - AND closed.status=\'closed\' + ON (closed.ticket_id=ticket.ticket_id + AND closed.status=\'closed\' AND closed.staff_id='.db_input($staff->getId()).')' .' WHERE (ticket.staff_id='.db_input($staff->getId()); @@ -2048,12 +1761,15 @@ class Ticket { $sql.=')'; + if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets())) + $sql.=' AND (ticket.staff_id=0 OR ticket.staff_id='.db_input($staff->getId()).') '; + return db_fetch_array(db_query($sql)); } - /* Quick client's tickets stats - @email - valid email. + /* Quick client's tickets stats + @email - valid email. */ function getClientStats($email) { @@ -2075,7 +1791,7 @@ class Ticket { * The mother of all functions...You break it you fix it! * * $autorespond and $alertstaff overwrites config settings... - */ + */ function create($vars, &$errors, $origin, $autorespond=true, $alertstaff=true) { global $ost, $cfg, $thisclient, $_FILES; @@ -2091,14 +1807,14 @@ class Ticket { } //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL) - if($cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff') + 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."; - $ost->logWarning('Ticket denied -'.$vars['email'], - sprintf('Max open tickets (%d) reached for %s ', + $ost->logWarning('Ticket denied -'.$vars['email'], + sprintf('Max open tickets (%d) reached for %s ', $cfg->getMaxOpenTickets(), $vars['email'])); return 0; @@ -2108,12 +1824,12 @@ class Ticket { //Init ticket filters... $ticket_filter = new TicketFilter($origin, $vars); // Make sure email contents should not be rejected - if($ticket_filter + if($ticket_filter && ($filter=$ticket_filter->shouldReject())) { $errors['err']='Ticket denied. Error #403'; $errors['errno'] = 403; - $ost->logWarning('Ticket denied', - sprintf('Ticket rejected ( %s) by filter "%s"', + $ost->logWarning('Ticket denied', + sprintf('Ticket rejected ( %s) by filter "%s"', $vars['email'], $filter->getName())); return 0; @@ -2145,7 +1861,7 @@ class Ticket { } $fields['priorityId'] = array('type'=>'int', 'required'=>0, 'error'=>'Invalid Priority'); $fields['phone'] = array('type'=>'phone', 'required'=>0, 'error'=>'Valid phone # required'); - + if(!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] ='Missing or invalid data - check the errors and try again'; @@ -2213,7 +1929,7 @@ class Ticket { $deptId=$deptId?$deptId:$cfg->getDefaultDeptId(); $topicId=$vars['topicId']?$vars['topicId']:0; $ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR']; - + //We are ready son...hold on to the rails. $extId=Ticket::genExtRandID(); $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() ' @@ -2227,7 +1943,7 @@ class Ticket { .' ,subject='.db_input(Format::striptags($vars['subject'])) .' ,phone="'.db_input($vars['phone'],false).'"' .' ,phone_ext='.db_input($vars['phone_ext']?$vars['phone_ext']:'') - .' ,ip_address='.db_input($ipaddress) + .' ,ip_address='.db_input($ipaddress) .' ,source='.db_input($source); //Make sure the origin is staff - avoid firebug hack! @@ -2239,17 +1955,20 @@ class Ticket { return null; /* -------------------- POST CREATE ------------------------ */ - - if(!$cfg->useRandomIds()){ + + 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. - db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1'); + 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] } $dept = $ticket->getDept(); + //post the message. - $msgid=$ticket->postMessage($vars , $source, false); + 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']); @@ -2267,10 +1986,8 @@ class Ticket { # Messages that are clearly auto-responses from email systems should # not have a return 'ping' message - if ($autorespond && $vars['header'] && - TicketFilter::isAutoResponse(Mail_Parse::splitHeaders($vars['header']))) { + if ($autorespond && $message && $message->isAutoResponse()) $autorespond=false; - } //Don't auto respond to mailer daemons. if( $autorespond && @@ -2281,8 +1998,8 @@ class Ticket { //post canned auto-response IF any (disables new ticket auto-response). if ($vars['cannedResponseId'] - && $ticket->postCannedReply($vars['cannedResponseId'], $msgid, $autorespond)) { - $ticket->markUnAnswered(); //Leave the ticket as unanswred. + && $ticket->postCannedReply($vars['cannedResponseId'], $message->getId(), $autorespond)) { + $ticket->markUnAnswered(); //Leave the ticket as unanswred. $autorespond = false; } @@ -2292,7 +2009,7 @@ class Ticket { $autorespond=false; /***** See if we need to send some alerts ****/ - $ticket->onNewTicket($vars['message'], $autorespond, $alertstaff); + $ticket->onNewTicket($message, $autorespond, $alertstaff); /************ check if the user JUST reached the max. open tickets limit **********/ if($cfg->getMaxOpenTickets()>0 @@ -2300,7 +2017,7 @@ class Ticket { && ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) { $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff'))); } - + /* Start tracking ticket lifecycle events */ $ticket->logEvent('created'); @@ -2310,33 +2027,34 @@ class Ticket { } function open($vars, &$errors) { - global $thisstaff,$cfg; + 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['issue']) $errors['issue']='Summary of the issue required'; else $vars['message']=$vars['issue']; - if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other'))) - $errors['source']='Invalid source - '.Format::htmlchars($vars['source']); - if(!($ticket=Ticket::create($vars, $errors, 'staff', false, (!$vars['assignId'])))) return false; $vars['msgId']=$ticket->getLastMsgId(); - $respId = 0; - + // post response - if any - if($vars['response']) { + $response = null; + if($vars['response'] && $thisstaff->canPostReply()) { $vars['response'] = $ticket->replaceVars($vars['response']); - if(($respId=$ticket->postReply($vars, $errors, false))) { + 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']); } } + //Post Internal note if($vars['assignId'] && $thisstaff->canAssignTickets()) { //Assign ticket to staff or team. $ticket->assign($vars['assignId'], $vars['note']); @@ -2347,24 +2065,24 @@ class Ticket { } $ticket->reload(); - + if(!$cfg->notifyONNewStaffTicket() || !isset($vars['alertuser'])) return $ticket; //No alerts. //Send Notice to user --- if requested AND enabled!! - + $dept=$ticket->getDept(); if(!$dept || !($tpl=$dept->getTemplate())) $tpl=$cfg->getDefaultTemplate(); - + if(!$dept || !($email=$dept->getEmail())) $email =$cfg->getDefaultEmail(); if($tpl && ($msg=$tpl->getNewTicketNoticeMsgTemplate()) && $email) { - + $message = $vars['issue']; - if($vars['response']) - $message.="\n\n".$vars['response']; + if($response) + $message.="\n\n".$response->getBody(); if($vars['signature']=='mine') $signature=$thisstaff->getSignature(); @@ -2372,23 +2090,23 @@ class Ticket { $signature=$dept->getSignature(); else $signature=''; - - $msg = $ticket->replaceVars($msg, + + $msg = $ticket->replaceVars($msg, array('message' => $message, 'signature' => $signature)); if($cfg->stripQuotedReply() && ($tag=trim($cfg->getReplySeparator()))) $msg['body'] ="\n$tag\n\n".$msg['body']; - $attachments =($cfg->emailAttachments() && $respId)?$ticket->getAttachments($respId,'R'):array(); + $attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array(); $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments); } return $ticket; - + } - + function checkOverdue() { - + $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 ' .' INNER JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) ' .' WHERE status=\'open\' AND isoverdue=0 ' @@ -2407,6 +2125,6 @@ class Ticket { } } - + } ?> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index fa8a5b4a5420df21565f463b79c59ac33ecceca7..6684413c753e61bcb3188b94f5c8581754e9aa2b 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -72,7 +72,9 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { <tr><th><?php echo Format::db_datetime($entry['created']); ?> <span><?php echo $poster; ?></span></th></tr> <tr><td><?php echo Format::display($entry['body']); ?></td></tr> <?php - if($entry['attachments'] && ($links=$ticket->getAttachmentsLinks($entry['id'], $entry['thread_type']))) { ?> + if($entry['attachments'] + && ($tentry=$ticket->getThreadEntry($entry['id'])) + && ($links=$tentry->getAttachmentsLinks())) { ?> <tr><td class="info"><?php echo $links; ?></td></tr> <?php } ?> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index 60d61257fe3ba781a4e98ae3bfd6ebab280f4259..4d3f47f6f90849c6b4b6ed313fa95bcb96df2cc4 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -96,7 +96,7 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <input type="checkbox" name="show_related_tickets" value="1" <?php echo $config['show_related_tickets'] ?'checked="checked"':''; ?> > <em>(Show all related tickets on user login - otherwise access is restricted to one ticket view per login)</em> </td> - </tr> + </tr> <tr> <td width="180">Show Notes Inline:</td> <td> @@ -154,7 +154,7 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) </tr> <tr> <th colspan="2"> - <em><b>Attachments</b>: Size setting mainly apply to web tickets.</em> + <em><b>Attachments</b>: Size and max. uploads setting mainly apply to web tickets.</em> </th> </tr> <tr> @@ -166,14 +166,14 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) </td> </tr> <tr> - <td width="180">Emailed Attachments:</td> + <td width="180">Emailed/API Attachments:</td> <td> - <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed files + <input type="checkbox" name="allow_email_attachments" <?php echo $config['allow_email_attachments']?'checked="checked"':''; ?>> Accept emailed/API attachments. <font class="error"> <?php echo $errors['allow_email_attachments']; ?></font> </td> </tr> <tr> - <td width="180">Online Attachments:</td> + <td width="180">Online/Web Attachments:</td> <td> <input type="checkbox" name="allow_online_attachments" <?php echo $config['allow_online_attachments']?'checked="checked"':''; ?> > Allow web upload diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index f36c1e0bcd97dcf1fd067fd71e585675283788f5..ed29b87391f632efbdddc886db8809edfefb51ee 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -219,6 +219,10 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <textarea name="issue" cols="21" rows="8" style="width:80%;"><?php echo $info['issue']; ?></textarea> </td> </tr> + <?php + //is the user allowed to post replies?? + if($thisstaff->canPostReply()) { + ?> <tr> <th colspan="2"> <em><strong>Response</strong>: Optional response to the above issue.</em> @@ -270,8 +274,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </div> </td> </tr> - <?php - } ?> + <?php + } ?> <?php if($thisstaff->canCloseTickets()) { ?> @@ -304,6 +308,9 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </table> </td> </tr> + <?php + } //end canPostReply + ?> <tr> <th colspan="2"> <em><strong>Internal Note</strong>: Optional internal note (recommended on assignment) <font class="error"> <?php echo $errors['note']; ?></font></em> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 2a1a7aeb6953582744ba7cbebc549c4eda3d2183..45f25805b215f158cf0496da5f4c61340e307737 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -306,7 +306,9 @@ if(!$cfg->showNotesInline()) { ?> </td> </tr> <?php - if($note['attachments'] && ($links=$ticket->getAttachmentsLinks($note['id'],'N'))) {?> + if($note['attachments'] + && ($tentry=$ticket->getThreadEntry($note['id'])) + && ($links=$tentry->getAttachmentsLinks())) { ?> <tr> <td class="info" colspan="2"><?php echo $links; ?></td> </tr> @@ -325,7 +327,10 @@ if(!$cfg->showNotesInline()) { ?> <?php $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note'); /* -------- Messages & Responses & Notes (if inline)-------------*/ - if(($thread=$ticket->getThread($cfg->showNotesInline()))) { + $types = array('M', 'R'); + if($cfg->showNotesInline()) + $types[] = 'N'; + if(($thread=$ticket->getThreadEntries($types))) { foreach($thread as $entry) { ?> <table class="<?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> @@ -336,7 +341,9 @@ if(!$cfg->showNotesInline()) { ?> </tr> <tr><td colspan=3><?php echo Format::display($entry['body']); ?></td></tr> <?php - if($entry['attachments'] && ($links=$ticket->getAttachmentsLinks($entry['id'], $entry['thread_type']))) {?> + if($entry['attachments'] + && ($tentry=$ticket->getThreadEntry($entry['id'])) + && ($links=$tentry->getAttachmentsLinks())) {?> <tr> <td class="info" colspan=3><?php echo $links; ?></td> </tr> @@ -512,7 +519,7 @@ if(!$cfg->showNotesInline()) { ?> <input type="hidden" name="a" value="postnote"> <table border="0" cellspacing="0" cellpadding="3"> <?php - if($errors['note']) {?> + if($errors['postnote']) {?> <tr> <td width="160"> </td> <td class="error"><?php echo $errors['postnote']; ?></td> diff --git a/scp/canned.php b/scp/canned.php index edd4a4c36f185e3e379337e4d59882264388328c..c085e4116c2530d21db6932d22696aa2b981eb6b 100644 --- a/scp/canned.php +++ b/scp/canned.php @@ -44,7 +44,7 @@ if($_POST && $thisstaff->canManageCannedResponses()) { } } //Upload NEW attachments IF ANY - TODO: validate attachment types?? - if($_FILES['attachments'] && ($files=Format::files($_FILES['attachments']))) + if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments']))) $canned->uploadAttachments($files); $canned->reload(); @@ -58,7 +58,7 @@ if($_POST && $thisstaff->canManageCannedResponses()) { $msg='Canned response added successfully'; $_REQUEST['a']=null; //Upload attachments - if($_FILES['attachments'] && ($c=Canned::lookup($id)) && ($files=Format::files($_FILES['attachments']))) + if($_FILES['attachments'] && ($c=Canned::lookup($id)) && ($files=AttachmentFile::format($_FILES['attachments']))) $c->uploadAttachments($files); } elseif(!$errors['err']) { diff --git a/scp/tickets.php b/scp/tickets.php index 37683eabfc7adc13ed2a7580ea35fdbdb34ce496..0a9ba30a443e88034c12c453989fc4537822d184 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -1,9 +1,9 @@ <?php /************************************************************************* tickets.php - + Handles all tickets related actions. - + Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket http://www.osticket.com @@ -46,23 +46,25 @@ if($_POST && !$errors): $errors['err'] = 'Action denied. Contact admin for access'; else { - if(!$_POST['msgId']) - $errors['err']='Missing message ID - Internal error'; if(!$_POST['response']) $errors['response']='Response required'; - //Use locks to avoid double replies if($lock && $lock->getStaffId()!=$thisstaff->getId()) $errors['err']='Action Denied. Ticket is locked by someone else!'; - + //Make sure the email is not banned if(!$errors['err'] && TicketFilter::isBanned($ticket->getEmail())) $errors['err']='Email is in banlist. Must be removed to reply.'; } $wasOpen =($ticket->isOpen()); + //If no error...do the do. - if(!$errors && ($respId=$ticket->postReply($_POST, $errorsi, isset($_POST['emailreply'])))) { + $vars = $_POST; + if(!$errors && $_FILES['attachments']) + $vars['files'] = AttachmentFile::format($_FILES['attachments']); + + if(!$errors && ($response=$ticket->postReply($vars, $errors, isset($_POST['emailreply'])))) { $msg='Reply posted successfully'; $ticket->reload(); if($ticket->isClosed() && $wasOpen) @@ -73,7 +75,7 @@ if($_POST && !$errors): } break; case 'transfer': /** Transfer ticket **/ - //Check permission + //Check permission if(!$thisstaff->canTransferTickets()) $errors['err']=$errors['transfer'] = 'Action Denied. You are not allowed to transfer tickets.'; else { @@ -85,13 +87,13 @@ if($_POST && !$errors): $errors['deptId'] = 'Ticket already in the department'; elseif(!($dept=Dept::lookup($_POST['deptId']))) $errors['deptId'] = 'Unknown or invalid department'; - + //Transfer message - required. if(!$_POST['transfer_comments']) $errors['transfer_comments'] = 'Transfer comments required'; elseif(strlen($_POST['transfer_comments'])<5) $errors['transfer_comments'] = 'Transfer comments too short!'; - + //If no errors - them attempt the transfer. if(!$errors && $ticket->transfer($_POST['deptId'], $_POST['transfer_comments'])) { $msg = 'Ticket transferred successfully to '.$ticket->getDeptName(); @@ -112,7 +114,7 @@ if($_POST && !$errors): else { $id = preg_replace("/[^0-9]/", "",$_POST['assignId']); - $claim = (is_numeric($_POST['assignId']) && $_POST['assignId']==$thisstaff->getId()); + $claim = (is_numeric($_POST['assignId']) && $_POST['assignId']==$thisstaff->getId()); if(!$_POST['assignId'] || !$id) $errors['assignId'] = 'Select assignee'; @@ -132,7 +134,7 @@ if($_POST && !$errors): $errors['assign_comments'] = 'Assignment comments required'; elseif(strlen($_POST['assign_comments'])<5) $errors['assign_comments'] = 'Comment too short'; - + if(!$errors && $ticket->assign($_POST['assignId'], $_POST['assign_comments'], !$claim)) { if($claim) { $msg = 'Ticket is NOW assigned to you!'; @@ -146,7 +148,7 @@ if($_POST && !$errors): $errors['assign'] = 'Correct the error(s) below and try again!'; } } - break; + break; case 'postnote': /* Post Internal Note */ //Make sure the staff can set desired state if($_POST['state']) { @@ -158,12 +160,22 @@ if($_POST && !$errors): } $wasOpen = ($ticket->isOpen()); - if(($noteId=$ticket->postNote($_POST, $errors, $thisstaff))) { + + $vars = $_POST; + if($_FILES['attachments']) + $vars['files'] = AttachmentFile::format($_FILES['attachments']); + + if(($note=$ticket->postNote($vars, $errors, $thisstaff))) { + $msg='Internal note posted successfully'; if($wasOpen && $ticket->isClosed()) $ticket = null; //Going back to main listing. + } else { - $errors['err'] = 'Unable to post internal note - missing or invalid data.'; + + if(!$errors['err']) + $errors['err'] = 'Unable to post internal note - missing or invalid data.'; + $errors['postnote'] = 'Unable to post the note. Correct the error(s) below and try again!'; } break; @@ -195,9 +207,9 @@ if($_POST && !$errors): $note = $_POST['ticket_status_notes']; else $note='Ticket closed (without comments)'; - + $ticket->logNote('Ticket Closed', $note, $thisstaff); - + //Going back to main listing. TicketLock::removeStaffLocks($thisstaff->getId(), $ticket->getId()); $page=$ticket=null; @@ -299,7 +311,7 @@ if($_POST && !$errors): } elseif(Banlist::remove($ticket->getEmail())) { $msg = 'Email removed from banlist'; } elseif(!BanList::includes($ticket->getEmail())) { - $warn = 'Email is not in the banlist'; + $warn = 'Email is not in the banlist'; } else { $errors['err']='Unable to remove the email from banlist. Try again.'; } @@ -333,7 +345,7 @@ if($_POST && !$errors): switch($_POST['a']) { case 'mass_process': if(!$thisstaff->canManageTickets()) - $errors['err']='You do not have permission to mass manage tickets. Contact admin for such access'; + $errors['err']='You do not have permission to mass manage tickets. Contact admin for such access'; elseif(!$_POST['tids'] || !is_array($_POST['tids'])) $errors['err']='No tickets selected. You must select at least one ticket.'; else { @@ -364,7 +376,7 @@ if($_POST && !$errors): if($thisstaff->canCloseTickets()) { $note='Ticket closed without response by '.$thisstaff->getName(); foreach($_POST['tids'] as $k=>$v) { - if(($t=Ticket::lookup($v)) && $t->isOpen() && @$t->close()) { + if(($t=Ticket::lookup($v)) && $t->isOpen() && @$t->close()) { $i++; $t->logNote('Ticket Closed', $note, $thisstaff); } @@ -401,7 +413,7 @@ if($_POST && !$errors): foreach($_POST['tids'] as $k=>$v) { if(($t=Ticket::lookup($v)) && @$t->delete()) $i++; } - + //Log a warning if($i) { $log = sprintf('%s (%s) just deleted %d ticket(s)', @@ -429,13 +441,19 @@ if($_POST && !$errors): $ticket=null; if(!$thisstaff || !$thisstaff->canCreateTickets()) { $errors['err']='You do not have permission to create tickets. Contact admin for such access'; - }elseif(($ticket=Ticket::open($_POST, $errors))) { - $msg='Ticket created successfully'; - $_REQUEST['a']=null; - if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed()) - $ticket=null; - }elseif(!$errors['err']) { - $errors['err']='Unable to create the ticket. Correct the error(s) and try again'; + } else { + $vars = $_POST; + if($_FILES['attachments']) + $vars['files'] = AttachmentFile::format($_FILES['attachments']); + + if(($ticket=Ticket::open($vars, $errors))) { + $msg='Ticket created successfully'; + $_REQUEST['a']=null; + if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed()) + $ticket=null; + } elseif(!$errors['err']) { + $errors['err']='Unable to create the ticket. Correct the error(s) and try again'; + } } break; } @@ -470,7 +488,7 @@ if($cfg->showAnsweredTickets()) { 'title'=>'Answered Tickets', 'href'=>'tickets.php?status=answered', 'iconclass'=>'answeredTickets'), - ($_REQUEST['status']=='answered')); + ($_REQUEST['status']=='answered')); } } @@ -515,7 +533,7 @@ if($thisstaff->canCreateTickets()) { $nav->addSubMenu(array('desc'=>'New Ticket', 'href'=>'tickets.php?a=open', 'iconclass'=>'newTicket'), - ($_REQUEST['a']=='open')); + ($_REQUEST['a']=='open')); } @@ -524,7 +542,7 @@ if($ticket) { $ost->setPageTitle('Ticket #'.$ticket->getNumber()); $nav->setActiveSubMenu(-1); $inc = 'ticket-view.inc.php'; - if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) + if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) $inc = 'ticket-edit.inc.php'; elseif($_REQUEST['a'] == 'print' && !$ticket->pdfExport($_REQUEST['psize'], $_REQUEST['notes'])) $errors['err'] = 'Internal error: Unable to export the ticket to PDF for print.'; diff --git a/tickets.php b/tickets.php index d175d49b595e8d1676df0a69b6293d47d7c10c78..d1293db874536f9d2edf836b37838a019366ea79 100644 --- a/tickets.php +++ b/tickets.php @@ -40,12 +40,11 @@ if($_POST && is_object($ticket) && $ticket->getId()): if(!$errors) { //Everything checked out...do the magic. - if(($msgid=$ticket->postMessage(array('message'=>$_POST['message']), 'Web'))) { - - //Upload files - if($cfg->allowOnlineAttachments() && $_FILES['attachments']) - $ticket->uploadFiles($_FILES['attachments'], $msgid, 'M'); + $vars = array('message'=>$_POST['message']); + if($cfg->allowOnlineAttachments() && $_FILES['attachments']) + $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); + if(($msgid=$ticket->postMessage($vars, 'Web'))) { $msg='Message Posted Successfully'; } else { $errors['err']='Unable to post the message. Try again';