diff --git a/README.md b/README.md index 14893ac9f41cfb17429b42c86da86bbe33904248..63eae4967a6558339879c92e98743dd950d7f56b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ How osTicket works for you 1. Users create tickets via your website, email, or phone 1. Incoming tickets are saved and assigned to agents 1. Agents help your users resolve their issues - + osTicket is an attractive alternative to higher-cost and complex customer support systems; simple, lightweight, reliable, open source, web-based and easy to setup and use. The best part is, it's completely free. @@ -28,7 +28,7 @@ you) and cd into it. Then clone the repository (the folder must be empty!): osTicket uses the git flow development model, so you’ll need to switch to the develop branch in order to see the bleeding-edge feature additions. - git checkout develop + git checkout develop Follow the usual install instructions (beginning from Manual Installation above), except, don't delete the setup/ folder. For this reason, such an @@ -42,9 +42,9 @@ osTicket codebase before embarking on an upgrade. To trigger the update process, fetch the osTicket-1.7 tarball from either the osTicket [github](http://github.com/osTicket/osTicket-1.7) page or from -the osTicket website. Extract the tarball into the folder of you osTicket +the osTicket website. Extract the tarball into the folder of your osTicket codebase. This can also be accomplished with the zip file, and a FTP client -can of course be used to upload the new source code to your server. +can of course be used to upload the new source code to your server. Any way you choose your adventure, when you have your codebase upgraded to osTicket-1.7, visit the /scp page of you ticketing system. The upgrader will @@ -57,7 +57,7 @@ Help ---- Visit the [wiki](http://osticket.com/wiki/Home) or the [forum](http://osticket.com/forums/). And if you'd like professional help -managing your osTicket installation, +managing your osTicket installation, [commercial support](http://osticket.com/support/) is available. Contributing @@ -85,4 +85,4 @@ osTicket is supported by several magical open source projects including: * [PEAR/Net_SMTP](http://pear.php.net/package/Net_SMTP) * [PEAR/Net_Socket](http://pear.php.net/package/Net_Socket) * [PEAR/Serivces_JSON](http://pear.php.net/package/Services_JSON) - * [phplint](http://antirez.com/page/phplint.html) + * [phplint](http://antirez.com/page/phplint.html) diff --git a/WHATSNEW.md b/WHATSNEW.md index 460b4020f95e758483da45e8cf37bd669b4cb15d..752e61fc94d82555e76d0ad61a7d3ea927871bca 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,3 +1,6 @@ +New stuff in 1.7.0 +==================== + * Bug fixes from rc6 New stuff in 1.7-rc6 ==================== @@ -67,9 +70,9 @@ New Features in 1.7 =================== Version 1.7 includes several new features -Email Filters +Ticket Filters ------------- -As an upgrade from email banning (which is still supported), email filters +As an upgrade from email banning (which is still supported), ticket filters allow for matching incoming email in the subject line and message body. For matching emails, the administrator has the ability to automatically route tickets: @@ -79,6 +82,12 @@ tickets: * Disable ticket auto-responses * Send automatic canned responses +Tickets filters are also applied to tickets submitted via all ticket +interfaces, including the API, email, staff and client web interfaces. And, +as a bonus, the filters can be configured to target only a single interface. +So an administrator could, for instance, target tickets received via email +from a particular domain. + Canned Attachments ------------------ Attach files to your canned responses. These attachments are automatically @@ -86,6 +95,13 @@ attached to the ticket thread along with the canned response. The attachments are not duplicated in the database and therefore use virtually no space. +Database-backed Attachments +--------------------------- +No more crazy security-related configuration to your host server in order to +support attachments. Attachments are now quietly stored in the database. The +upgrade migration will automatically port attachments from the previous +locations into the database. + Service Level Agreements ------------------------ Service level agreements allow for a configurable grace period based on the @@ -99,13 +115,15 @@ Manage a searchable help document portal for your users Dashboard Reports ----------------- -Flashy reports of ticket system activiy as well as exportable ticket system +Flashy reports of ticket system activity as well as exportable ticket system statistics, allowing for easy report generation from office spreadsheet applications. Ticket Export ------------- -Convert the ticket thread to a printed format for long term storage. +Convert the ticket thread to a printed format for long term storage. The +ticket view page now supports a print feature, which will render the ticket +as a PDF document. API --- @@ -114,3 +132,28 @@ tickets are createable by submitting an HTTP POST request to either /api/tickets.xml /api/tickets.json + +The API can also be used to pipe emails into the osTicket system. Use the +included `automail.php` or `automail.pl` script to pipe emails to the +system, or post raw email messages directly to + + /api/tickets.email + +Use of the API requires an API key, which can be created and configured in +the admin panel of the support system. + +For technical details, please refer to [API Docs] (setup/doc/api.md). + +Geeky New Features +================== + +Unicode +------- +Better and more consistent international text handling + +Flexible Template Variables +--------------------------- +Template variables have been redesigned to be more flexible. They have been +integrated into the respective object classes so that an object as well as +its properties can be represented in template variables. For instance +%{ticket.staff.name} diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 1d887f04349241327a324d43b8a2d31545c45cf1..34c46325ed84d304c7a8c031bb8a5b686ea337a1 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -19,7 +19,7 @@ if(!defined('INCLUDE_DIR')) die('403'); include_once(INCLUDE_DIR.'class.ticket.php'); class TicketsAjaxAPI extends AjaxController { - + function lookup() { global $thisstaff; @@ -33,12 +33,12 @@ class TicketsAjaxAPI extends AjaxController { $sql='SELECT DISTINCT ticketID, email' .' FROM '.TICKET_TABLE .' WHERE ticketID LIKE \''.db_input($_REQUEST['q'], false).'%\''; - + $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); - + if(($teams=$thisstaff->getTeams()) && count(array_filter($teams))) $sql.=' OR team_id IN('.implode(',', db_input(array_filter($teams))).')'; - + if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; @@ -63,7 +63,7 @@ class TicketsAjaxAPI extends AjaxController { $sql='SELECT email, count(ticket_id) as tickets ' .' FROM '.TICKET_TABLE .' WHERE email LIKE \'%'.db_input(strtolower($_REQUEST['q']), false).'%\' '; - + $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); if(($teams=$thisstaff->getTeams()) && count(array_filter($teams))) @@ -71,11 +71,11 @@ class TicketsAjaxAPI extends AjaxController { if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; - + $sql.=' ) ' .' GROUP BY email ' .' ORDER BY created LIMIT '.$limit; - + if(($res=db_query($sql)) && db_num_rows($res)) { while(list($email, $count)=db_fetch_row($res)) $tickets[] = array('email'=>$email, 'value'=>$email, 'info'=>"$email ($count)"); @@ -86,7 +86,7 @@ class TicketsAjaxAPI extends AjaxController { function search() { global $thisstaff, $cfg; - + $result=array(); $select = 'SELECT count( DISTINCT ticket.ticket_id) as tickets '; $from = ' FROM '.TICKET_TABLE.' ticket '; @@ -127,7 +127,7 @@ class TicketsAjaxAPI extends AjaxController { break; } - //Assignee + //Assignee if(isset($_REQUEST['assignee']) && strcasecmp($_REQUEST['status'], 'closed')) { $id=preg_replace("/[^0-9]/", "", $_REQUEST['assignee']); $assignee = $_REQUEST['assignee']; @@ -147,16 +147,16 @@ class TicketsAjaxAPI extends AjaxController { $where.= ' OR ticket.status="closed" '; $where.= ' ) '; - } elseif($_REQUEST['staffId']) { + } elseif($_REQUEST['staffId']) { $where.=' AND (ticket.staff_id='.db_input($_REQUEST['staffId']).' AND ticket.status="closed") '; } - + //dates $startTime =($_REQUEST['startDate'] && (strlen($_REQUEST['startDate'])>=8))?strtotime($_REQUEST['startDate']):0; $endTime =($_REQUEST['endDate'] && (strlen($_REQUEST['endDate'])>=8))?strtotime($_REQUEST['endDate']):0; if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)) $startTime=$endTime=0; - + if($startTime) $where.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')'; @@ -166,17 +166,17 @@ class TicketsAjaxAPI extends AjaxController { //Query if($_REQUEST['query']) { $queryterm=db_real_escape($_REQUEST['query'], false); - + $from.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )'; $where.=" AND ( ticket.email LIKE '%$queryterm%'" ." OR ticket.name LIKE '%$queryterm%'" ." OR ticket.subject LIKE '%$queryterm%'" ." OR thread.title LIKE '%$queryterm%'" - ." OR thread.body LIKE '%$queryterm%'" + ." OR thread.body LIKE '%$queryterm%'" .' )'; } - - $sql="$select $from $where $groupby"; + + $sql="$select $from $where"; if(($tickets=db_result(db_query($sql)))) { $result['success'] =sprintf("Search criteria matched %s - <a href='tickets.php?%s'>view</a>", ($tickets>1?"$tickets tickets":"$tickets ticket"), @@ -184,26 +184,26 @@ class TicketsAjaxAPI extends AjaxController { } else { $result['fail']='No tickets found matching your search criteria.'; } - + return $this->json_encode($result); } function acquireLock($tid) { global $cfg,$thisstaff; - if(!$tid or !is_numeric($tid) or !$thisstaff or !$cfg or !$cfg->getLockTime()) + if(!$tid or !is_numeric($tid) or !$thisstaff or !$cfg or !$cfg->getLockTime()) return 0; - + if(!($ticket = Ticket::lookup($tid)) || !$ticket->checkStaffAccess($thisstaff)) return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>'Lock denied!')); - + //is the ticket already locked? if($ticket->isLocked() && ($lock=$ticket->getLock()) && !$lock->isExpired()) { /*Note: Ticket->acquireLock does the same logic...but we need it here since we need to know who owns the lock up front*/ //Ticket is locked by someone else.?? if($lock->getStaffId()!=$thisstaff->getId()) return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>'Unable to acquire lock.')); - + //Ticket already locked by staff...try renewing it. $lock->renew(); //New clock baby! } elseif(!($lock=$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime()))) { @@ -220,17 +220,17 @@ class TicketsAjaxAPI extends AjaxController { if(!$id or !is_numeric($id) or !$thisstaff) return $this->json_encode(array('id'=>0, 'retry'=>true)); - + $lock= TicketLock::lookup($id); if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired return self::acquireLock($tid); //acquire the lock - + if($lock->getStaffId()!=$thisstaff->getId()) //user doesn't own the lock anymore??? sorry...try to next time. return $this->json_encode(array('id'=>0, 'retry'=>false)); //Give up... - + //Renew the lock. $lock->renew(); //Failure here is not an issue since the lock is not expired yet.. client need to check time! - + return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime())); } @@ -238,12 +238,12 @@ class TicketsAjaxAPI extends AjaxController { global $thisstaff; if($id && is_numeric($id)){ //Lock Id provided! - + $lock = TicketLock::lookup($id, $tid); //Already gone? if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired return 1; - + //make sure the user actually owns the lock before releasing it. return ($lock->getStaffId()==$thisstaff->getId() && $lock->release())?1:0; @@ -269,7 +269,7 @@ class TicketsAjaxAPI extends AjaxController { $warn.=' <span class="Icon lockedTicket">Ticket is locked by '.$lock->getStaffName().'</span>'; elseif($ticket->isOverdue()) $warn.=' <span class="Icon overdueTicket">Marked overdue!</span>'; - + ob_start(); echo sprintf( '<div style="width:500px; padding: 2px 2px 0 5px;"> @@ -353,7 +353,7 @@ class TicketsAjaxAPI extends AjaxController { $options[]=array('action'=>'Thread ('.$ticket->getThreadCount().')','url'=>"tickets.php?id=$tid"); if($ticket->getNumNotes()) $options[]=array('action'=>'Notes ('.$ticket->getNumNotes().')','url'=>"tickets.php?id=$tid#notes"); - + if($ticket->isOpen()) $options[]=array('action'=>'Reply','url'=>"tickets.php?id=$tid#reply"); diff --git a/include/class.api.php b/include/class.api.php index dd15234676d1c3783f7bbb5af357b77feb945c56..cbaf2e8116ade833a15084bb0639eedd3d577525 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -32,10 +32,10 @@ class API { $sql='SELECT * FROM '.API_KEY_TABLE.' WHERE id='.db_input($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; } @@ -54,7 +54,7 @@ class API { function getIPAddr() { return $this->ht['ipaddr']; } - + function getNotes() { return $this->ht['notes']; } @@ -79,7 +79,7 @@ class API { if(!API::save($this->getId(), $vars, $errors)) return false; - + $this->reload(); return true; @@ -104,7 +104,7 @@ class API { $sql='SELECT id FROM '.API_KEY_TABLE.' WHERE apikey='.db_input($key); if($ip) $sql.=' AND ipaddr='.db_input($ip); - + if(($res=db_query($sql)) && db_num_rows($res)) list($id) = db_fetch_row($res); @@ -123,7 +123,7 @@ class API { if(!$id && (!$vars['ipaddr'] || !Validator::is_ip($vars['ipaddr']))) $errors['ipaddr'] = 'Valid IP required'; - + if($errors) return false; $sql=' updated=NOW() ' @@ -219,7 +219,7 @@ class ApiController { if (!($data = $parser->parse($stream))) $this->exerr(400, $parser->lastError()); - + //Validate structure of the request. $this->validate($data, $format); @@ -242,10 +242,10 @@ class ApiController { * API will further validate the contents of the request */ 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 : "*"; + $search = (isset($structure[$key]) && !is_numeric($key)) ? $key : "*"; if (isset($structure[$search])) { $this->validateRequestStructure($info, $structure[$search], "$prefix$key/"); continue; @@ -265,7 +265,7 @@ class ApiController { */ function validate(&$data, $format) { return $this->validateRequestStructure( - $data, + $data, $this->getRequestStructure($format) ); } @@ -329,8 +329,8 @@ class ApiXmlDataParser extends XmlDataParser { $value = $value['file']; if($value && is_array($value)) { - foreach ($value as &$info) { - $info["data"] = $info[":text"]; + foreach ($value as &$info) { + $info["data"] = $info[":text"]; unset($info[":text"]); } unset($info); @@ -355,7 +355,7 @@ class ApiJsonDataParser extends JsonDataParser { foreach ($current as $key=>&$value) { if ($key == "phone") { list($value, $current["phone_ext"]) - = explode("X", strtoupper($value), 2); + = explode("X", strtoupper($value), 2); } else if ($key == "alert") { $value = (bool)$value; } else if ($key == "autorespond") { @@ -386,11 +386,11 @@ class ApiJsonDataParser extends JsonDataParser { # for the database. Otherwise, assume utf-8 list($param,$charset) = explode('=', $extra); if ($param == 'charset' && $charset) - $contents = Formart::utf8encode($contents, $charset); + $contents = Format::utf8encode($contents, $charset); } } unset($value); - } + } if (is_array($value)) { $value = $this->fixup($value); } diff --git a/include/class.format.php b/include/class.format.php index 7f6cc957b152d2e1b28a033ad481b3166bf7a667..7a339e429212a1d1b7919dc14b7bd6bb0bd0e24c 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -2,7 +2,7 @@ /********************************************************************* class.format.php - Collection of helper function used for formatting + Collection of helper function used for formatting Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket @@ -19,11 +19,11 @@ class Format { function file_size($bytes) { - + if(!is_numeric($bytes)) return $bytes; if($bytes<1024) - return $bytes.' bytes'; + return $bytes.' bytes'; if($bytes <102400) return round(($bytes/1024),1).' kb'; @@ -38,19 +38,21 @@ class Format { function encode($text, $charset=null, $encoding='utf-8') { //Try auto-detecting charset/encoding - if(!$charset && function_exists('mb_detect_encoding')) + if(!$charset && function_exists('mb_detect_encoding')) $charset = mb_detect_encoding($text); - //Cleanup - junk + //Cleanup - junk if($charset && in_array(trim($charset), array('default','x-user-defined'))) - $charset = 'ISO-8859-1'; + $charset = 'ISO-8859-1'; if(function_exists('iconv') && $charset) - return iconv($charset, $encoding.'//IGNORE', $text); - elseif(function_exists('iconv_mime_decode')) - return iconv_mime_decode($text, 0, $encoding); - else //default to utf8 encoding. - return utf8_encode($text); + $text = iconv($charset, $encoding.'//IGNORE', $text); + elseif(function_exists('mb_convert_encoding') && $charset && $encoding) + $text = mb_convert_encoding($text, $encoding, $charset); + elseif(!strcasecmp($encoding, 'utf-8')) //forced blind utf8 encoding. + $text = function_exists('imap_utf8')?imap_utf8($text):utf8_encode($text); + + return $text; } //Wrapper for utf-8 encoding. @@ -58,6 +60,24 @@ class Format { return Format::encode($text, $charset, 'utf-8'); } + function mimedecode($text, $encoding='UTF-8') { + + if(function_exists('imap_mime_header_decode') + && ($parts = imap_mime_header_decode($text))) { + $str =''; + foreach ($parts as $part) + $str.= Format::encode($part->text, $part->charset, $encoding); + + $text = $str; + } elseif(function_exists('iconv_mime_decode')) { + $text = iconv_mime_decode($text, 0, $encoding); + } elseif(!strcasecmp($encoding, 'utf-8') && function_exists('imap_utf8')) { + $text = imap_utf8($text); + } + + return $text; + } + function phone($phone) { $stripped= preg_replace("/[^0-9]/", "", $phone); @@ -70,10 +90,10 @@ class Format { } function truncate($string,$len,$hard=false) { - + if(!$len || $len>strlen($string)) return $string; - + $string = substr($string,0,$len); return $hard?$string:(substr($string,0,strrpos($string,' ')).' ...'); @@ -93,11 +113,17 @@ class Format { } function safe_html($html) { - return Format::html($html,array('safe'=>1,'balance'=>1)); + $config = array( + 'safe' => 1, //Exclude applet, embed, iframe, object and script tags. + 'balance' => 1, //balance and close unclosed tags. + 'comment' => 1 //Remove html comments (OUTLOOK LOVE THEM) + ); + + return Format::html($html, $config); } function sanitize($text, $striptags= true) { - + //balance and neutralize unsafe tags. $text = Format::safe_html($text); @@ -127,7 +153,7 @@ class Format { $flags = ENT_COMPAT; if (phpversion() >= '5.4.0') $flags |= ENT_HTML401; - + return html_entity_decode($var, $flags, 'UTF-8'); } @@ -161,12 +187,12 @@ class Format { return strip_tags($decode?Format::htmldecode($var):$var); } - //make urls clickable. Mainly for display + //make urls clickable. Mainly for display function clickableurls($text) { global $ost; - + $token = $ost->getLinkToken(); - //Not perfect but it works - please help improve it. + //Not perfect but it works - please help improve it. $text=preg_replace_callback('/(((f|ht){1}tp(s?):\/\/)[-a-zA-Z0-9@:%_\+.~#?&;\/\/=]+)/', create_function('$matches', sprintf('return "<a href=\"l.php?url=".urlencode($matches[1])."&auth=%s\" target=\"_blank\">".$matches[1]."</a>";', @@ -191,7 +217,7 @@ class Format { return preg_replace("/\n{3,}/", "\n\n", $string); } - + function linebreaks($string) { return urldecode(ereg_replace("%0D", " ", urlencode($string))); } @@ -208,17 +234,17 @@ class Format { * @return string The imploded array */ function array_implode( $glue, $separator, $array ) { - + if ( !is_array( $array ) ) return $array; $string = array(); foreach ( $array as $key => $val ) { if ( is_array( $val ) ) $val = implode( ',', $val ); - + $string[] = "{$key}{$glue}{$val}"; } - + return implode( $separator, $string ); } @@ -236,7 +262,7 @@ class Format { return $tstring; } - + /* Dates helpers...most of this crap will change once we move to PHP 5*/ function db_date($time) { global $cfg; @@ -247,7 +273,7 @@ class Format { global $cfg; return Format::userdate($cfg->getDateTimeFormat(), Misc::db2gmtime($time)); } - + function db_daydatetime($time) { global $cfg; return Format::userdate($cfg->getDayDateTimeFormat(), Misc::db2gmtime($time)); @@ -256,16 +282,16 @@ class Format { function userdate($format, $gmtime) { return Format::date($format, $gmtime, $_SESSION['TZ_OFFSET'], $_SESSION['TZ_DST']); } - + function date($format, $gmtimestamp, $offset=0, $daylight=false){ - + if(!$gmtimestamp || !is_numeric($gmtimestamp)) - return ""; - + return ""; + $offset+=$daylight?date('I', $gmtimestamp):0; //Daylight savings crap. - + return date($format, ($gmtimestamp+ ($offset*3600))); } - + } ?> diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 1bcf6d690cdbc244c233ae65b90ad334ed79185b..d1032ba506971dac36d97ba7ffeba41111213626 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -372,16 +372,20 @@ class MailFetcher { $emailId = $this->getEmailId(); $vars = array(); - $vars['name']=$this->mime_decode($mailinfo['name']); $vars['email']=$mailinfo['email']; + $vars['name']=$this->mime_decode($mailinfo['name']); $vars['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; $vars['message']=Format::stripEmptyLines($this->getBody($mid)); $vars['header']=$this->getHeader($mid); $vars['emailId']=$emailId?$emailId:$ost->getConfig()->getDefaultEmailId(); //ok to default? - $vars['name']=$vars['name']?$vars['name']:$vars['email']; //No name? use email $vars['mid']=$mailinfo['mid']; - if(!$vars['message']) //An email with just attachments can have empty body. + //Missing FROM name - use email address. + if(!$vars['name']) + $vars['name'] = $vars['email']; + + //An email with just attachments can have empty body. + if(!$vars['message']) $vars['message'] = '(EMPTY)'; if($ost->getConfig()->useEmailPriority()) diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 3e412675a3ada7ff0d319b15f2e8a89f4b9cade9..3347451de99a1aa3f1b116b2098de60b2c3e0864 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -27,18 +27,29 @@ class Mail_Parse { var $struct; - function Mail_parse($mimeMessage,$includeBodies=true,$decodeHeaders=TRUE,$decodeBodies=TRUE){ + var $charset ='UTF-8'; //Default charset. - $this->mime_message=$mimeMessage; - $this->include_bodies=$includeBodies; - $this->decode_headers=$decodeHeaders; - $this->decode_bodies=$decodeBodies; + function Mail_parse($mimeMessage, $charset=null){ + + $this->mime_message = $mimeMessage; + + if($charset) + $this->charset = $charset; + + $this->include_bodies = true; + $this->decode_headers = true; + $this->decode_bodies = true; + + //Desired charset + if($charset) + $this->charset = $charset; } function decode() { $params = array('crlf' => "\r\n", - 'input' =>$this->mime_message, + 'charset' => $this->charset, + 'input' => $this->mime_message, 'include_bodies'=> $this->include_bodies, 'decode_headers'=> $this->decode_headers, 'decode_bodies' => $this->decode_bodies); @@ -146,12 +157,18 @@ class Mail_Parse { return $body; } - function getPart($struct,$ctypepart) { + function getPart($struct, $ctypepart) { if($struct && !$struct->parts) { $ctype = @strtolower($struct->ctype_primary.'/'.$struct->ctype_secondary); - if($ctype && strcasecmp($ctype,$ctypepart)==0) - return $struct->body; + if($ctype && strcasecmp($ctype,$ctypepart)==0) { + $content = $struct->body; + //Encode to desired encoding - ONLY if charset is known?? + if(isset($struct->ctype_parameters['charset']) && strcasecmp($struct->ctype_parameters['charset'], $this->charset)) + $content = Format::encode($content, $struct->ctype_parameters['charset'], $this->charset); + + return $content; + } } $data=''; @@ -272,11 +289,15 @@ class EmailDataParser { break; } + $data['email'] = $from->mailbox.'@'.$from->host; + $data['name'] = trim($from->personal,'"'); if($from->comment && $from->comment[0]) $data['name'].= ' ('.$from->comment[0].')'; - $data['email'] = $from->mailbox.'@'.$from->host; + //Use email address as name when FROM address doesn't have a name. + if(!$data['name'] && $data['email']) + $data['name'] = $data['email']; } //TO Address:Try to figure out the email address... associated with the incoming email. @@ -295,8 +316,8 @@ class EmailDataParser { } } - $data['subject'] = Format::utf8encode($parser->getSubject()); - $data['message'] = Format::utf8encode(Format::stripEmptyLines($parser->getBody())); + $data['subject'] = $parser->getSubject(); + $data['message'] = Format::stripEmptyLines($parser->getBody()); $data['header'] = $parser->getHeader(); $data['mid'] = $parser->getMessageId(); $data['priorityId'] = $parser->getPriority(); diff --git a/include/class.thread.php b/include/class.thread.php index 705ba5ec5c623b40844d648b5cef6ccbfc945e5b..f45c0e518bd179d3a41b5000ada56b7b5ccdc1b5 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -542,8 +542,8 @@ Class ThreadEntry { $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'])) + .' ,title='.db_input(Format::sanitize($vars['title'], true)) + .' ,body='.db_input(Format::sanitize($vars['body'], true)) .' ,staff_id='.db_input($vars['staffId']) .' ,poster='.db_input($vars['poster']) .' ,source='.db_input($vars['source']); diff --git a/include/class.ticket.php b/include/class.ticket.php index 17d2c56b3545733d3569261eacb1d8bd79328070..21a4bd8f1777e100d839dedaf44525c61455f381 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -969,7 +969,7 @@ class Ticket { //recipients $recipients=array(); //Assigned staff or team... if any - if($this->isAssigned() && $cfg->alertAssignedONTransfer()) { + if($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) { if($this->getStaffId()) $recipients[]=$this->getStaff(); elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers())) @@ -1102,11 +1102,11 @@ class Ticket { $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()) + if($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime()) $sql.=', duedate=NULL'; //Clear SLA if est. due date is in the past - if($this->getSLADueDate() && strtotime($this->getSLADueDate())<=time()) + if($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime()) $sql.=', sla_id=0 '; $sql.=' WHERE ticket_id='.db_input($this->getId()); @@ -1651,6 +1651,14 @@ class Ticket { $this->logNote('Ticket Updated', $vars['note'], $thisstaff); $this->reload(); + //Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue. + if($this->isOverdue() + && (!$this->getEstDueDate() //Duedate + SLA cleared + || Misc::db2gmtime($this->getEstDueDate()) > Misc::gmtime() //New due date in the future. + )) { + $this->clearOverdue(); + } + return true; } @@ -1756,8 +1764,7 @@ class Ticket { 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()).')' + AND closed.status=\'closed\' )' .' WHERE (ticket.staff_id='.db_input($staff->getId()); if(($teams=$staff->getTeams())) diff --git a/include/pear/Mail/mimeDecode.php b/include/pear/Mail/mimeDecode.php index 59b6e1923c32d5ea5a3816e5c14821b1913fd3bf..b300195824c30528f7ff44987679fd11875889e3 100644 --- a/include/pear/Mail/mimeDecode.php +++ b/include/pear/Mail/mimeDecode.php @@ -28,8 +28,8 @@ * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * - Neither the name of the authors, nor the names of its contributors - * may be used to endorse or promote products derived from this + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" @@ -174,9 +174,9 @@ class Mail_mimeDecode extends PEAR function getHeader() { return $this->_header; } - - + + /** * Begins the decoding process. If called statically @@ -219,6 +219,8 @@ class Mail_mimeDecode extends PEAR $params['decode_bodies'] : false; $this->_decode_headers = isset($params['decode_headers']) ? $params['decode_headers'] : false; + $this->_charset = isset($params['charset']) ? + $params['charset'] : 'UTF-8'; $structure = $this->_decode($this->_header, $this->_body); if ($structure === false) { @@ -374,7 +376,7 @@ class Mail_mimeDecode extends PEAR } for ($i = 0; $i < count($structure->parts); $i++) { - + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { $prepend = $prepend . $mime_number . '.'; $_mime_number = ''; @@ -394,7 +396,7 @@ class Mail_mimeDecode extends PEAR $structure->mime_id = $prepend . $mime_number; $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; } - + return $return; } @@ -567,6 +569,16 @@ class Mail_mimeDecode extends PEAR break; } + //Convert decoded text to the desired charset. + if($charset && $this->_charset && strcasecmp($this->_charset, $charset)) { + if(function_exists('iconv')) + $text = iconv($charset, $this->_charset.'//IGNORE', $text); + elseif(function_exists('mb_convert_encoding')) + $text = mb_convert_encoding($text, $this->_charset, $charset); + elseif(!strcasecmp($this->_charset, 'utf-8')) //forced blind utf8 encoding. + $text = function_exists('imap_utf8')?imap_utf8($text):utf8_encode($text); + } + $input = str_replace($encoded, $text, $input); } @@ -698,7 +710,7 @@ class Mail_mimeDecode extends PEAR /** * getSendArray() returns the arguments required for Mail::send() - * used to build the arguments for a mail::send() call + * used to build the arguments for a mail::send() call * * Usage: * $mailtext = Full email (for example generated by a template) @@ -741,7 +753,7 @@ class Mail_mimeDecode extends PEAR } $to = substr($to,1); return array($to,$header,$this->_body); - } + } /** * Returns a xml copy of the output of diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php index 3743d5c9e062f112489d229c8f9e3cfb71a68adc..ef1081422c941d1662dbbc3cfe83541a4858a45b 100644 --- a/include/staff/filter.inc.php +++ b/include/staff/filter.inc.php @@ -27,7 +27,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="hidden" name="do" value="<?php echo $action; ?>"> <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>"> <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> - <h2>Incoming Email Filter</h2> + <h2>Ticket Filter</h2> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> @@ -113,7 +113,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="radio" name="match_all_rules" value="0" <?php echo !$info['match_all_rules']?'checked="checked"':''; ?>>Match Any <span class="error">* </span> <em>(case-insensitive comparison)</em> - + </td> </tr> <?php @@ -143,7 +143,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="text" size="30" name="rule_v<?php echo $i; ?>" value="<?php echo $info["rule_v$i"]; ?>"> <span class="error"> <?php echo $errors["rule_$i"]; ?></span> </div> - <?php + <?php if($info["rule_w$i"] || $info["rule_h$i"] || $info["rule_v$i"]){ ?> <div style="float:right;text-align:right;padding-right:20px;"><a href="#" class="clearrule">(clear)</a></div> <?php diff --git a/main.inc.php b/main.inc.php index c4b10edc0fa5af958a25126b77bc7409338e7b06..c6a6d0fe7240e316e0ad4c704c3a774dc8000208 100644 --- a/main.inc.php +++ b/main.inc.php @@ -75,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-RC6'); //Shown on admin panel + define('THIS_VERSION','1.7.0'); //Shown on admin panel define('SCHEMA_SIGNATURE', 'd959a00e55c75e0c903b9e37324fd25d'); //MD5 signature of the db schema. (used to trigger upgrades) #load config info $configfile=''; diff --git a/setup/cli/modules/unpack.php b/setup/cli/modules/unpack.php index 0f6aed8ab9cc6ac862e09389393c57f3e62a7fb8..58fc70e4f781b6199829b35ba39c2a5dfa3e6d9b 100644 --- a/setup/cli/modules/unpack.php +++ b/setup/cli/modules/unpack.php @@ -3,12 +3,12 @@ require_once dirname(__file__) . "/class.module.php"; class Unpacker extends Module { - + var $prologue = "Unpacks osTicket into target install path"; var $epilog = "Copies an unpacked osticket tarball or zipfile into a production - location, optionally placing the include/ folder in a separate + location, optionally placing the include/ folder in a separate location if requested"; var $options = array( @@ -118,7 +118,7 @@ class Unpacker extends Module { $this->destination = $this->getArgument('install-path'); if (!is_dir($this->destination)) if (!mkdir($this->destination, 0751, true)) - $this->die("Destination path does not exist and cannot be created"); + die("Destination path does not exist and cannot be created"); # Determine if this is an upgrade, and if so, where the include/ # folder is currently located diff --git a/setup/cli/package.php b/setup/cli/package.php index 9b5ecae51e212cc570b220c36b75935f75b79b93..67d20a32b5d8bbcc7f232c2f7392430a232e99da 100755 --- a/setup/cli/package.php +++ b/setup/cli/package.php @@ -21,7 +21,7 @@ function get_osticket_root_path() { function glob_recursive($pattern, $flags = 0) { $files = glob($pattern, $flags); foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { - $files = array_merge($files, + $files = array_merge($files, glob_recursive($dir.'/'.basename($pattern), $flags)); } return $files; @@ -89,22 +89,23 @@ package("*.php", 'upload/'); foreach (array('assets','css','images','js') as $dir) package("$dir/*", "upload/$dir", -1, "*less"); +# Load API +package('api/{,.}*', 'upload/api'); + # Load the knowledgebase package("kb/*.php", "upload/kb"); # Load the staff interface package("scp/*.php", "upload/scp/", -1); foreach (array('css','images','js') as $dir) - package("$dir/*", "upload/scp/$dir", -1); + package("scp/$dir/*", "upload/scp/$dir", -1); # Load in the scripts mkdir("$stage_path/scripts/"); package("setup/scripts/*", "scripts/", -1, "*stage"); # Load the heart of the system -package("include/*.php", "upload/include", -1); -# And the sql patches -package("include/upgrader/*.sql", "upload/include/upgrader", -1); +package("include/{,.}*", "upload/include", -1, array('*ost-config.php', '*.sw[a-z]')); # Include the installer package("setup/*.{php,txt}", "upload/setup", -1, array("*scripts","*test","*stage")); @@ -130,8 +131,8 @@ foreach ($version_info as $line) $pwd = getcwd(); chdir($stage_path); -shell_exec("tar cjf '$pwd/osticket-".THIS_VERSION.".tar.bz2' *"); -shell_exec("zip -r '$pwd/osticket-".THIS_VERSION.".zip' *"); +shell_exec("tar cjf '$pwd/osTicket-".THIS_VERSION.".tar.bz2' *"); +shell_exec("zip -r '$pwd/osTicket-".THIS_VERSION.".zip' *"); chdir($pwd); ?> diff --git a/setup/setup.inc.php b/setup/setup.inc.php index 5339d9988cf81edb0bd5de920c2456ef583efcd9..52b7563a51f9b545c5d58da094da4fd65a09a8ef 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-RC6'); +define('THIS_VERSION', '1.7.0'); #inits - error reporting. $error_reporting = E_ALL & ~E_NOTICE;