diff --git a/include/class.format.php b/include/class.format.php index e3f7b415a7553f6f77e6da85b31aff4d2c2f9ba5..ac9f94c3b42a9e5f1af87d196d0a2e24ac4fdacb 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -301,19 +301,19 @@ class Format { } //make urls clickable. Mainly for display - function clickableurls($text) { + function clickableurls($text, $trampoline=true) { global $ost; $token = $ost->getLinkToken(); // Find all text between tags $text = preg_replace_callback(':^[^<]+|>[^<]+:', - function($match) use ($token) { + function($match) use ($token, $trampoline) { // Scan for things that look like URLs return preg_replace_callback( '`(?<!>)(((f|ht)tp(s?)://|(?<!//)www\.)([-+~%/.\w]+)(?:[-?#+=&;%@.\w]*)?)' .'|(\b[_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,4})`', - function ($match) use ($token) { + function ($match) use ($token, $trampoline) { if ($match[1]) { while (in_array(substr($match[1], -1), array('.','?','-',':',';'))) { @@ -323,9 +323,13 @@ class Format { if (strpos($match[2], '//') === false) { $match[1] = 'http://' . $match[1]; } - return '<a href="l.php?url='.urlencode($match[1]) - .sprintf('&auth=%s" target="_blank">', $token) - .$match[1].'</a>'.$match[9]; + if ($trampoline) + return '<a href="l.php?url='.urlencode($match[1]) + .sprintf('&auth=%s" target="_blank">', $token) + .$match[1].'</a>'.$match[9]; + else + return sprintf('<a href="%s">%s</a>%s', + $match[1], $match[1], $match[9]); } elseif ($match[6]) { return sprintf('<a href="mailto:%1$s" target="_blank">%1$s</a>', $match[6]); diff --git a/include/class.pdf.php b/include/class.pdf.php index 404e4dd7bd942ad0114a919fe0efec83d1c83a28..8f41ec2db867a3e6487b664d7aac37a62ab6a763 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -293,15 +293,17 @@ class Ticket2PDF extends mPDF $this->WriteCell($w, 7, Format::truncate($entry['title'], 50), 'TB', 0, 'L', true); $this->WriteCell($w/2, 7, $entry['name'] ?: $entry['poster'], 'TBR', 1, 'L', true); $this->SetFont(''); - $text= $entry['body']; + $text = $entry['body']->display('pdf'); if($entry['attachments'] && ($tentry=$ticket->getThreadEntry($entry['id'])) && ($attachments = $tentry->getAttachments())) { $files = array(); foreach($attachments as $attachment) - $files[]= $attachment['name']; + if (!$attachment['inline']) + $files[]= $attachment['name']; - $text.="<div>Files Attached: [".implode(', ',$files)."]</div>"; + if ($files) + $text.="<div>Files Attached: [".implode(', ',$files)."]</div>"; } $this->WriteHtml('<div class="thread-body">'.$text.'</div>', 2, false, false); $this->Ln(5); diff --git a/include/class.thread.php b/include/class.thread.php index 02c215aae12d0797f7bc4c1fd4aa6746125a09b1..dcb4ec2afd8144c1c3bf06f3d95592b035a513f3 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -138,9 +138,12 @@ class Thread { .' ORDER BY thread.created '.$order; $entries = array(); - if(($res=db_query($sql)) && db_num_rows($res)) - while($rec=db_fetch_array($res)) + if(($res=db_query($sql)) && db_num_rows($res)) { + while($rec=db_fetch_array($res)) { + $rec['body'] = ThreadBody::fromFormattedText($rec['body'], $rec['format']); $entries[] = $rec; + } + } return $entries; } @@ -305,15 +308,22 @@ Class ThreadEntry { } function getBody() { - return $this->ht['body']; + return ThreadBody::fromFormattedText($this->ht['body'], $this->ht['format']); } function setBody($body) { global $cfg; + if (!$body instanceof ThreadBody) { + if ($cfg->isHtmlThreadEnabled()) + $body = new HtmlThreadBody($body); + else + $body = new TextThreadBody($body); + } + $sql='UPDATE '.TICKET_THREAD_TABLE.' SET updated=NOW()' - .',body='.db_input(Format::sanitize($body, - !$cfg->isHtmlThreadEnabled())) + .',format='.db_input($body->getType()) + .',body='.db_input((string) $body) .' WHERE id='.db_input($this->getId()); return db_query($sql) && db_affected_rows(); } @@ -760,7 +770,7 @@ Class ThreadEntry { /* variables */ function __toString() { - return $this->getBody(); + return (string) $this->getBody(); } function asVar() { @@ -969,9 +979,7 @@ Class ThreadEntry { $vars['body'] = new TextThreadBody($vars['body']); } - // Drop stripped images. NOTE: This should be done before - // ->convert() because the strippedImages list will not propagate to - // the newly converted thread body + // Drop stripped images if ($vars['attachments']) { foreach ($vars['body']->getStrippedImages() as $cid) { foreach ($vars['attachments'] as $i=>$a) { @@ -994,8 +1002,7 @@ Class ThreadEntry { } } - if (!($body = Format::sanitize( - (string) $vars['body']->convertTo('html')))) + if (!($body = $vars['body']->getClean())) $body = '-'; //Special tag used to signify empty message as stored. $poster = $vars['poster']; @@ -1006,6 +1013,7 @@ Class ThreadEntry { .' ,thread_type='.db_input($vars['type']) .' ,ticket_id='.db_input($vars['ticketId']) .' ,title='.db_input(Format::sanitize($vars['title'], true)) + .' ,format='.db_input($vars['body']->getType()) .' ,staff_id='.db_input($vars['staffId']) .' ,user_id='.db_input($vars['userId']) .' ,poster='.db_input($poster) @@ -1251,13 +1259,21 @@ class ThreadBody /* extends SplString */ { var $type; var $stripped_images = array(); var $embedded_images = array(); + var $options = array( + 'strip-embedded' => true + ); - function __construct($body, $type='text') { + function __construct($body, $type='text', $options=array()) { $type = strtolower($type); if (!in_array($type, static::$types)) throw new Exception($type.': Unsupported ThreadBody type'); $this->body = (string) $body; $this->type = $type; + $this->options = array_merge($this->options, $options); + } + + function isEmpty() { + return !$this->body || $this->body == '-'; } function convertTo($type) { @@ -1307,21 +1323,66 @@ class ThreadBody /* extends SplString */ { return $this->embedded_images; } + function getType() { + return $this->type; + } + + function getClean() { + return trim($this->body); + } + function __toString() { - return $this->body; + return (string) $this->body; + } + + function toHtml() { + return $this->display('html'); + } + + function display($format=false) { + throw new Exception('display: Abstract dispplay() method not implemented'); + } + + static function fromFormattedText($text, $format=false) { + switch ($format) { + case 'text': + return new TextThreadBody($text); + case 'html': + return new HtmlThreadBody($text, array('strip-embedded'=>false)); + default: + return new ThreadBody($text); + } } } class TextThreadBody extends ThreadBody { - function __construct($body) { - parent::__construct(Format::stripEmptyLines($body), 'text'); + function __construct($body, $options=array()) { + parent::__construct($body, 'text', $options); + } + + function getClean() { + return Format::stripEmptyLines($this->body); + } + + function display($output=false) { + if ($this->isEmpty()) + return '(empty)'; + + switch ($output) { + case 'html': + return '<div style="white-space:pre-wrap">'.$this->body.'</div>'; + case 'pdf': + return nl2br($this->body); + default: + return '<pre>'.$this->body.'</pre>'; + } } } class HtmlThreadBody extends ThreadBody { - function __construct($body) { - $body = $this->extractEmbeddedHtmlImages($body); - $body = trim($body, " <>br/\t\n\r") ? Format::safe_html($body) : ''; - parent::__construct($body, 'html'); + function __construct($body, $options=array()) { + parent::__construct($body, 'html', $options); + if ($this->options['strip-embedded']) + $this->body = $this->extractEmbeddedHtmlImages($this->body); } function extractEmbeddedHtmlImages($body) { @@ -1336,5 +1397,21 @@ class HtmlThreadBody extends ThreadBody { return 'src="cid:'.$info['cid'].'"'; }, $body); } + + function getClean() { + return trim($body, " <>br/\t\n\r") ? Format::sanitize($body) : ''; + } + + function display($output=false) { + if ($this->isEmpty()) + return '(empty)'; + + switch ($output) { + case 'pdf': + return Format::clickableurls($this->body, false); + default: + return Format::display($this->body); + } + } } ?> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index f26737719309736c0863e03877e7a9a1c302e60a..deaa097d9a92692728e0fe5fb3eddb49b2a27581 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -101,8 +101,6 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $idx=>$form) { if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { $threadType=array('M' => 'message', 'R' => 'response'); foreach($thread as $entry) { - if ($entry['body'] == '-') - $entry['body'] = '(EMPTY)'; //Making sure internal notes are not displayed due to backend MISTAKES! if(!$threadType[$entry['thread_type']]) continue; @@ -112,7 +110,7 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { ?> <table class="thread-entry <?php echo $threadType[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> <tr><th><?php echo Format::db_datetime($entry['created']); ?> <span class="textra"></span><span><?php echo $poster; ?></span></th></tr> - <tr><td class="thread-body"><div><?php echo Format::viewableImages(Format::display($entry['body'])); ?></div></td></tr> + <tr><td class="thread-body"><div><?php echo $entry['body']->toHtml(); ?></div></td></tr> <?php if($entry['attachments'] && ($tentry=$ticket->getThreadEntry($entry['id'])) diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 1f05d04a5daf6445e1683d083862ca32937510de..f885a7ceff0e3ae2aa1307e9d93e0695137d6408 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -342,10 +342,7 @@ $tcount+= $ticket->getNumNotes(); /* -------- Messages & Responses & Notes (if inline)-------------*/ $types = array('M', 'R', 'N'); if(($thread=$ticket->getThreadEntries($types))) { - foreach($thread as $entry) { - if ($entry['body'] == '-') - $entry['body'] = '(EMPTY)'; - ?> + foreach($thread as $entry) { ?> <table class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> <tr> <th colspan="4" width="100%"> @@ -365,7 +362,7 @@ $tcount+= $ticket->getNumNotes(); </tr> <tr><td colspan="4" class="thread-body" id="thread-id-<?php echo $entry['id']; ?>"><div><?php - echo Format::viewableImages(Format::display($entry['body'])); ?></div></td></tr> + echo $entry['body']->toHtml(); ?></div></td></tr> <?php if($entry['attachments'] && ($tentry = $ticket->getThreadEntry($entry['id'])) diff --git a/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql b/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql index 19ddac83bf11b1d262c167e6f73b190db63babb7..936b3b0c2273a18e9a9bdd9410cd321121cb1ed1 100644 --- a/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql +++ b/include/upgrader/streams/core/d51f303a-dad45ca2.patch.sql @@ -156,21 +156,9 @@ UPDATE `%TABLE_PREFIX%canned_response` '>', '>'), '\n', '<br/>'); --- Migrate ticket-thread to HTML --- XXX: Migrate & -> & ? -- the problem is that there's a fix in 1.7.1 --- that properly encodes these characters, so encoding & would mean possible --- double encoding. -UPDATE `%TABLE_PREFIX%ticket_thread` - SET `body` = REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( - `body`, - '\r', ''), - '\n ', '\n'), - '\n\n\n', '\n\n'), - '\n\n\n', '\n\n'), - '\n\n\n', '\n\n'), - '<', '<'), - '>', '>'), - '\n', '<br/>'); +-- Mark all thread entries as text +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + ADD `format` varchar(16) NOT NULL default 'text' AFTER `body`; -- Finished with patch UPDATE `%TABLE_PREFIX%config` diff --git a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql index ee2296eb5754404acf176b006b250529554a3f55..a75a8372d3bbf205d9d85446a59ae923076f0075 100644 --- a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql +++ b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql @@ -136,6 +136,25 @@ UPDATE `%TABLE_PREFIX%content` SET `content_id` = LAST_INSERT_ID() DELETE FROM `%TABLE_PREFIX%email_template` WHERE `code_name` IN ('staff.pwreset', 'user.accesslink'); +-- The original patch for d51f303a-dad45ca2.patch.sql migrated all the +-- thread entries from text to html. Now that the format column exists in +-- the ticket_thread table, we opted to retroactively add the format column +-- to the dad45ca2 patch. Therefore, anyone upgrading from osTicket < 1.8.0 +-- to v1.8.2 and further will alreay have a `format` column when they arrive +-- at this patch. In such a case, we'll just change the default to 'html' +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = '%TABLE_PREFIX%ticket_thread' + AND table_schema = DATABASE() + AND column_name = 'format' + ) > 0, + "ALTER TABLE `%TABLE_PREFIX%ticket_thread` CHANGE `format` `format` varchar(16) NOT NULL default 'html'", + "ALTER TABLE `%TABLE_PREFIX%ticket_thread` ADD `format` varchar(16) NOT NULL default 'html' AFTER `body`" +)); +PREPARE stmt FROM @s; +EXECUTE stmt; + -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = '4323a6a81c35efbf7722b7fc4e475440'