diff --git a/bootstrap.php b/bootstrap.php index ef080d86f275ce07d4cad1dababa0529cb15724b..35580ce9e89442862b1fd444c028ac98cddfdc6f 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -111,11 +111,14 @@ class Bootstrap { $configfile=INCLUDE_DIR.'settings.php'; //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!'); + Http::response(500, + 'Please rename config file include/settings.php to ' + .'include/ost-config.php to continue!'); } elseif(file_exists(ROOT_DIR.'setup/')) header('Location: '.ROOT_PATH.'setup/'); - if(!$configfile || !file_exists($configfile)) die('<b>Error loading settings. Contact admin.</b>'); + if(!$configfile || !file_exists($configfile)) + Http::response(500,'<b>Error loading settings. Contact admin.</b>'); require($configfile); define('CONFIG_FILE',$configfile); //used in admin.php to check perm. @@ -150,11 +153,11 @@ class Bootstrap { } function croak($message) { - $msg=$ferror."\n\n".THISPAGE; - Mailer::sendmail(ADMIN_EMAIL, 'osTicket Fatal Error', $msg, sprintf('"osTicket Alerts"<%s>', ADMIN_EMAIL)); + $msg = $message."\n\n".THISPAGE; + Mailer::sendmail(ADMIN_EMAIL, 'osTicket Fatal Error', $msg, + sprintf('"osTicket Alerts"<%s>', ADMIN_EMAIL)); //Display generic error to the user - die("<b>Fatal Error:</b> Contact system administrator."); - exit; + Http::response(500, "<b>Fatal Error:</b> Contact system administrator."); } } diff --git a/include/class.config.php b/include/class.config.php index 4114cfcafe264e5e48a384b85527e776d202b5c2..76fec8e783522fa57c9bce71df831b19e043d526 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -883,7 +883,7 @@ class OsticketConfig extends Config { $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']) + if($vars['strip_quoted_reply'] && !trim($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. diff --git a/include/class.file.php b/include/class.file.php index a896d9e9cefd4098be9a7e35080157bbe222d894..d53ff3d50bfe2210210d85e1fa82ab04c0281456 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -402,12 +402,19 @@ class AttachmentChunkedData { } function deleteOrphans() { - - $sql = 'DELETE c.* FROM '.FILE_CHUNK_TABLE.' c ' + $deleted = 0; + $sql = 'SELECT c.file_id, c.chunk_id FROM '.FILE_CHUNK_TABLE.' c ' . ' LEFT JOIN '.FILE_TABLE.' f ON(f.id=c.file_id) ' . ' WHERE f.id IS NULL'; - return db_query($sql)?db_affected_rows():0; + $res = db_query($sql); + while (list($file_id, $chunk_id) = db_fetch_row($res)) { + db_query('DELETE FROM '.FILE_CHUNK_TABLE + .' WHERE file_id='.db_input($file_id) + .' AND chunk_id='.db_input($chunk_id)); + $deleted += db_affected_rows(); + } + return $deleted; } } ?> diff --git a/include/class.format.php b/include/class.format.php index 892cc42f17bd56fe2e934d6c1ffb0c2a6f305fa5..f016896deb39cfefb05b43bf4913bb06feb6fe84 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -86,6 +86,23 @@ class Format { return $text; } + /** + * Decodes filenames given in the content-disposition header according + * to RFC5987, such as filename*=utf-8''filename.png. Note that the + * language sub-component is defined in RFC5646, and that the filename + * is URL encoded (in the charset specified) + */ + function decodeRfc5987($filename) { + $match = array(); + if (preg_match("/([\w!#$%&+^_`{}~-]+)'([\w-]*)'(.*)$/", + $filename, $match)) + // XXX: Currently we don't care about the language component. + // The encoding hint is sufficient. + return self::utf8encode(urldecode($match[3]), $match[1]); + else + return $filename; + } + function phone($phone) { $stripped= preg_replace("/[^0-9]/", "", $phone); diff --git a/include/class.mailer.php b/include/class.mailer.php index d291effb228bf81cc5b7cae62e58b03dcce37bc9..adb0fd0d1188a30ed64e09226fd1c919b90c7662 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -150,7 +150,7 @@ class Mailer { //Desired encodings... $encodings=array( 'head_encoding' => 'quoted-printable', - 'text_encoding' => 'quoted-printable', + 'text_encoding' => 'base64', 'html_encoding' => 'base64', 'html_charset' => 'utf-8', 'text_charset' => 'utf-8', diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 7fee6868a50a70cb1681f05ae8e2c7604510234f..2e5089a5742e0ac9dc3b097dfbf5ef1780df32df 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -28,7 +28,6 @@ class MailFetcher { var $srvstr; 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') { @@ -108,7 +107,7 @@ class MailFetcher { } function getArchiveFolder() { - return $this->ht['archive_folder']; + return $this->mailbox_encode($this->ht['archive_folder']); } /* Core */ @@ -124,10 +123,19 @@ class MailFetcher { /* Default folder is inbox - TODO: provide user an option to fetch from diff folder/label */ function open($box='INBOX') { - if($this->mbox) + if ($this->mbox) $this->close(); - $this->mbox = imap_open($this->srvstr.$box, $this->getUsername(), $this->getPassword()); + $args = array($this->srvstr.$this->mailbox_encode($box), + $this->getUsername(), $this->getPassword()); + + // Disable Kerberos and NTLM authentication if it happens to be + // supported locally or remotely + if (version_compare(PHP_VERSION, '5.3.2', '>=')) + $args += array(NULL, 0, array( + 'DISABLE_AUTHENTICATOR' => array('GSSAPI', 'NTLM'))); + + $this->mbox = call_user_func_array('imap_open', $args); return $this->mbox; } @@ -158,7 +166,8 @@ class MailFetcher { if(!$folder) return false; - return imap_createmailbox($this->mbox, imap_utf7_encode($this->srvstr.trim($folder))); + return imap_createmailbox($this->mbox, + $this->srvstr.$this->mailbox_encode(trim($folder))); } /* check if a folder exists - create one if requested */ @@ -198,6 +207,18 @@ class MailFetcher { return Format::encode($text, $charset, $encoding); } + function mailbox_encode($mailbox) { + if (!$mailbox) + return null; + // Properly encode the mailbox to UTF-7, according to rfc2060, + // section 5.1.3 + elseif (function_exists('mb_convert_encoding')) + return mb_convert_encoding($mailbox, 'UTF7-IMAP', 'utf-8'); + else + // XXX: This function has some issues on some versions of PHP + return imap_utf7_encode($mailbox); + } + //Generic decoder - resulting text is utf8 encoded -> mirrors imap_utf8 function mime_decode($text, $encoding='utf-8') { @@ -308,6 +329,31 @@ class MailFetcher { return $text; } + /** + * Searches the attribute list for a possible filename attribute. If + * found, the attribute value is returned. If the attribute uses rfc5987 + * to encode the attribute value, the value is returned properly decoded + * if possible + * + * Attribute Search Preference: + * filename + * filename* + * name + * name* + */ + function findFilename($attributes) { + foreach (array('filename', 'name') as $pref) { + foreach ($attributes as $a) { + if (strtolower($a->attribute) == $pref) + return $a->value; + // Allow the RFC5987 specification of the filename + elseif (strtolower($a->attribute) == $pref.'*') + return Format::decodeRfc5987($a->value); + } + } + return false; + } + /* getAttachments @@ -319,23 +365,16 @@ class MailFetcher { if($part && !$part->parts) { //Check if the part is an attachment. - $filename = ''; - if($part->ifdisposition && in_array(strtolower($part->disposition), array('attachment', 'inline'))) { - $filename = $part->dparameters[0]->value; - //Some inline attachments have multiple parameters. - if(count($part->dparameters)>1) { - foreach($part->dparameters as $dparameter) { - if(!in_array(strtoupper($dparameter->attribute), array('FILENAME', 'NAME'))) continue; - $filename = $dparameter->value; - break; - } - } - } elseif($part->ifparameters && $part->parameters && $part->type > 0) { //inline attachments without disposition. - foreach($part->parameters as $parameter) { - if(!in_array(strtoupper($parameter->attribute), array('FILENAME', 'NAME'))) continue; - $filename = $parameter->value; - break; - } + $filename = false; + if ($part->ifdisposition + && in_array(strtolower($part->disposition), + array('attachment', 'inline'))) { + $filename = $this->findFilename($part->dparameters); + } + // Inline attachments without disposition. + if (!$filename && $part->ifparameters && $part->parameters + && $part->type > 0) { + $filename = $this->findFilename($part->parameters); } if($filename) { @@ -423,19 +462,27 @@ class MailFetcher { $newticket=true; $errors=array(); + $seen = false; - if (($thread = ThreadEntry::lookupByEmailHeaders($vars)) + if (($thread = ThreadEntry::lookupByEmailHeaders($vars, $seen)) && ($message = $thread->postEmail($vars))) { if (!$message instanceof ThreadEntry) // Email has been processed previously return $message; $ticket = $message->getTicket(); + } elseif ($seen) { + // Already processed, but for some reason (like rejection), no + // thread item was created. Ignore the email + return true; } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) { $message = $ticket->getLastMessage(); } else { //Report success if the email was absolutely rejected. - if(isset($errors['errno']) && $errors['errno'] == 403) + if(isset($errors['errno']) && $errors['errno'] == 403) { + // Never process this email again! + ThreadEntry::logEmailHeaders(0, $vars['mid']); return true; + } # check if it's a bounce! if($vars['header'] && TicketFilter::isAutoBounce($vars['header'])) { diff --git a/include/class.mailparse.php b/include/class.mailparse.php index b9ab3c33ff34fd44496a92affbc38a276fc418b9..039a48b74be371ed1def64f502cad0db16c8506e 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -201,16 +201,38 @@ class Mail_Parse { function getAttachments($part=null){ - if($part==null) - $part=$this->getStruct(); - - if($part && $part->disposition - && (!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? + /* Consider this part as an attachment if + * * It has a Content-Disposition header + * * AND it is specified as either 'attachment' or 'inline' + * * The Content-Type header specifies + * * type is image/* or application/* + * * has a name parameter + */ + if($part && ( + ($part->disposition + && (!strcasecmp($part->disposition,'attachment') + || !strcasecmp($part->disposition,'inline')) + ) + || (!strcasecmp($part->ctype_primary,'image') + || !strcasecmp($part->ctype_primary,'application')))) { + + if (isset($part->d_parameters['filename'])) + $filename = $part->d_parameters['filename']; + elseif (isset($part->d_parameters['filename*'])) + // Support RFC 6266, section 4.3 and RFC, and RFC 5987 + $filename = Format::decodeRfc5987( + $part->d_parameters['filename*']); + + // Support attachments that do not specify a content-disposition + // but do specify a "name" parameter in the content-type header. + elseif (isset($part->ctype_parameters['name'])) + $filename=$part->ctype_parameters['name']; + elseif (isset($part->ctype_parameters['name*'])) + $filename = Format::decodeRfc5987( + $part->ctype_parameters['name*']); + else + // Not an attachment? + return false; $file=array( 'name' => $filename, @@ -229,6 +251,9 @@ class Mail_Parse { return array($file); } + if($part==null) + $part=$this->getStruct(); + $files=array(); if($part->parts){ foreach($part->parts as $k=>$p){ diff --git a/include/class.osticket.php b/include/class.osticket.php index f23dc824c68c541fd2799259cd4c5acdc954f794..a2d2205de0adedfcff9e722efe5745485cda00fc 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -288,14 +288,14 @@ class osTicket { $level=3; //Debug } - //Alert admin if enabled... - if($alert) - $this->alertAdmin($title, $message); - //Logging everything during upgrade. if($this->getConfig()->getLogLevel()<$level && !$force) return false; + //Alert admin if enabled... + if($alert && $this->getConfig()->getLogLevel() >= $level) + $this->alertAdmin($title, $message); + //Save log based on system log level settings. $loglevel=array(1=>'Error','Warning','Debug'); $sql='INSERT INTO '.SYSLOG_TABLE.' SET created=NOW(), updated=NOW() ' @@ -367,63 +367,28 @@ class osTicket { || !strcasecmp($_SERVER['DOCUMENT_ROOT'], $dir)) return '/'; - /* If DOCUMENT_ROOT is set and isn't the same as the directory for - * main.inc.php, then assume that the two have something in common. - * For instance, you might have the following configurations - * - * +-----------------+-----------------------+------------+----------+ - * | DOCUMENT_ROOT | dirname(main.inc.php) | ROOT_PATH | Comments | - * +-----------------+-----------------------+------------+----------+ - * | /var/www | /var/www/osticket | /osticket/ | vanilla | - * | /srv/httpd/www | /httpd/www | / | chrooted | - * | /srv/httpd/www | /httpd/www/osticket | /osticket/ | chrooted | - * +-----------------+-----------------------+------------+----------+ - * - * This algorithm will walk the two paths right to left, chipping - * away at the path of main.inc.php. When the two paths are equal, - * the part removed from the main.inc.php path is the ROOT_PATH - */ - $dir = str_replace('\\', '/', $dir); - $root = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']); - - // Not chrooted - if(strpos($dir, $root)!==false) - return substr($dir, strlen($root)); - - // Chrooted ? - $path = ''; - while (strpos($root, $dir) === false) { - $lastslash = strrpos($dir, '/'); - $path = substr($dir, $lastslash) . $path; - $dir = substr($dir, 0, $lastslash); - if (!$dir) - break; - } - - if($dir && $path) - return $path; - - /* The last resort is to try and use SCRIPT_FILENAME and - * SCRIPT_NAME. The SCRIPT_FILENAME server variable should be the - * full path of the originally-executed-script. The SCRIPT_NAME - * should be the path of that script inside the DOCUMENT_ROOT. This - * is most likely useful if osTicket is run using something like - * Apache UserDir setting where the DOCUMENT_ROOT of Apache and the - * installation path of osTicket have nothing in comon. + /* The main idea is to try and use full-path filename of PHP_SELF and + * SCRIPT_NAME. The SCRIPT_NAME should be the path of that script + * inside the DOCUMENT_ROOT. This is most likely useful if osTicket + * is run using something like Apache UserDir setting where the + * DOCUMENT_ROOT of Apache and the installation path of osTicket + * have nothing in comon. * * +---------------------------+-------------------+----------------+ - * | SCRIPT_FILENAME | SCRIPT_NAME | ROOT_PATH | + * | PHP Script | SCRIPT_NAME | ROOT_PATH | * +---------------------------+-------------------+----------------+ * | /home/u1/www/osticket/... | /~u1/osticket/... | /~u1/osticket/ | * +---------------------------+-------------------+----------------+ * * The algorithm will remove the directory of main.inc.php from - * SCRIPT_FILENAME. What's left should be the script executed inside + * as seen. What's left should be the script executed inside * the osTicket installation. That is removed from SCRIPT_NAME. * What's left is the ROOT_PATH. */ - $path = substr($_SERVER['SCRIPT_FILENAME'], strlen(ROOT_DIR)); - if($path && ($pos=strpos($_SERVER['SCRIPT_NAME'], $path))!==false) + $frame = array_pop(debug_backtrace(false)); + $file = str_replace('\\','/', $frame['file']); + $path = substr($file, strlen(ROOT_DIR)); + if($path && ($pos=strpos($_SERVER['SCRIPT_NAME'], $path))!==false) return substr($_SERVER['SCRIPT_NAME'], 0, $pos); return null; diff --git a/include/class.ostsession.php b/include/class.ostsession.php index b99e5c99135589001de75e1162b5699befa88af8..78b118299f2ceac11acc85206ab212740cb8079a 100644 --- a/include/class.ostsession.php +++ b/include/class.ostsession.php @@ -25,7 +25,12 @@ class osTicketSession { if(!$this->ttl) $this->ttl=SESSION_TTL; - if (defined('DISABLE_SESSION') || OsticketConfig::getDBVersion()) + session_name('OSTSESSID'); + + if (OsticketConfig::getDBVersion()) + return session_start(); + + elseif (defined('DISABLE_SESSION')) return; # Cookies @@ -35,7 +40,9 @@ class osTicketSession { if (isset($_SERVER['HTTP_HOST']) && strpos($_SERVER['HTTP_HOST'], '.') !== false && !Validator::is_ip($_SERVER['HTTP_HOST'])) - $domain = $_SERVER['HTTP_HOST']; + // Remote port specification, as it will make an invalid domain + list($domain) = explode(':', $_SERVER['HTTP_HOST']); + session_set_cookie_params(86400, ROOT_PATH, $domain, osTicket::is_https()); @@ -52,7 +59,6 @@ class osTicketSession { register_shutdown_function('session_write_close'); //Start the session. - session_name('OSTSESSID'); session_start(); } diff --git a/include/class.pdf.php b/include/class.pdf.php index f210149346c7d36d561bdca8bc3fb9ad59771226..ea658378685c04f8345a5e3e73d7042499b1eeb1 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -14,7 +14,7 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ -define('THIS_DIR', str_replace('\\\\', '/', realpath(dirname(__FILE__))) . '/'); //Include path.. +define('THIS_DIR', str_replace('\\', '/', realpath(dirname(__FILE__))) . '/'); //Include path.. define('FPDF_DIR', THIS_DIR . 'fpdf/'); define('FPDF_FONTPATH', FPDF_DIR . 'font/'); //fonts directory. require (FPDF_DIR . 'fpdf.php'); @@ -48,23 +48,46 @@ class Ticket2PDF extends FPDF return $this->ticket; } + function getLogoFile() { + global $ost; + + if (!function_exists('imagecreatefromstring') + || (!($logo = $ost->getConfig()->getClientLogo()))) { + return INCLUDE_DIR.'fpdf/print-logo.png'; + } + + $tmp = tempnam("", 'pdf') . '.jpg'; + $img = imagecreatefromstring($logo->getData()); + // Handle transparent images with white background + $img2 = imagecreatetruecolor(imagesx($img), imagesy($img)); + $white = imagecolorallocate($img2, 255, 255, 255); + imagefill($img2, 0, 0, $white); + imagecopy($img2, $img, 0, 0, 0, 0, imagesx($img), imagesy($img)); + imagejpeg($img2, $tmp); + return $tmp; + } + //report header...most stuff are hard coded for now... function Header() { global $cfg; //Common header - $this->Ln(2); + $logo = $this->getLogoFile(); + $this->Image($logo, null, $this->tMargin, 0, 20); + if (strpos($logo, INCLUDE_DIR) === false) + unlink($logo); $this->SetFont('Times', 'B', 16); - $this->Image(FPDF_DIR . 'print-logo.png', null, 10, 0, 20); - $this->SetX(200, 15); - $this->Cell(0, 15, $cfg->getTitle(), 0, 1, 'R', 0); - //$this->SetY(40); + $this->SetY($this->tMargin + 20); $this->SetX($this->lMargin); - $this->Cell(0, 3, '', "B", 2, 'L'); + $this->Cell(0, 0, '', "B", 2, 'L'); + $this->Ln(1); + $this->SetFont('Arial', 'B',10); + $this->Cell(0, 5, $cfg->getTitle(), 0, 0, 'L'); $this->SetFont('Arial', 'I',10); - $this->Cell(0, 5, 'Generated on '.Format::date($cfg->getDateTimeFormat(), Misc::gmtime(), $_SESSION['TZ_OFFSET'], $_SESSION['TZ_DST']), 0, 0, 'L'); - $this->Cell(0, 5, 'Date & Time based on GMT '.$_SESSION['TZ_OFFSET'], 0, 1, 'R'); - $this->Ln(10); + $this->Cell(0, 5, Format::date($cfg->getDateTimeFormat(), Misc::gmtime(), + $_SESSION['TZ_OFFSET'], $_SESSION['TZ_DST']) + .' GMT '.$_SESSION['TZ_OFFSET'], 0, 1, 'R'); + $this->Ln(5); } //Page footer baby @@ -91,11 +114,21 @@ class Ticket2PDF extends FPDF } function _utf8($text) { + // Assume text is in utf-8 charset + $flags = ENT_COMPAT; + if (phpversion() >= '5.4.0') + $flags |= ENT_HTML401; + + // Assume text in the database is HTML + $text = html_entity_decode($text, $flags, 'UTF-8'); - if(function_exists('iconv')) + if (function_exists('iconv')) return iconv('UTF-8', 'windows-1252', $text); + elseif (function_exists('utf8_decode')) + return utf8_decode($text); - return utf8_encode($text); + // XXX: FPDF does not support UTF-8 encoding + return $text; } function _print() { diff --git a/include/class.thread.php b/include/class.thread.php index c31a1915165bfcc2ffc983da3c91083587a7f324..29e1b1d574c44e79bc8c1045b3fbf021fc58f3c4 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -528,12 +528,15 @@ Class ThreadEntry { 'reply_to' => $this, ); + if (isset($mailinfo['attachments'])) + $vars['attachments'] = $mailinfo['attachments']; + $body = $mailinfo['message']; // Disambiguate if the user happens also to be a staff member of the // system. The current ticket owner should _always_ post messages // instead of notes or responses - if ($mailinfo['email'] == $ticket->getEmail()) { + if (strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) { $vars['message'] = $body; return $ticket->postMessage($vars, 'Email'); } @@ -544,7 +547,7 @@ Class ThreadEntry { $vars['note'] = $body; return $ticket->postNote($vars, $errors, $poster); } - elseif (Email::lookupByEmail($mailinfo['email'])) { + elseif (Email::getIdByEmail($mailinfo['email'])) { // Don't process the email -- it came FROM this system return true; } @@ -579,17 +582,23 @@ Class ThreadEntry { 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. - if (isset($vars['header'])) - $sql .= ', headers='.db_input($vars['header']); - $this->ht['email_mid'] = $vars['mid']; - return db_query($sql)?db_insert_id():0; + $header = false; + if (isset($vars['header'])) + $header = $vars['header']; + self::logEmailHeaders($this->getId(), $vars['mid'], $header); } + /* static */ + function logEmailHeaders($id, $mid, $header=false) { + $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE + .' SET message_id='.db_input($id) //TODO: change it to thread_id + .', email_mid='.db_input($mid); //TODO: change it to message_id. + if ($header) + $sql .= ', headers='.db_input($header); + return db_query($sql)?db_insert_id():0; + } /* variables */ @@ -640,28 +649,39 @@ Class ThreadEntry { * - "in-reply-to" => Message-Id the email is a direct response to * - "references" => List of Message-Id's the email is in response * - "subject" => Find external ticket number in the subject line + * + * seen (by-ref:bool) a flag that will be set if the message-id was + * positively found, indicating that the message-id has been + * previously seen. This is useful if no thread-id is associated + * with the email (if it was rejected for instance). */ - function lookupByEmailHeaders($mailinfo) { + function lookupByEmailHeaders($mailinfo, &$seen=false) { // Search for messages using the References header, then the // in-reply-to header - $search = 'SELECT message_id FROM '.TICKET_EMAIL_INFO_TABLE + $search = 'SELECT message_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE . ' WHERE email_mid=%s ORDER BY message_id DESC'; - if ($id = db_result(db_query( - sprintf($search, db_input($mailinfo['mid']))))) + if (list($id, $mid) = db_fetch_row(db_query( + sprintf($search, db_input($mailinfo['mid']))))) { + $seen = true; return ThreadEntry::lookup($id); + } foreach (array('mid', 'in-reply-to', 'references') as $header) { $matches = array(); if (!isset($mailinfo[$header]) || !$mailinfo[$header]) continue; // Header may have multiple entries (usually separated by - // semi-colons (;)) + // spaces ( ) elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header], $matches)) continue; - foreach ($matches[0] as $mid) { + // The References header will have the most recent message-id + // (parent) on the far right. + // @see rfc 1036, section 2.2.5 + // @see http://www.jwz.org/doc/threading.html + foreach (array_reverse($matches[0]) as $mid) { $res = db_query(sprintf($search, db_input($mid))); while (list($id) = db_fetch_row($res)) { if ($t = ThreadEntry::lookup($id)) @@ -671,11 +691,16 @@ Class ThreadEntry { } // Search for ticket by the [#123456] in the subject line + // This is the last resort - emails must match to avoid message + // injection by third-party. $subject = $mailinfo['subject']; $match = array(); - if ($subject && preg_match("/\[#([0-9]{1,10})\]/", $subject, $match)) + if ($subject && $mailinfo['email'] + && preg_match("/\[#([0-9]{1,10})\]/", $subject, $match) + && ($tid = Ticket::getIdByExtId((int)$match[1], $mailinfo['email'])) + ) // Return last message for the thread - return Message::lastByExtTicketId((int)$match[1]); + return Message::lastByTicketId($tid); return null; } @@ -780,15 +805,16 @@ class Message extends ThreadEntry { )?$m:null; } - function lastByExtTicketId($ticketId) { - $sql = 'SELECT thread.id FROM '.TICKET_THREAD_TABLE - .' thread JOIN '.TICKET_TABLE.' ticket ON (ticket.ticket_id = thread.ticket_id) - WHERE thread_type=\'M\' AND ticket.ticketID = '.db_input($ticketId) + function lastByTicketId($ticketId) { + + $sql=' SELECT thread.id FROM '.TICKET_THREAD_TABLE.' thread ' + .' WHERE thread_type=\'M\' AND thread.ticket_id = '.db_input($ticketId) .' ORDER BY thread.id DESC LIMIT 1'; - if (($res = db_query($sql)) && (list($id) = db_fetch_row($res))) + + if (($res = db_query($sql)) && ($id = db_result($res))) return Message::lookup($id); - else - return null; + + return null; } } diff --git a/include/class.ticket.php b/include/class.ticket.php index 85b7e3b3820d447ba02c810fdbf975e899887022..294291f65cd079ae43e756bdeca738abc3011662 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -471,7 +471,11 @@ class Ticket { } function getLastMessage() { - return Message::lookup($this->getLastMsgId(), $this->getId()); + + if($this->getLastMsgId()) + return Message::lookup($this->getLastMsgId(), $this->getId()); + + return Message::lastByTicketId($this->getId()); } function getThread() { diff --git a/include/fpdf/print-logo.png b/include/fpdf/print-logo.png index 6526ebe0a15f393382e0339b38cef8422d7ff471..1d8b7d61d2cda843409c22cc19db77aafd5981ac 100644 Binary files a/include/fpdf/print-logo.png and b/include/fpdf/print-logo.png differ diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index d67243af734c466eebb31a264a53d013fe3356c0..3d119fb8ce5376675e570acf45799900eb683911 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -294,7 +294,7 @@ if(!$cfg->showNotesInline()) { ?> <th width="640"> <?php echo sprintf('%s <em>posted by <b>%s</b></em>', - Format::htmlchars($note['title']), + $note['title'], Format::htmlchars($note['poster'])); ?> </th> @@ -338,7 +338,7 @@ if(!$cfg->showNotesInline()) { ?> <table class="<?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> <tr> <th width="200"><?php echo Format::db_datetime($entry['created']);?></th> - <th width="440"><span><?php echo Format::htmlchars($entry['title']); ?></span></th> + <th width="440"><span><?php echo $entry['title']; ?></span></th> <th width="300" class="tmeta"><?php echo Format::htmlchars($entry['poster']); ?></th> </tr> <tr><td colspan=3><?php echo Format::display($entry['body']); ?></td></tr> diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php index efde7fa537a4b25cba6fda11e0ee5e338a56bd1c..cfc84132a748a85a6d3c1af32e26d1b15e17ae17 100644 --- a/include/staff/tpl.inc.php +++ b/include/staff/tpl.inc.php @@ -29,8 +29,7 @@ if (is_a($template, EmailTemplateGroup)) { $msgtemplates=$template->getGroup()->all_names; $info=array_merge(array('subject'=>$template->getSubject(), 'body'=>$template->getBody()),$info); } -$info['tpl']=($info['tpl'] && $msgtemplates[$info['tpl']])?$info['tpl']:'ticket.autoresp'; -$tpl=$msgtemplates[$info['tpl']]; +$tpl=$msgtemplates[$selected]; ?> <h2>Email Template Message - <span><?php echo $name; ?></span></h2> diff --git a/setup/cli/modules/deploy.php b/setup/cli/modules/deploy.php index 4d48689927d1cd6fec2d087421229e3520fa23b9..77798797b9e5aebd2fd3810af570cf0263170df6 100644 --- a/setup/cli/modules/deploy.php +++ b/setup/cli/modules/deploy.php @@ -16,6 +16,10 @@ class Deployment extends Unpacker { 'action'=>'store_true', 'help'=>'Don\'t actually deploy new code. Just show the files that would be copied'); + $this->options['setup'] = array('-s','--setup', + 'action'=>'store_true', + 'help'=>'Deploy the setup folder. Useful for deploying for new + installations.'); # super(*args); call_user_func_array(array('parent', '__construct'), func_get_args()); } @@ -52,10 +56,14 @@ class Deployment extends Unpacker { # Locate the upload folder $root = $this->find_root_folder(); + $exclusions = array("$root/include", "$root/.git*", + "*.sw[a-z]","*.md", "*.txt"); + if (!$options['setup']) + $exclusions[] = "$root/setup"; + # Unpack everything but the include/ folder $this->unpackage("$root/{,.}*", $this->destination, -1, - array("$root/setup", "$root/include", "$root/.git*", - "*.sw[a-z]","*.md", "*.txt")); + $exclusions); # Unpack the include folder $this->unpackage("$root/include/{,.}*", $include, -1, array("*/include/ost-config.php")); diff --git a/setup/cli/package.php b/setup/cli/package.php index 83a63d27dc4fda0cde37f69e566613df224d42d9..f61d9792d934b6585c42438ee531c529bd82efcf 100755 --- a/setup/cli/package.php +++ b/setup/cli/package.php @@ -17,6 +17,10 @@ function get_osticket_root_path() { return realpath($start); } +function run_tests($root) { + return (require "$root/setup/test/run-tests.php"); +} + # Check PHP syntax across all php files function glob_recursive($pattern, $flags = 0) { $files = glob($pattern, $flags); @@ -64,6 +68,10 @@ function package($pattern, $destination, $recurse=false, $exclude=false) { } } +# Run tests before continuing +if (run_tests($root) > 0) + die("Regression tests failed. Cowardly refusing to package\n"); + # Create the stage folder for the install files if (!is_dir($stage_path)) mkdir($stage_path); diff --git a/setup/setup.inc.php b/setup/setup.inc.php index addbb4fddc8c57a2c35643a8cae985d2c9823ab6..f0fee2989584117a279d36376066e990b62bb4ce 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.0+'); +define('THIS_VERSION', '1.7-git'); #inits - error reporting. $error_reporting = E_ALL & ~E_NOTICE; diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php index 0f42dd7d024f04bf03d87a8b5611a3b658c6953e..3a3df3a1dd09702d7f74bf770b080928dae5bf43 100644 --- a/setup/test/run-tests.php +++ b/setup/test/run-tests.php @@ -4,25 +4,29 @@ if (php_sapi_name() != 'cli') exit(); require_once "tests/class.test.php"; -function get_osticket_root_path() { - # Hop up to the root folder - $start = dirname(__file__); - for (;;) { - if (file_exists($start . '/main.inc.php')) break; - $start .= '/..'; +if (!function_exists('get_osticket_root_path')) { + function get_osticket_root_path() { + # Hop up to the root folder + $start = dirname(__file__); + for (;;) { + if (file_exists($start . '/main.inc.php')) break; + $start .= '/..'; + } + return realpath($start); } - return realpath($start); } $root = get_osticket_root_path(); -# Check PHP syntax across all php files -function glob_recursive($pattern, $flags = 0) { - $files = glob($pattern, $flags); - foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { - $files = array_merge($files, - glob_recursive($dir.'/'.basename($pattern), $flags)); +if (!function_exists('glob_recursive')) { + # Check PHP syntax across all php files + function glob_recursive($pattern, $flags = 0) { + $files = glob($pattern, $flags); + foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { + $files = array_merge($files, + glob_recursive($dir.'/'.basename($pattern), $flags)); + } + return $files; } - return $files; } $fails = array(); @@ -37,6 +41,7 @@ function show_fails() { $script = str_replace($root.'/', '', $script); print("$test: $message @ $script:$line\n"); } + return count($fails); } } if (function_exists('pcntl_signal')) { @@ -44,8 +49,7 @@ if (function_exists('pcntl_signal')) { function show_fails_on_ctrlc() { while (@ob_end_flush()); print("\n"); - show_fails(); - exit(); + exit(show_fails()); } pcntl_signal(SIGINT, 'show_fails_on_ctrlc'); } @@ -64,4 +68,10 @@ foreach (glob_recursive(dirname(__file__)."/tests/test.*.php") as $t) { } show_fails(); +// If executed directly expose the fail count to a shell script +global $argv; +if (!strcasecmp(basename($argv[0]), basename(__file__))) + exit(count($fails)); +else + return count($fails); ?> diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php index 80a07b87b31acf34126712a132139c5c55c2f3ac..5ce1297619714533c19bdfd7f2c083b4b9d255b8 100644 --- a/setup/test/tests/class.test.php +++ b/setup/test/tests/class.test.php @@ -10,6 +10,7 @@ class Test { '/include/PasswordHash.php', '/include/pear/', '/include/Spyc.php', + '/setup/cli/stage/', ); function Test() { diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php new file mode 100644 index 0000000000000000000000000000000000000000..38cd382cdfc0810e0698446496a35f92bdd4cc5d --- /dev/null +++ b/setup/test/tests/stubs.php @@ -0,0 +1,15 @@ +<?php + +class mysqli { + + function query() {} + function real_escape_string() {} + function fetch_row() {} + +} + +class ReflectionClass { + function getMethods() {} +} + +?>