diff --git a/WHATSNEW.md b/WHATSNEW.md index afe5476bb591121a4c58a81881e0df7e644aab2d..460b4020f95e758483da45e8cf37bd669b4cb15d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,10 +1,23 @@ + +New stuff in 1.7-rc6 +==================== + * Bug fixes and enhancements from rc5 + +New stuff in 1.7-rc5 +==================== + * Bug fixes from rc4 + +New stuff in 1.7-rc4 +==================== + * Bug fixes from rc3 + New stuff in 1.7-rc3 ==================== * Bug fixes from rc2 * Canned auto-reply template * Modal dialogs * PEAR packages upgrade - * Email encoding + * Email encoding New stuff in 1.7-rc2 ==================== @@ -33,7 +46,7 @@ New stuff in 1.7-dpr4 New stuff in 1.7-dpr3 ====================== * Advanced search on tickets page - * Ticket thread -- revised ticket message storage model for greater + * Ticket thread -- revised ticket message storage model for greater flexability * New database upgrade system allowing for continuous updates to the database model. This will greatly simplify the process of making diff --git a/ajax.php b/ajax.php index 5210b628be577f830648e34a7f4d4a9a4c4fb3a1..a629af6392e312f35f02a5dc387b380fddf737eb 100644 --- a/ajax.php +++ b/ajax.php @@ -30,5 +30,5 @@ $dispatcher = patterns('', url_get('^client', 'client') )) ); -print $dispatcher->resolve($_SERVER['PATH_INFO']); +print $dispatcher->resolve($ost->get_path_info()); ?> diff --git a/api/cron.php b/api/cron.php index 43ff2b285a52bff60876ef061209bf868db2a178..0ca641b52062d6a817f67d49297e9e062d63f31b 100644 --- a/api/cron.php +++ b/api/cron.php @@ -13,8 +13,11 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -if (substr(php_sapi_name(), 0, 3) != 'cli') - die('cron.php only supports local cron jobs - use http -> api/task/cron'); +@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. +require('api.inc.php'); + +if (!osTicket::is_cli()) + die('cron.php only supports local cron calls - use http -> api/tasks/cron'); @chdir(realpath(dirname(__FILE__)).'/'); //Change dir. require('api.inc.php'); diff --git a/api/http.php b/api/http.php index 985d8e49893a280e180f4631bb2188f9116fa525..90926d1e5b531771c057ca82276711520c305b9e 100644 --- a/api/http.php +++ b/api/http.php @@ -20,11 +20,11 @@ require_once INCLUDE_DIR."class.dispatcher.php"; $dispatcher = patterns('', url_post("^/tickets\.(?P<format>xml|json|email)$", array('api.tickets.php:TicketApiController','create')), - url('^/task/', patterns('', + url('^/tasks/', patterns('', url_post("^cron$", array('api.cron.php:CronApiController', 'execute')) )) ); # Call the respective function -print $dispatcher->resolve($_SERVER['PATH_INFO']); +print $dispatcher->resolve($ost->get_path_info()); ?> diff --git a/api/pipe.php b/api/pipe.php index aa35f898463ce08b632e622a3555d4a474cf61ab..7cf1ad1b4f7c5edd88ba75cb5c7c66b683a109dc 100644 --- a/api/pipe.php +++ b/api/pipe.php @@ -14,14 +14,14 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +ini_set('memory_limit', '256M'); //The concern here is having enough mem for emails with attachments. +@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. +require('api.inc.php'); //Only local piping supported via pipe.php -if (substr(php_sapi_name(), 0, 3) != 'cli') +if (!osTicket::is_cli()) die('pipe.php only supports local piping - use http -> api/tickets.email'); -ini_set('memory_limit', '256M'); //The concern here is having enough mem for emails with attachments. -@chdir(realpath(dirname(__FILE__)).'/'); //Change dir. -require('api.inc.php'); require_once(INCLUDE_DIR.'api.tickets.php'); PipeApiController::process(); ?> diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index 9780f0e048f7598682038fd90f876af17c3f77ed..c8b90631856d3c6810a52dc38468dd5c8fb61702 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -702,9 +702,10 @@ body { .Icon.phoneTicket { background-image: url('../images/icons/ticket_source_phone.gif'); } -.Icon.otherTicket { +.Icon.otherTicket, .Icon.apiTicket { background-image: url('../images/icons/ticket_source_other.gif'); } + .Icon.attachment { background-image: url('../images/icons/attachment.gif'); } diff --git a/include/ajax.config.php b/include/ajax.config.php index 398b6dccdd151c205a32b307a664f764db92d608..2a01e284071ca6122024ffc78c390759f3b48467 100644 --- a/include/ajax.config.php +++ b/include/ajax.config.php @@ -24,8 +24,6 @@ class ConfigAjaxAPI extends AjaxController { $config=array( 'lock_time' => ($cfg->getLockTime()*3600), - 'file_types' => $cfg->getAllowedFileTypes(), - 'max_file_size' => (int) $cfg->getMaxFileSize(), 'max_file_uploads'=> (int) $cfg->getStaffMaxFileUploads() ); return $this->json_encode($config); diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index bb834578064358a26e77c2a1a6ec82f5df6eba9f..1d887f04349241327a324d43b8a2d31545c45cf1 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -85,10 +85,10 @@ class TicketsAjaxAPI extends AjaxController { } function search() { - global $thisstaff; + global $thisstaff, $cfg; $result=array(); - $select = 'SELECT count(ticket.ticket_id) as tickets '; + $select = 'SELECT count( DISTINCT ticket.ticket_id) as tickets '; $from = ' FROM '.TICKET_TABLE.' ticket '; $where = ' WHERE 1 '; @@ -107,11 +107,18 @@ class TicketsAjaxAPI extends AjaxController { if($_REQUEST['deptId']) $where.=' AND ticket.dept_id='.db_input($_REQUEST['deptId']); + //Help topic + if($_REQUEST['topicId']) + $where.=' AND ticket.topic_id='.db_input($_REQUEST['topicId']); + //Status switch(strtolower($_REQUEST['status'])) { - case 'open'; + case 'open': $where.=' AND ticket.status="open" '; break; + case 'answered': + $where.=' AND ticket.status="open" AND ticket.isanswered=1 '; + break; case 'overdue': $where.=' AND ticket.status="open" AND ticket.isoverdue=1 '; break; @@ -121,19 +128,23 @@ class TicketsAjaxAPI extends AjaxController { } //Assignee - if($_REQUEST['assignee'] && strcasecmp($_REQUEST['status'], 'closed')) { + if(isset($_REQUEST['assignee']) && strcasecmp($_REQUEST['status'], 'closed')) { $id=preg_replace("/[^0-9]/", "", $_REQUEST['assignee']); $assignee = $_REQUEST['assignee']; - $where.= ' AND ( '; + $where.= ' AND ( ( ticket.status="open" '; if($assignee[0]=='t') - $where.=' (ticket.team_id='.db_input($id). ' AND ticket.status="open") '; + $where.=' AND ticket.team_id='.db_input($id); elseif($assignee[0]=='s') - $where.=' (ticket.staff_id='.db_input($id). ' AND ticket.status="open") '; - else - $where.=' (ticket.staff_id='.db_input($id). ' AND ticket.status="open") '; + $where.=' AND ticket.staff_id='.db_input($id); + elseif(is_numeric($id)) + $where.=' AND ticket.staff_id='.db_input($id); + + $where.=')'; if($_REQUEST['staffId'] && !$_REQUEST['status']) //Assigned TO + Closed By - $where.= ' OR (ticket.staff_id='.db_input($_REQUEST['staffId']). ' AND ticket.status="closed") '; + $where.= ' OR (ticket.staff_id='.db_input($_REQUEST['staffId']). ' AND ticket.status="closed") '; + elseif(isset($_REQUEST['staffId'])) // closed by any + $where.= ' OR ticket.status="closed" '; $where.= ' ) '; } elseif($_REQUEST['staffId']) { @@ -163,7 +174,6 @@ class TicketsAjaxAPI extends AjaxController { ." OR thread.title LIKE '%$queryterm%'" ." OR thread.body LIKE '%$queryterm%'" .' )'; - $groupby = 'GROUP BY ticket.ticket_id '; } $sql="$select $from $where $groupby"; diff --git a/include/api.cron.php b/include/api.cron.php index 912fff41090df4887598a3b7f6cd0f915910b73b..32d1b0aefb25b2ac546af5188191f5e896410b4f 100644 --- a/include/api.cron.php +++ b/include/api.cron.php @@ -6,7 +6,7 @@ class CronApiController extends ApiController { function execute() { - if(!($key=$this->requireApiKey()) || !$key->canExecuteCronJob()) + if(!($key=$this->requireApiKey()) || !$key->canExecuteCron()) return $this->exerr(401, 'API key not authorized'); $this->run(); 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 dd0706ceaa374b5ee35c065ce9066590c53e4e05..dd15234676d1c3783f7bbb5af357b77feb945c56 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -71,7 +71,7 @@ class API { return ($this->ht['can_create_tickets']); } - function canExecuteCronjob() { + function canExecuteCron() { return ($this->ht['can_exec_cron']); } @@ -192,8 +192,9 @@ class ApiController { * work will be done for XML requests */ function getRequest($format) { - - $input = (substr(php_sapi_name(), 0, 3) == 'cli')?'php://stdin':'php://input'; + global $ost; + + $input = $ost->is_cli()?'php://stdin':'php://input'; if (!($stream = @fopen($input, 'r'))) $this->exerr(400, "Unable to read request body"); @@ -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.export.php b/include/class.export.php index 3c289550121366e8fd53e59c51e8715f0db27385..372cf62e2d6c55f09e25c59be29ba9691f075df5 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -41,6 +41,7 @@ class Export { 'name' => 'From', 'priority_desc' => 'Priority', 'dept_name' => 'Department', + 'helptopic' => 'Help Topic', 'source' => 'Source', 'status' => 'Current Status' ), 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 220f207426ccaa71089cab46b82467b3dfd7e1b0..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') { @@ -68,7 +55,7 @@ class Format { //Wrapper for utf-8 encoding. function utf8encode($text, $charset=null) { - return Format::enecode($text, $charset, 'utf-8'); + return Format::encode($text, $charset, 'utf-8'); } function phone($phone) { @@ -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..512d390e284bdd64b7587f523f71c2ec75712fe4 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -26,11 +26,11 @@ define('LOG_WARN',LOG_WARNING); class osTicket { var $loglevel=array(1=>'Error','Warning','Debug'); - + //Page errors. var $errors; - //System + //System var $system; @@ -47,7 +47,7 @@ class osTicket { var $csrf; function osTicket($cfgId) { - + $this->config = Config::lookup($cfgId); //DB based session storage was added starting with v1.7 @@ -109,13 +109,13 @@ class osTicket { $name = $name?$name:$this->getCSRF()->getTokenName(); if(isset($_POST[$name]) && $this->validateCSRFToken($_POST[$name])) return true; - + if(isset($_SERVER['HTTP_X_CSRFTOKEN']) && $this->validateCSRFToken($_SERVER['HTTP_X_CSRFTOKEN'])) return true; $msg=sprintf('Invalid CSRF token [%s] on %s', ($_POST[$name].''.$_SERVER['HTTP_X_CSRFTOKEN']), THISPAGE); - $this->logWarning('Invalid CSRF Token '.$name, $msg); + $this->logWarning('Invalid CSRF Token '.$name, $msg, false); return false; } @@ -129,7 +129,7 @@ class osTicket { } function isFileTypeAllowed($file, $mimeType='') { - + if(!$file || !($allowedFileTypes=$this->getConfig()->getAllowedFileTypes())) return false; @@ -146,38 +146,11 @@ 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()) { - + $replacer = new VariableReplacer(); - $replacer->assign(array_merge($vars, + $replacer->assign(array_merge($vars, array('url' => $this->getConfig()->getBaseUrl()) )); @@ -247,7 +220,7 @@ class osTicket { function alertAdmin($subject, $message, $log=false) { - + //Set admin's email address if(!($to=$this->getConfig()->getAdminEmail())) $to=ADMIN_EMAIL; @@ -258,7 +231,7 @@ class osTicket { //Try getting the alert email. $email=null; - if(!($email=$this->getConfig()->getAlertEmail())) + if(!($email=$this->getConfig()->getAlertEmail())) $email=$this->getConfig()->getDefaultEmail(); //will take the default email. if($email) { @@ -284,7 +257,7 @@ class osTicket { function logWarning($title, $message, $alert=true) { return $this->log(LOG_WARN, $title, $message, $alert); } - + function logError($title, $error, $alert=true) { return $this->log(LOG_ERR, $title, $error, $alert); } @@ -302,8 +275,8 @@ class osTicket { //We are providing only 3 levels of logs. Windows style. switch($priority) { case LOG_EMERG: - case LOG_ALERT: - case LOG_CRIT: + case LOG_ALERT: + case LOG_CRIT: case LOG_ERR: $level=1; //Error break; @@ -333,9 +306,9 @@ class osTicket { ',log_type='.db_input($loglevel[$level]). ',log='.db_input($message). ',ip_address='.db_input($_SERVER['REMOTE_ADDR']); - + mysql_query($sql); //don't use db_query to avoid possible loop. - + return true; } @@ -347,11 +320,48 @@ class osTicket { //System logs $sql='DELETE FROM '.SYSLOG_TABLE.' WHERE DATE_ADD(created, INTERVAL '.$gp.' MONTH)<=NOW()'; db_query($sql); - + //TODO: Activity logs return true; } + /* + * Util functions + * + */ + + function get_var($index, $vars, $default='', $type=null) { + + if(is_array($vars) + && array_key_exists($index, $vars) + && (!$type || gettype($vars[$index])==$type)) + return $vars[$index]; + + return $default; + } + + function get_db_input($index, $vars, $quote=true) { + return db_input($this->get_var($index, $vars), $quote); + } + + function get_path_info() { + if(isset($_SERVER['PATH_INFO'])) + return $_SERVER['PATH_INFO']; + + if(isset($_SERVER['ORIG_PATH_INFO'])) + return $_SERVER['ORIG_PATH_INFO']; + + //TODO: conruct possible path info. + + return null; + } + + /* returns true if script is being executed via commandline */ + function is_cli() { + return (!strcasecmp(substr(php_sapi_name(), 0, 3), 'cli') + || (!$_SERVER['REQUEST_METHOD'] && !$_SERVER['HTTP_HOST']) //Fallback when php-cgi binary is used via cli + ); + } /**** static functions ****/ function start($configId) { 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 a7a16e4644ba357db5f619ec50760de71de60a20..17d2c56b3545733d3569261eacb1d8bd79328070 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,24 +205,24 @@ 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() { + function getSLADueDate() { return $this->ht['sla_duedate']; } 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); - } - - function getThreadWithoutNotes() { - return $this->getThread(false); + return $this->getThreadEntries(array('M', 'R')); } - function getThread($includeNotes=false, $order='') { - - $treadtypes=array('M', 'R'); // messages and responses. - if($includeNotes) //Include notes?? - $treadtypes[] = 'N'; - - return $this->getThreadByType($treadtypes, $order); + function getThreadEntry($id) { + return $this->getThread()->getEntry($id); } - - 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 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,24 +1094,31 @@ class Ticket { function clearOverdue() { - if(!$this->isOverdue()) + if(!$this->isOverdue()) return true; + //NOTE: Previously logged overdue event is NOT annuled. + $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() '; + //clear due date if it's in the past if($this->getDueDate() && strtotime($this->getDueDate())<=time()) $sql.=', duedate=NULL'; + //Clear SLA if est. due date is in the past + if($this->getSLADueDate() && strtotime($this->getSLADueDate())<=time()) + $sql.=', sla_id=0 '; + $sql.=' WHERE ticket_id='.db_input($this->getId()); return (db_query($sql) && db_affected_rows()); } - //Dept Tranfer...with alert.. done by staff + //Dept Tranfer...with alert.. done by staff function transfer($deptId, $comments, $alert = true) { - + global $cfg, $thisstaff; - + if(!$thisstaff || !$thisstaff->canTransferTickets()) return false; @@ -1256,8 +1126,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 +1135,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 +1150,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 +1176,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 +1193,7 @@ class Ticket { if(!is_object($staff) && !($staff=Staff::lookup($staff))) return false; - + if(!$this->setStaffId($staff->getId())) return false; @@ -1357,7 +1227,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 +1238,7 @@ class Ticket { return $rv; } - + //unassign primary assignee function unassign() { @@ -1391,52 +1261,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 +1310,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 +1350,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 +1377,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 +1429,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 +1494,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 +1518,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 +1528,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 +1570,25 @@ class Ticket { exit; } - //online based attached files. - function uploadAttachments($files, $refid, $type, $checkFileTypes=false) { - global $ost; + function delete() { - $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(){ - - $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 +1604,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 +1614,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 +1635,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 +1650,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 +1674,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 +1700,7 @@ class Ticket { return $id; } - function getIdByMessageId($mid,$email) { + function getIdByMessageId($mid, $email) { if(!$mid || !$email) return 0; @@ -1996,7 +1716,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,28 +1725,39 @@ 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; - $sql='SELECT count(open.ticket_id) as open, count(answered.ticket_id) as answered ' .' ,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\' AND open.isanswered=0) ' + 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\' AND answered.isanswered=1) ' + 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\' AND overdue.isoverdue=1) ' + 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\' AND assigned.staff_id='.db_input($staff->getId()).')' + 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\' AND closed.staff_id='.db_input($staff->getId()).')' + 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()); if(($teams=$staff->getTeams())) @@ -2040,13 +1771,12 @@ class Ticket { 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) { @@ -2068,7 +1798,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; @@ -2084,14 +1814,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; @@ -2101,12 +1831,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; @@ -2138,7 +1868,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'; @@ -2206,7 +1936,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() ' @@ -2220,7 +1950,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! @@ -2232,17 +1962,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']); @@ -2260,10 +1993,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 && @@ -2274,8 +2005,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; } @@ -2285,7 +2016,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 @@ -2293,7 +2024,7 @@ class Ticket { && ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) { $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff'))); } - + /* Start tracking ticket lifecycle events */ $ticket->logEvent('created'); @@ -2303,33 +2034,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']); @@ -2340,24 +2072,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(); @@ -2365,23 +2097,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 ' @@ -2400,6 +2132,6 @@ class Ticket { } } - + } ?> diff --git a/include/class.upgrader.php b/include/class.upgrader.php index 7e45e9e6a0e33933148ac7133b280c2759f09fd4..8b86b9e5724f07ea3fbb4bf3132a554e79ca7586 100644 --- a/include/class.upgrader.php +++ b/include/class.upgrader.php @@ -23,13 +23,16 @@ class Upgrader extends SetupWizard { var $sqldir; var $signature; + var $state; + var $mode; + function Upgrader($signature, $prefix, $sqldir) { $this->signature = $signature; - $this->shash = substr($signature, 0, 8); $this->prefix = $prefix; $this->sqldir = $sqldir; $this->errors = array(); + $this->mode = 'ajax'; // //Disable time limit if - safe mode is set. if(!ini_get('safe_mode')) @@ -38,6 +41,8 @@ class Upgrader extends SetupWizard { //Init persistent state of upgrade. $this->state = &$_SESSION['ost_upgrader']['state']; + $this->mode = &$_SESSION['ost_upgrader']['mode']; + //Init the task Manager. if(!isset($_SESSION['ost_upgrader'][$this->getShash()])) $_SESSION['ost_upgrader'][$this->getShash()]['tasks']=array(); @@ -45,8 +50,8 @@ class Upgrader extends SetupWizard { //Tasks to perform - saved on the session. $this->tasks = &$_SESSION['ost_upgrader'][$this->getShash()]['tasks']; - //Database migrater - $this->migrater = new DatabaseMigrater($this->signature, SCHEMA_SIGNATURE, $this->sqldir); + //Database migrater + $this->migrater = null; } function onError($error) { @@ -87,7 +92,7 @@ class Upgrader extends SetupWizard { } function getShash() { - return $this->shash; + return substr($this->getSchemaSignature(), 0, 8); } function getTablePrefix() { @@ -106,13 +111,31 @@ class Upgrader extends SetupWizard { $this->state = $state; } + function getMode() { + return $this->mode; + } + + function setMode($mode) { + $this->mode = $mode; + } + + function getMigrater() { + if(!$this->migrater) + $this->migrater = new DatabaseMigrater($this->signature, SCHEMA_SIGNATURE, $this->sqldir); + + return $this->migrater; + } + function getPatches() { - return $this->migrater->getPatches(); + $patches = array(); + if($this->getMigrater()) + $patches = $this->getMigrater()->getPatches(); + + return $patches; } function getNextPatch() { - $p = $this->getPatches(); - return (count($p)) ? $p[0] : false; + return (($p=$this->getPatches()) && count($p)) ? $p[0] : false; } function getNextVersion() { @@ -140,7 +163,7 @@ class Upgrader extends SetupWizard { $action='Upgrade osTicket to '.$this->getVersion(); if($this->getNumPendingTasks() && ($task=$this->getNextTask())) { $action = $task['desc']; - if($task['status']) //Progress report... + if($task['status']) //Progress report... $action.=' ('.$task['status'].')'; } elseif($this->isUpgradable() && ($nextversion = $this->getNextVersion())) { $action = "Upgrade to $nextversion"; @@ -161,9 +184,9 @@ class Upgrader extends SetupWizard { foreach($tasks as $k => $task) { if(!$task['done']) $pending[$k] = $task; - } + } } - + return $pending; } @@ -198,7 +221,11 @@ class Upgrader extends SetupWizard { if(!($tasks=$this->getPendingTasks())) return true; //Nothing to do. - $ost->logDebug('Upgrader', sprintf('There are %d pending upgrade tasks', count($tasks))); + $c = count($tasks); + $ost->logDebug( + sprintf('Upgrader - %s (%d pending tasks).', $this->getShash(), $c), + sprintf('There are %d pending upgrade tasks for %s patch', $c, $this->getShash()) + ); $start_time = Misc::micro_time(); foreach($tasks as $k => $task) { //TODO: check time used vs. max execution - break if need be @@ -211,7 +238,7 @@ class Upgrader extends SetupWizard { return $this->getPendingTasks(); } - + function upgrade() { global $ost; @@ -227,18 +254,20 @@ class Upgrader extends SetupWizard { if (!$this->load_sql_file($patch, $this->getTablePrefix())) return false; - //clear previous patch info - + //clear previous patch info - unset($_SESSION['ost_upgrader'][$this->getShash()]); $phash = substr(basename($patch), 0, 17); + $shash = substr($phash, 9, 8); //Log the patch info - $logMsg = "Patch $phash applied "; + $logMsg = "Patch $phash applied successfully "; if(($info = $this->readPatchInfo($patch)) && $info['version']) $logMsg.= ' ('.$info['version'].') '; - $ost->logDebug('Upgrader - Patch applied', $logMsg); - + $ost->logDebug("Upgrader - $shash applied", $logMsg); + $this->signature = $shash; //Update signature to the *new* HEAD + //Check if the said patch has scripted tasks if(!($tasks=$this->getTasksForPatch($phash))) { //Break IF elapsed time is greater than 80% max time allowed. @@ -250,16 +279,14 @@ class Upgrader extends SetupWizard { } //We have work to do... set the tasks and break. - $shash = substr($phash, 9, 8); $_SESSION['ost_upgrader'][$shash]['tasks'] = $tasks; $_SESSION['ost_upgrader'][$shash]['state'] = 'upgrade'; - - $ost->logDebug('Upgrader', sprintf('Found %d tasks to be executed for %s', - count($tasks), $shash)); break; - } + //Reset the migrater + $this->migrater = null; + return true; } @@ -286,9 +313,9 @@ class Upgrader extends SetupWizard { break; } - //Check IF SQL cleanup exists. + //Check IF SQL cleanup exists. $file=$this->getSQLDir().$phash.'.cleanup.sql'; - if(file_exists($file)) + if(file_exists($file)) $tasks[] = array('func' => 'cleanup', 'desc' => 'Post-upgrade cleanup!', 'phash' => $phash); @@ -306,7 +333,7 @@ class Upgrader extends SetupWizard { if(!file_exists($file)) //No cleanup script. return 0; - //We have a cleanup script ::XXX: Don't abort on error? + //We have a cleanup script ::XXX: Don't abort on error? if($this->load_sql_file($file, $this->getTablePrefix(), false, true)) return 0; @@ -317,7 +344,7 @@ class Upgrader extends SetupWizard { function migrateAttachments2DB($taskId) { global $ost; - + if(!($max_time = ini_get('max_execution_time'))) $max_time = 30; //Default to 30 sec batches. @@ -330,7 +357,7 @@ class Upgrader extends SetupWizard { function migrateSessionFile2DB($taskId) { # How about 'dis for a hack? - osTicketSession::write(session_id(), session_encode()); + osTicketSession::write(session_id(), session_encode()); return 0; } diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 7f1751872e3ec4d2db60f0855dd433edbafb2d3a..8eda7eca323e9cf5a9cc8b142575acd9a72d59de 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -28,7 +28,7 @@ if($sort && $sortOptions[$sort]) $order_by =$sortOptions[$sort]; $order_by=$order_by?$order_by:'ticket_created'; -if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) +if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) $order=$orderWays[strtoupper($_REQUEST['order'])]; $order=$order?$order:'ASC'; @@ -69,7 +69,8 @@ if($search) { } $total=db_count('SELECT count(DISTINCT ticket.ticket_id) '.$qfrom.' '.$qwhere); -$pageNav=new Pagenate($total,$page, PAGE_LIMIT); +$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1; +$pageNav=new Pagenate($total, $page, PAGE_LIMIT); $pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); //more stuff... @@ -95,8 +96,15 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting <input type="text" name="q" size="20" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>"> <select name="status"> <option value="">— Any Status —</option> - <option value="open" <?php echo ($status=='open')?'selected="selected"':'';?>>Open</option> - <option value="closed" <?php echo ($status=='closed')?'selected="selected"':'';?>>Closed</option> + <option value="open" + <?php echo ($status=='open')?'selected="selected"':'';?>>Open (<?php echo $thisclient->getNumOpenTickets(); ?>)</option> + <?php + if($thisclient->getNumClosedTickets()) { + ?> + <option value="closed" + <?php echo ($status=='closed')?'selected="selected"':'';?>>Closed (<?php echo $thisclient->getNumClosedTickets(); ?>)</option> + <?php + } ?> </select> <input type="submit" value="Go"> </form> @@ -144,7 +152,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting ?> <tr id="<?php echo $row['ticketID']; ?>"> <td class="centered"> - <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>" + <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>" href="tickets.php?id=<?php echo $row['ticketID']; ?>"><?php echo $ticketID; ?></a> </td> <td> <?php echo Format::db_date($row['created']); ?></td> @@ -165,7 +173,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting </tbody> </table> <?php -if($res && $num>0) { +if($res && $num>0) { echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; } ?> 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/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index ecbedfc668bda09277ebd21ef71e4bd921391ebb..ed5916196bcbb731cd1ced3ed4db19e4a2ea80bb 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -19,10 +19,9 @@ if($search) { $searchTerm=''; } } -$showoverdue=$showanswered=$showassigned=false; +$showoverdue=$showanswered=false; $staffId=0; //Nothing for now...TODO: Allow admin and manager to limit tickets to single staff level. -//show Assigned To column, if enabled. Admins and managers can overwrite system settings! -$showassigned=(($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets()) && !$search); +$showassigned= true; //show Assigned To column - defaults to true //Get status we are actually going to use on the query...making sure it is clean! $status=null; @@ -32,7 +31,7 @@ switch(strtolower($_REQUEST['status'])){ //Status is overloaded break; case 'closed': $status='closed'; - $showassigned=false; + $showassigned=true; //closed by. break; case 'overdue': $status='open'; @@ -51,7 +50,7 @@ switch(strtolower($_REQUEST['status'])){ //Status is overloaded break; default: if(!$search) - $status='open'; + $_REQUEST['status']=$status='open'; } $qwhere =''; @@ -77,8 +76,8 @@ if($status) { $qwhere.=' AND status='.db_input(strtolower($status)); } -//Overloaded sub-statuses - you've got to just have faith! -if($staffId && ($staffId==$thisstaff->getId())) { //Staff's assigned tickets. +//Queues: Overloaded sub-statuses - you've got to just have faith! +if($staffId && ($staffId==$thisstaff->getId())) { //My tickets $results_type='Assigned Tickets'; $qwhere.=' AND ticket.staff_id='.db_input($staffId); $showassigned=false; //My tickets...already assigned to the staff. @@ -86,14 +85,20 @@ if($staffId && ($staffId==$thisstaff->getId())) { //Staff's assigned tickets. $qwhere.=' AND isoverdue=1 '; }elseif($showanswered) { ////Answered $qwhere.=' AND isanswered=1 '; -}elseif(!$search && !$cfg->showAnsweredTickets() && !strcasecmp($status,'open')) { - $qwhere.=' AND isanswered=0 '; +}elseif(!strcasecmp($status, 'open') && !$search) { //Open queue (on search OPEN means all open tickets - regardless of state). + //Showing answered tickets on open queue?? + if(!$cfg->showAnsweredTickets()) + $qwhere.=' AND isanswered=0 '; + + /* Showing assigned tickets on open queue? + Don't confuse it with show assigned To column -> F'it it's confusing - just trust me! + */ + if(!($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets())) { + $qwhere.=' AND ticket.staff_id=0 '; //XXX: NOT factoring in team assignments - only staff assignments. + $showassigned=false; //Not showing Assigned To column since assigned tickets are not part of open queue + } } -//******* Showing assigned tickets? (don't confuse it with show assigned To column). F'it it's confusing - just trust me! ***/ -if(!($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets()) && strcasecmp($status,'closed') && !$search) - $sql.=' AND (ticket.staff_id=0 OR ticket.staff_id='.db_input($thisstaff->getId()).') '; - //Search?? Somebody...get me some coffee $deep_search=false; if($search): @@ -137,25 +142,36 @@ if($search): $qwhere.=' AND ticket.dept_id='.db_input($_REQUEST['deptId']); $qstr.='&deptId='.urlencode($_REQUEST['deptId']); } + + //Help topic + if($_REQUEST['topicId']) { + $qwhere.=' AND ticket.topic_id='.db_input($_REQUEST['topicId']); + $qstr.='&topicId='.urlencode($_REQUEST['topicId']); + } //Assignee - if($_REQUEST['assignee'] && strcasecmp($_REQUEST['status'], 'closed')) { + if(isset($_REQUEST['assignee']) && strcasecmp($_REQUEST['status'], 'closed')) { $id=preg_replace("/[^0-9]/", "", $_REQUEST['assignee']); $assignee = $_REQUEST['assignee']; $qstr.='&assignee='.urlencode($_REQUEST['assignee']); - $qwhere.= ' AND ( '; + $qwhere.= ' AND ( + ( ticket.status="open" '; if($assignee[0]=='t') - $qwhere.=' (ticket.team_id='.db_input($id). ' AND ticket.status="open") '; + $qwhere.=' AND ticket.team_id='.db_input($id); elseif($assignee[0]=='s') - $qwhere.=' (ticket.staff_id='.db_input($id). ' AND ticket.status="open") '; - else - $qwhere.=' (ticket.staff_id='.db_input($id). ' AND ticket.status="open") '; + $qwhere.=' AND ticket.staff_id='.db_input($id); + elseif(is_numeric($id)) + $qwhere.=' AND ticket.staff_id='.db_input($id); + $qwhere.=' ) '; if($_REQUEST['staffId'] && !$_REQUEST['status']) { //Assigned TO + Closed By $qwhere.= ' OR (ticket.staff_id='.db_input($_REQUEST['staffId']). ' AND ticket.status="closed") '; $qstr.='&staffId='.urlencode($_REQUEST['staffId']); + }elseif(isset($_REQUEST['staffId'])) { + $qwhere.= ' OR ticket.status="closed" '; + $qstr.='&staffId='.urlencode($_REQUEST['staffId']); } $qwhere.= ' ) '; @@ -263,7 +279,8 @@ $qselect.=' ,count(attach.attach_id) as attachments ' .' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate ' .' ,IF(ticket.reopened is NULL,IF(ticket.lastmessage is NULL,ticket.created,ticket.lastmessage),ticket.reopened) as effective_date ' .' ,CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team ' - .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned '; + .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned ' + .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '; $qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.priority_id) ' .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW() @@ -272,7 +289,10 @@ $qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.pri .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) ' .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) ' .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) ' - .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '; + .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) ' + .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) ' + .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) '; + $query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); //echo $query; @@ -346,20 +366,23 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <?php } - if($showassigned){ ?> - <th width="150"> - <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="Sort By Assignee <?php echo $negorder;?>">Assigned To</a></th> - <?php - } elseif(!strcasecmp($status,'closed')) { ?> - <th width="150"> - <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>" - title="Sort By Closing Staff Name <?php echo $negorder; ?>">Closed By</a></th> - <?php + if($showassigned ) { + //Closed by + if(!strcasecmp($status,'closed')) { ?> + <th width="150"> + <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Closing Staff Name <?php echo $negorder; ?>">Closed By</a></th> + <?php + } else { //assigned to ?> + <th width="150"> + <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + title="Sort By Assignee <?php echo $negorder;?>">Assigned To</a></th> + <?php + } } else { ?> - <th width="150"> - <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>" - title="Sort By Department <?php echo $negorder; ?>">Department</a></th> + <th width="150"> + <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>" + title="Sort By Department <?php echo $negorder; ?>">Department</a></th> <?php } ?> </tr> @@ -379,7 +402,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. $flag='overdue'; $lc=''; - if($showassigned || !strcasecmp($status,'closed')) { + if($showassigned) { if($row['staff_id']) $lc=sprintf('<span class="Icon staffAssigned">%s</span>',Format::truncate($row['staff'],40)); elseif($row['team_id']) @@ -544,6 +567,11 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <select id="status" name="status"> <option value="">— Any Status —</option> <option value="open">Open</option> + <?php + if(!$cfg->showAnsweredTickets()) {?> + <option value="answered">Answered</option> + <?php + } ?> <option value="overdue">Overdue</option> <option value="closed">Closed</option> </select> @@ -563,7 +591,9 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <fieldset class="owner"> <label for="assignee">Assigned To:</label> <select id="assignee" name="assignee"> - <option value="0">— Anyone —</option> + <option value="">— Anyone —</option> + <option value="0">— Unassigned —</option> + <option value="<?php echo $thisstaff->getId(); ?>">Me</option> <?php if(($users=Staff::getStaffMembers())) { echo '<OPTGROUP label="Staff Members ('.count($users).')">'; @@ -587,6 +617,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <label for="staffId">Closed By:</label> <select id="staffId" name="staffId"> <option value="0">— Anyone —</option> + <option value="<?php echo $thisstaff->getId(); ?>">Me</option> <?php if(($users=Staff::getStaffMembers())) { foreach($users as $id => $name) @@ -595,6 +626,18 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. ?> </select> </fieldset> + <fieldset> + <label for="topicId">Help Topic:</label> + <select id="topicId" name="topicId"> + <option value="" selected >— All Help Topics —</option> + <?php + if($topics=Topic::getHelpTopics()) { + foreach($topics as $id =>$name) + echo sprintf('<option value="%d" >%s</option>', $id, $name); + } + ?> + </select> + </fieldset> <fieldset class="date_range"> <label>Date Range:</label> <input class="dp" type="input" size="20" name="startDate"> diff --git a/include/upgrader/upgrade.inc.php b/include/upgrader/upgrade.inc.php index fae6947d94849c6cfbd4a7df849df708e470f1de..b157b6a030e4e0e23165a4a5720e31fe114ea133 100644 --- a/include/upgrader/upgrade.inc.php +++ b/include/upgrader/upgrade.inc.php @@ -1,5 +1,16 @@ <?php if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied'); + +//See if we need to switch the mode of upgrade...e.g from ajax (default) to manual +if(($mode = $ost->get_var('m', $_GET)) && $mode!=$upgrader->getMode()) { + //Set Persistent mode/ + $upgrader->setMode($mode); + //Log warning about ajax calls - most likely culprit is AcceptPathInfo directive. + if($mode=='manual') + $ost->logWarning('Ajax calls are failing', + 'Make sure your server has AcceptPathInfo directive set to "ON" or get technical help'); +} + $action=$upgrader->getNextAction(); ?> <h2>osTicket Upgrade</h2> @@ -9,7 +20,7 @@ $action=$upgrader->getNextAction(); <p>Thank you for taking the time to upgrade your osTicket intallation!</p> <p>Please don't cancel or close the browser, any errors at this stage will be fatal.</p> </div> - <h2><?php echo $action ?></h2> + <h2 id="task"><?php echo $action ?></h2> <p>The upgrade wizard will now attempt to upgrade your database and core settings!</p> <ul> <li>Database enhancements</li> @@ -20,8 +31,9 @@ $action=$upgrader->getNextAction(); <form method="post" action="upgrade.php" id="upgrade"> <?php csrf_token(); ?> <input type="hidden" name="s" value="upgrade"> + <input type="hidden" id="mode" name="m" value="<?php echo $upgrader->getMode(); ?>"> <input type="hidden" name="sh" value="<?php echo $upgrader->getSchemaSignature(); ?>"> - <input class="btn" type="submit" name="submit" value="Do It Now!"> + <input class="btn" type="submit" name="submit" value="Upgrade Now!"> </form> </div> </div> @@ -33,9 +45,11 @@ $action=$upgrader->getNextAction(); </div> <div class="clear"></div> <div id="upgrading"> - <h4><?php echo $action; ?></h4> + <h4 id="action"><?php echo $action; ?></h4> Please wait... while we upgrade your osTicket installation! - <div id="msg" style="font-weight: bold;padding-top:10px;">Smile!</div> + <div id="msg" style="font-weight: bold;padding-top:10px;"> + <?php echo sprintf("%s - Relax!", $thisstaff->getFirstName()); ?> + </div> </div> </div> -<div class="clear"></div>` +<div class="clear"></div> diff --git a/js/jquery.multifile.js b/js/jquery.multifile.js index 9b9a23b7d47065e1ecdea827f4b4d2cc05574dc6..0c0dd4a1cc4fa75e59a0bc232acde768dede0b60 100644 --- a/js/jquery.multifile.js +++ b/js/jquery.multifile.js @@ -3,7 +3,9 @@ Multifile plugin that allows users to upload multiple files at once in unobstructive manner - cleaner interface. - Allows limiting number of files and file type(s) using file extension. + Allows limiting number of files + Whitelist file type(s) using file extension + Limit file sizes. NOTE: * Files are not uploaded until the form is submitted @@ -50,7 +52,21 @@ if(fObj.data('files')>=settings.max_uploads || (fObj.data('files')+file.count)>settings.max_uploads) { alert('You have reached the maximum number of files ('+ settings.max_uploads+') allowed per upload'); - } else if($.fn.multifile.checkFileTypes(file, settings.allowedFileTypes)) { + } else if(!$.fn.multifile.checkFileTypes(file, settings.allowedFileTypes)) { + var msg = 'Selected file type is NOT allowed'; + if(file.count>1) + msg = 'File type of one or more of the selected files is NOT allowed'; + + alert('Error: '+msg); + $this.replaceWith(new_input); + } else if(!$.fn.multifile.checkFileSize(file, settings.max_file_size)) { + var msg = 'Selected file exceeds allowed size'; + if(file.count>1) + msg = 'File size of one or more of the selected files exceeds allowed size'; + + alert('Error: '+msg); + $this.replaceWith(new_input); + } else { $this.hide(); settings @@ -61,15 +77,6 @@ fObj.data('files', fObj.data('files')+file.count); if(fObj.data('files')<settings.max_uploads) $this.after(new_input); - - } else { - var msg = 'Selected file type is NOT allowed'; - if(file.count>1) - msg = 'File type of one or more of the selected files is NOT allowed'; - - alert('Error: '+msg); - - $this.replaceWith(new_input); } } @@ -126,6 +133,20 @@ if(filenames[i] && $.inArray('.'+filenames[i].split('.').pop(), allowedFileTypes) == -1) return false; + return true; + }; + + $.fn.multifile.checkFileSize = function(file, MaxFileSize) { + + //Size info not available or max file is not set (let server-side handle it). + if(!MaxFileSize || !file.size) + return true; + + var filesizes = $.map(file.size.split(','), $.trim); + for (var i = 0, _len = filesizes.length; i < _len; i++) + if(filesizes[i] > MaxFileSize) + return false; + return true; }; @@ -150,16 +171,20 @@ file.count = 1; // check for HTML5 FileList support if ( !!global.FileList ) { - if ( input.files.length == 1 ) + if ( input.files.length == 1 ) { file.name = input.files[0].name; - else { //Multi-select + file.size = '' + input.files[0].size; + } else { //Multi-select // We do this in order to support `multiple` files. // You can't display them separately because they // belong to only one file input. It is impossible // to remove just one of the files. file.name = input.files[0].name; - for (var i = 1, _len = input.files.length; i < _len; i++) + file.size = '' + input.files[0].size; + for (var i = 1, _len = input.files.length; i < _len; i++) { file.name += ', ' + input.files[i].name; + file.size += ', ' + input.files[i].size; + } file.count = i; } @@ -173,6 +198,7 @@ //Default options $.fn.multifile.defaults = { max_uploads: 1, - file_types: '.*' + file_types: '.*', + max_file_size: 0 }; })(jQuery, this); diff --git a/js/osticket.js b/js/osticket.js index ceca388f0d15339967933876f29f43688d5854cf..4a0e87b030c0f0ebc26cc21521cf0586acff88bb 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -64,6 +64,7 @@ $(document).ready(function(){ $('.multifile').multifile({ container: '.uploads', max_uploads: ($config && $config.max_file_uploads)?$config.max_file_uploads:1, - file_types: ($config && $config.file_types)?$config.file_types:".*" + file_types: ($config && $config.file_types)?$config.file_types:".*", + max_file_size: ($config && $config.max_file_size)?$config.max_file_size:0 }); }); diff --git a/main.inc.php b/main.inc.php index 4ec8bbe8a5f2c8f5a005b2c966c9bbec80c5e53f..c4b10edc0fa5af958a25126b77bc7409338e7b06 100644 --- a/main.inc.php +++ b/main.inc.php @@ -13,8 +13,8 @@ See LICENSE.TXT for details. vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ - +**********************************************************************/ + #Disable direct access. if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!'); @@ -44,9 +44,22 @@ if (defined('E_DEPRECATED')) # 5.3.0 $error_reporting &= ~(E_DEPRECATED | E_USER_DEPRECATED); error_reporting($error_reporting); //Respect whatever is set in php.ini (sysadmin knows better??) + #Don't display errors - ini_set('display_errors',1); - ini_set('display_startup_errors',1); + ini_set('display_errors', 0); + ini_set('display_startup_errors', 0); + + //Default timezone + if (!ini_get('date.timezone')) { + if(function_exists('date_default_timezone_set')) { + if(@date_default_timezone_get()) //Let PHP determine the timezone. + @date_default_timezone_set(@date_default_timezone_get()); + else //Default to EST - if PHP can't figure it out. + date_default_timezone_set('America/New_York'); + } else { //Default when all fails. PHP < 5. + ini_set('date.timezone', 'America/New_York'); + } + } #Set Dir constants if(!defined('ROOT_PATH')) define('ROOT_PATH','./'); //root path. Damn directories @@ -62,7 +75,7 @@ /*############## Do NOT monkey with anything else beyond this point UNLESS you really know what you are doing ##############*/ #Current version && schema signature (Changes from version to version) - define('THIS_VERSION','1.7-RC5'); //Shown on admin panel + define('THIS_VERSION','1.7-RC6'); //Shown on admin panel define('SCHEMA_SIGNATURE', 'd959a00e55c75e0c903b9e37324fd25d'); //MD5 signature of the db schema. (used to trigger upgrades) #load config info $configfile=''; @@ -70,7 +83,7 @@ $configfile=ROOT_DIR.'ostconfig.php'; elseif(file_exists(INCLUDE_DIR.'settings.php')) { //OLD config file.. v 1.6 RC5 $configfile=INCLUDE_DIR.'settings.php'; - //Die gracefully on upgraded v1.6 RC5 installation - otherwise script dies with confusing message. + //Die gracefully on upgraded v1.6 RC5 installation - otherwise script dies with confusing message. if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']), 'settings.php')) die('Please rename config file include/settings.php to include/ost-config.php to continue!'); } elseif(file_exists(INCLUDE_DIR.'ost-config.php')) //NEW config file v 1.6 stable ++ @@ -82,18 +95,18 @@ require($configfile); define('CONFIG_FILE',$configfile); //used in admin.php to check perm. - + //Path separator if(!defined('PATH_SEPARATOR')){ if(strpos($_ENV['OS'],'Win')!==false || !strcasecmp(substr(PHP_OS, 0, 3),'WIN')) define('PATH_SEPARATOR', ';' ); //Windows - else + else define('PATH_SEPARATOR',':'); //Linux } //Set include paths. Overwrite the default paths. ini_set('include_path', './'.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR); - + #include required files require(INCLUDE_DIR.'class.osticket.php'); @@ -121,7 +134,7 @@ #Session related define('SESSION_SECRET', MD5(SECRET_SALT)); //Not that useful anymore... define('SESSION_TTL', 86400); // Default 24 hours - + define('DEFAULT_MAX_FILE_UPLOADS',ini_get('max_file_uploads')?ini_get('max_file_uploads'):5); define('DEFAULT_PRIORITY_ID',1); @@ -157,24 +170,24 @@ define('TICKET_LOCK_TABLE',TABLE_PREFIX.'ticket_lock'); define('TICKET_EVENT_TABLE',TABLE_PREFIX.'ticket_event'); define('TICKET_EMAIL_INFO_TABLE',TABLE_PREFIX.'ticket_email_info'); - + define('EMAIL_TABLE',TABLE_PREFIX.'email'); define('EMAIL_TEMPLATE_TABLE',TABLE_PREFIX.'email_template'); define('FILTER_TABLE',TABLE_PREFIX.'filter'); define('FILTER_RULE_TABLE',TABLE_PREFIX.'filter_rule'); - + define('BANLIST_TABLE',TABLE_PREFIX.'email_banlist'); //Not in use anymore....as of v 1.7 define('SLA_TABLE',TABLE_PREFIX.'sla'); define('API_KEY_TABLE',TABLE_PREFIX.'api_key'); - define('TIMEZONE_TABLE',TABLE_PREFIX.'timezone'); + define('TIMEZONE_TABLE',TABLE_PREFIX.'timezone'); #Global overwrite if($_SERVER['HTTP_X_FORWARDED_FOR']) //Can contain multiple IPs - use the last one. $_SERVER['REMOTE_ADDR'] = array_pop(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])); - + #Connect to the DB && get configuration from database $ferror=null; if (!db_connect(DBHOST,DBUSER,DBPASS) || !db_select_database(DBNAME)) { @@ -191,7 +204,7 @@ die("<b>Fatal Error:</b> Contact system administrator."); exit; } - + //Init $session = $ost->getSession(); diff --git a/open.php b/open.php index 79c55a91713f62e6d0595368fa7f18de65489bf6..0ce680707f6930cc324d3c4952a7bdf550ff60e6 100644 --- a/open.php +++ b/open.php @@ -1,59 +1,59 @@ -<?php -/********************************************************************* - open.php - - New tickets handle. - - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com - - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. - - vim: expandtab sw=4 ts=4 sts=4: -**********************************************************************/ -require('client.inc.php'); -define('SOURCE','Web'); //Ticket source. -$inc='open.inc.php'; //default include. -$errors=array(); -if($_POST): - $_POST['deptId']=$_POST['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected. - if($thisclient) { - $_POST['name']=$thisclient->getName(); - $_POST['email']=$thisclient->getEmail(); - } elseif($cfg->isCaptchaEnabled()) { - if(!$_POST['captcha']) - $errors['captcha']='Enter text shown on the image'; - elseif(strcmp($_SESSION['captcha'],md5($_POST['captcha']))) - $errors['captcha']='Invalid - try again!'; - } - - //Ticket::create...checks for errors.. - if(($ticket=Ticket::create($_POST,$errors,SOURCE))){ - $msg='Support ticket request created'; - //Upload attachments... - if($cfg->allowOnlineAttachments() && $_FILES['attachments']) - $ticket->uploadFiles($_FILES['attachments'], $ticket->getLastMsgId(), 'M'); - - //Logged in...simply view the newly created ticket. - if($thisclient && $thisclient->isValid()) { - if(!$cfg->showRelatedTickets()) - $_SESSION['_client']['key']= $ticket->getExtId(); //Resetting login Key to the current ticket! - session_write_close(); - session_regenerate_id(); - @header('Location: tickets.php?id='.$ticket->getExtId()); - } - //Thank the user and promise speedy resolution! - $inc='thankyou.inc.php'; - }else{ - $errors['err']=$errors['err']?$errors['err']:'Unable to create a ticket. Please correct errors below and try again!'; - } -endif; - -//page -$nav->setActiveNav('new'); -require(CLIENTINC_DIR.'header.inc.php'); -require(CLIENTINC_DIR.$inc); -require(CLIENTINC_DIR.'footer.inc.php'); -?> +<?php +/********************************************************************* + open.php + + New tickets handle. + + Peter Rotich <peter@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require('client.inc.php'); +define('SOURCE','Web'); //Ticket source. +$inc='open.inc.php'; //default include. +$errors=array(); +if($_POST): + $vars = $_POST; + $vars['deptId']=$vars['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected. + if($thisclient) { + $vars['name']=$thisclient->getName(); + $vars['email']=$thisclient->getEmail(); + } elseif($cfg->isCaptchaEnabled()) { + if(!$_POST['captcha']) + $errors['captcha']='Enter text shown on the image'; + elseif(strcmp($_SESSION['captcha'],md5($_POST['captcha']))) + $errors['captcha']='Invalid - try again!'; + } + + if(!$errors && $cfg->allowOnlineAttachments() && $_FILES['attachments']) + $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); + + //Ticket::create...checks for errors.. + if(($ticket=Ticket::create($vars, $errors, SOURCE))){ + $msg='Support ticket request created'; + //Logged in...simply view the newly created ticket. + if($thisclient && $thisclient->isValid()) { + if(!$cfg->showRelatedTickets()) + $_SESSION['_client']['key']= $ticket->getExtId(); //Resetting login Key to the current ticket! + session_write_close(); + session_regenerate_id(); + @header('Location: tickets.php?id='.$ticket->getExtId()); + } + //Thank the user and promise speedy resolution! + $inc='thankyou.inc.php'; + }else{ + $errors['err']=$errors['err']?$errors['err']:'Unable to create a ticket. Please correct errors below and try again!'; + } +endif; + +//page +$nav->setActiveNav('new'); +require(CLIENTINC_DIR.'header.inc.php'); +require(CLIENTINC_DIR.$inc); +require(CLIENTINC_DIR.'footer.inc.php'); +?> diff --git a/scp/ajax.php b/scp/ajax.php index e4100f4e31c4f4028086e4608785df4fcdebba41..0f0771d3742efe983fe325fff52fe937f31e8dc5 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -27,7 +27,7 @@ require('staff.inc.php'); ini_set('display_errors','0'); //Disable error display ini_set('display_startup_errors','0'); -//TODO: disable direct access via the browser? i,e All request must have REFER? +//TODO: disable direct access via the browser? i,e All request must have REFER? if(!defined('INCLUDE_DIR')) Http::response(500, 'Server configuration error'); require_once INCLUDE_DIR.'/class.dispatcher.php'; @@ -65,5 +65,5 @@ $dispatcher = patterns('', ); # Call the respective function -print $dispatcher->resolve($_SERVER['PATH_INFO']); +print $dispatcher->resolve($ost->get_path_info()); ?> 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/js/scp.js b/scp/js/scp.js index cf32e42d8b638ff1b465cb23eb9a196e72be31ba..aa80487bddfcf77353bbc5b3307e92b38c1ec37b 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -259,7 +259,8 @@ $(document).ready(function(){ $('.multifile').multifile({ container: '.uploads', max_uploads: ($config && $config.max_file_uploads)?$config.max_file_uploads:1, - file_types: ($config && $config.file_types)?$config.file_types:".*" + file_types: ($config && $config.file_types)?$config.file_types:".*", + max_file_size: ($config && $config.max_file_size)?$config.max_file_size:0 }); /* Datepicker */ @@ -369,6 +370,7 @@ $(document).ready(function(){ break; case 'open': case 'overdue': + case 'answered': $('select#staffId').find('option:first').attr('selected', 'selected').parent('select'); $('select#staffId').attr('disabled','disabled'); $('select#assignee').removeAttr('disabled'); diff --git a/scp/js/upgrader.js b/scp/js/upgrader.js index 1631ac5cd5a7a156498f44e6f13020dae875dde2..2ac77d99f86c5db1b2a54d36f4afc709e2b9bad5 100644 --- a/scp/js/upgrader.js +++ b/scp/js/upgrader.js @@ -1,5 +1,5 @@ jQuery(function($) { - + $("#overlay").css({ opacity : 0.3, top : 0, @@ -12,18 +12,21 @@ jQuery(function($) { top : ($(window).height() / 3), left : ($(window).width() / 2 - 160) }); - + $('form#upgrade').submit(function(e) { - e.preventDefault(); var form = $(this); $('input[type=submit]', this).attr('disabled', 'disabled'); $('#overlay, #upgrading').show(); - doTasks('upgrade.php',form.serialize()); - - return false; - }); + if($('input#mode', form).val() == 'manual') { + return true; + } else { + e.preventDefault(); + autoUpgrade('upgrade.php',form.serialize()); + return false; + } + }); - function doTasks(url, data) { + function autoUpgrade(url, data) { function _lp(count) { $.ajax({ type: 'POST', @@ -33,26 +36,34 @@ jQuery(function($) { data: data, dataType: 'text', success: function(res) { - if (res) { - $('#loading #msg').html(res); - } + $('#main #task').html(res); + $('#upgrading #action').html(res); + $('#upgrading #msg').html('Still busy... smile #'+count); }, statusCode: { 200: function() { - setTimeout(function() { _lp(count+1); }, 2); + setTimeout(function() { _lp(count+1); }, 200); }, 201: function() { - $('#loading #msg').html("We're done... cleaning up!"); + $('#upgrading #msg').html("Cleaning up!..."); setTimeout(function() { location.href =url+'?c='+count+'&r='+Math.floor((Math.random()*100)+1); }, 3000); } }, - error: function() { - $('#loading #msg').html("Something went wrong"); - setTimeout(function() { location.href =url+'?c='+count+'&r='+Math.floor((Math.random()*100)+1); }, 1000); + error: function(jqXHR, textStatus, errorThrown) { + $('#upgrading #action').html('Error occurred. Aborting...'); + switch(jqXHR.status) { + case 404: + $('#upgrading #msg').html("Manual upgrade required (ajax failed)"); + setTimeout(function() { location.href =url+'?m=manual&c='+count+'&r='+Math.floor((Math.random()*100)+1); }, 2000); + break; + default: + $('#upgrading #msg').html("Something went wrong"); + setTimeout(function() { location.href =url+'?c='+count+'&r='+Math.floor((Math.random()*100)+1); }, 2000); + } } }); }; - _lp(0); + _lp(1); } }); diff --git a/scp/tickets.php b/scp/tickets.php index 9e436f200fe08f3348423d46f0118ba101fa0813..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; } @@ -450,7 +468,7 @@ $stats= $thisstaff->getTicketsStats(); //Navigation $nav->setTabActive('tickets'); if($cfg->showAnsweredTickets()) { - $nav->addSubMenu(array('desc'=>'Open ('.($stats['open']+$stats['answered']).')', + $nav->addSubMenu(array('desc'=>'Open ('.number_format($stats['open']+$stats['answered']).')', 'title'=>'Open Tickets', 'href'=>'tickets.php', 'iconclass'=>'Ticket'), @@ -458,7 +476,7 @@ if($cfg->showAnsweredTickets()) { } else { if($stats) { - $nav->addSubMenu(array('desc'=>'Open ('.$stats['open'].')', + $nav->addSubMenu(array('desc'=>'Open ('.number_format($stats['open']).')', 'title'=>'Open Tickets', 'href'=>'tickets.php', 'iconclass'=>'Ticket'), @@ -466,11 +484,11 @@ if($cfg->showAnsweredTickets()) { } if($stats['answered']) { - $nav->addSubMenu(array('desc'=>'Answered ('.$stats['answered'].')', + $nav->addSubMenu(array('desc'=>'Answered ('.number_format($stats['answered']).')', 'title'=>'Answered Tickets', 'href'=>'tickets.php?status=answered', 'iconclass'=>'answeredTickets'), - ($_REQUEST['status']=='answered')); + ($_REQUEST['status']=='answered')); } } @@ -478,7 +496,7 @@ if($stats['assigned']) { if(!$ost->getWarning() && $stats['assigned']>10) $ost->setWarning($stats['assigned'].' tickets assigned to you! Do something about it!'); - $nav->addSubMenu(array('desc'=>'My Tickets ('.$stats['assigned'].')', + $nav->addSubMenu(array('desc'=>'My Tickets ('.number_format($stats['assigned']).')', 'title'=>'Assigned Tickets', 'href'=>'tickets.php?status=assigned', 'iconclass'=>'assignedTickets'), @@ -486,7 +504,7 @@ if($stats['assigned']) { } if($stats['overdue']) { - $nav->addSubMenu(array('desc'=>'Overdue ('.$stats['overdue'].')', + $nav->addSubMenu(array('desc'=>'Overdue ('.number_format($stats['overdue']).')', 'title'=>'Stale Tickets', 'href'=>'tickets.php?status=overdue', 'iconclass'=>'overdueTickets'), @@ -497,14 +515,14 @@ if($stats['overdue']) { } if($thisstaff->showAssignedOnly() && $stats['closed']) { - $nav->addSubMenu(array('desc'=>'My Closed Tickets ('.$stats['closed'].')', + $nav->addSubMenu(array('desc'=>'My Closed Tickets ('.number_format($stats['closed']).')', 'title'=>'My Closed Tickets', 'href'=>'tickets.php?status=closed', 'iconclass'=>'closedTickets'), ($_REQUEST['status']=='closed')); } else { - $nav->addSubMenu(array('desc'=>'Closed Tickets', + $nav->addSubMenu(array('desc'=>'Closed Tickets ('.number_format($stats['closed']).')', 'title'=>'Closed Tickets', 'href'=>'tickets.php?status=closed', 'iconclass'=>'closedTickets'), @@ -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/setup/doc/api.md b/setup/doc/api.md index 7509616aa2406b842f56cacd7a6c0693b23415ca..f40d7423c68730d2a20a2df58b36a6a1b2104a17 100644 --- a/setup/doc/api.md +++ b/setup/doc/api.md @@ -27,3 +27,4 @@ Resources --------- - [Tickets](api/tickets.md) +- [Tasks](api/tasks.md) diff --git a/setup/doc/api/tasks.md b/setup/doc/api/tasks.md new file mode 100644 index 0000000000000000000000000000000000000000..b40bbab21290188145b4488d34c145b1d313ea8e --- /dev/null +++ b/setup/doc/api/tasks.md @@ -0,0 +1,9 @@ +Tasks +======= +The API supports tasks execution via the HTTP API. Cron is the only supported task as the moment. + +Execute Cron Job +--------------- + +Cron job can be executed, remotely, by making a post to `POST /api/tasks/cron`. See `scripts/rcron.php` + diff --git a/setup/doc/api/tickets.md b/setup/doc/api/tickets.md index fc79b1aead833a9f4cf9fd3a214635dcba9e52fc..a8bfd39f1b0791d9bb097484d0d09364e1ef1749 100644 --- a/setup/doc/api/tickets.md +++ b/setup/doc/api/tickets.md @@ -9,7 +9,7 @@ Create a Ticket --------------- Tickets can be created in the osTicket system by sending an HTTP POST to -`api/tickets.xml` or `api/tickets.json` depending on the format of the +`api/tickets.xml`, `api/tickets.email` or `api/tickets.json` depending on the format of the request content. ### Fields ###### @@ -32,7 +32,7 @@ request content. * __name__: *required* name of the file to be attached. Multiple files with the same name are allowable * __type__: Mime type of the file. Default is `text/plain` - * __encoding__: Set to `base64` if content is base64 encoded + * __encoding__: Set to `base64` if content is base64 encoded ### XML Payload Example ###### @@ -109,6 +109,59 @@ an object or array definition, and newlines are not allowed inside strings. [rfc 2397]: http://www.ietf.org/rfc/rfc2397.txt "Data URLs" +### Email Payload Example ###### + +* `POST /api/tickets.email` + +osTicket supports both remote (over http) and local piping. Please refer to the wiki on step-by-step instruction of setting up email piping. + +```email + +MIME-Version: 1.0 +Received: by 10.194.9.167 with HTTP; Thu, 7 Feb 2013 09:01:04 -0800 (PST) +Date: Thu, 7 Feb 2013 11:01:04 -0600 +Delivered-To: support@osticket.com +Message-ID: <CAL4KyrgKmpYxdX+6u3HyHZ3qN5K0mU2_sdfoVu6rT8cUNn+52w@osticket.com> +Subject: Testing +From: Peter Rotich <peter@osticket.com> +To: support@osticket.com +Content-Type: multipart/mixed; boundary=047d7bfcfaf263782204d52563a5 + +--047d7bfcfaf263782204d52563a5 +Content-Type: multipart/alternative; boundary=047d7bfcfaf263781204d52563a3 + +--047d7bfcfaf263781204d52563a3 +Content-Type: text/plain; charset=ISO-8859-1 + +Testing testing. + +-- +Peter Rotich +http://www.osticket.com + +--047d7bfcfaf263781204d52563a3 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr">Testing testing.<br clear=3D"all"><div><br></div>-- <br>Pe= +ter Rotich<br> +<a href=3D"http://www.osticket.com" target=3D"_blank">http://www.osticket.= +com</a> +</div> + +--047d7bfcfaf263781204d52563a3-- +--047d7bfcfaf263782204d52563a5 +Content-Type: text/plain; charset=US-ASCII; name="file.txt" +Content-Disposition: attachment; filename="file.txt" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_hcw5kqf60 + +Sm9lIGRhZGR5cyBjb250ZW50Cg== +--047d7bfcfaf263782204d52563a5-- +``` + +Local piping can utilize `api/pipe.php` without the neeed to setup an API key. + ### Response ###### If successful, the server will send `HTTP/201 Created`. Otherwise, it will diff --git a/setup/setup.inc.php b/setup/setup.inc.php index e0691939210ec57a797070d292f9099d1b6a321d..5339d9988cf81edb0bd5de920c2456ef583efcd9 100644 --- a/setup/setup.inc.php +++ b/setup/setup.inc.php @@ -15,7 +15,7 @@ **********************************************************************/ #This version - changed on every release -define('THIS_VERSION', '1.7-RC5'); +define('THIS_VERSION', '1.7-RC6'); #inits - error reporting. $error_reporting = E_ALL & ~E_NOTICE; @@ -24,7 +24,7 @@ if (defined('E_STRICT')) # 5.4.0 if (defined('E_DEPRECATED')) # 5.3.0 $error_reporting &= ~(E_DEPRECATED | E_USER_DEPRECATED); -error_reporting($error_reporting); +error_reporting($error_reporting); ini_set('magic_quotes_gpc', 0); ini_set('session.use_trans_sid', 0); ini_set('session.cache_limiter', 'nocache'); 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';