diff --git a/composer.json b/composer.json index efd4bd081..c24292454 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "league/html-to-markdown": "^4.8", "level-2/dice": "^4", "lightopenid/lightopenid": "dev-master", + "matriphe/iso-639": "^1.2", "michelf/php-markdown": "^1.7", "mobiledetect/mobiledetectlib": "^2.8", "monolog/monolog": "^1.25", @@ -47,6 +48,7 @@ "psr/container": "^1.0", "seld/cli-prompt": "^1.0", "smarty/smarty": "^3.1", + "xemlock/htmlpurifier-html5": "^0.1.11", "fxp/composer-asset-plugin": "^1.4", "bower-asset/base64": "^1.0", "bower-asset/chart-js": "^2.8", @@ -64,8 +66,7 @@ "npm-asset/moment": "^2.24", "npm-asset/perfect-scrollbar": "0.6.16", "npm-asset/textcomplete": "^0.18.2", - "npm-asset/typeahead.js": "^0.11.1", - "matriphe/iso-639": "^1.2" + "npm-asset/typeahead.js": "^0.11.1" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index 8ba31ecb0..779c3b51b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fd22bd8c29dcea3d6b6eeb117d79af52", + "content-hash": "7d8031c9b95fd94d8872804759a26509", "packages": [ { "name": "asika/simple-console", @@ -3431,6 +3431,57 @@ "shim" ], "time": "2020-05-12T16:14:59+00:00" + }, + { + "name": "xemlock/htmlpurifier-html5", + "version": "v0.1.11", + "source": { + "type": "git", + "url": "https://github.com/xemlock/htmlpurifier-html5.git", + "reference": "f0d563f9fd4a82a3d759043483f9a94c0d8c2255" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/xemlock/htmlpurifier-html5/zipball/f0d563f9fd4a82a3d759043483f9a94c0d8c2255", + "reference": "f0d563f9fd4a82a3d759043483f9a94c0d8c2255", + "shasum": "" + }, + "require": { + "ezyang/htmlpurifier": "^4.8", + "php": ">=5.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.1|^2.1", + "phpunit/phpunit": ">=4.7 <8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "library/HTMLPurifier/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "xemlock", + "email": "xemlock@gmail.com" + } + ], + "description": "HTML5 element definitions for HTML Purifier", + "keywords": [ + "HTML5", + "Purifier", + "html", + "htmlpurifier", + "security", + "tidy", + "validator", + "xss" + ], + "time": "2019-08-07T17:19:21+00:00" } ], "packages-dev": [ diff --git a/src/Content/PageInfo.php b/src/Content/PageInfo.php index 786385c3e..5396bc1bb 100644 --- a/src/Content/PageInfo.php +++ b/src/Content/PageInfo.php @@ -265,7 +265,7 @@ class PageInfo } if (!$matches && $searchNakedUrls) { - preg_match('~(?<=\W|^)(?', $data['url'], self::proxyUrl($data['image'], $simplehtml), $data['title']); } else { if (!empty($data['image'])) { - $return .= sprintf('
', $data['url'], self::proxyUrl($data['image'], $simplehtml), $data['title']); + $return .= sprintf('
', $data['url'], self::proxyUrl($data['image'], $simplehtml), $data['title']); } elseif (!empty($data['preview'])) { - $return .= sprintf('
', $data['url'], self::proxyUrl($data['preview'], $simplehtml), $data['title']); + $return .= sprintf('
', $data['url'], self::proxyUrl($data['preview'], $simplehtml), $data['title']); } $return .= sprintf('

%s

', $data['url'], $data['title']); } @@ -1033,7 +1036,7 @@ class BBCode switch ($simplehtml) { case self::API: - $text = ($is_quote_share? '
' : '') . '

' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ':

' . "\n" . $content; + $text = ($is_quote_share? '
' : '') . '

' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . ':

' . "\n" . $content; break; case self::DIASPORA: if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0) { @@ -1062,7 +1065,7 @@ class BBCode break; case self::OSTATUS: - $text = ($is_quote_share? '
' : '') . '

' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' @' . $author_contact['addr'] . ': ' . $content . '

' . "\n"; + $text = ($is_quote_share? '
' : '') . '

' . html_entity_decode('♲ ', ENT_QUOTES, 'UTF-8') . ' @' . $author_contact['addr'] . ': ' . $content . '

' . "\n"; break; case self::ACTIVITYPUB: $author = '@' . $author_contact['addr'] . ':'; @@ -1275,6 +1278,8 @@ class BBCode return ''; } + Hook::callAll('bbcode', $text); + $a = DI::app(); $text = self::performWithEscapedTags($text, ['code'], function ($text) use ($try_oembed, $simple_html, $for_plaintext, $a) { @@ -1300,11 +1305,12 @@ class BBCode return $return; }; - - // Remove the abstract element. It is a non visible element. $text = self::stripAbstract($text); + // Line ending normalisation + $text = str_replace("\r\n", "\n", $text); + // Move new lines outside of tags $text = preg_replace("#\[(\w*)](\n*)#ism", '$2[$1]', $text); $text = preg_replace("#(\n*)\[/(\w*)]#ism", '[/$2]$1', $text); @@ -1338,16 +1344,6 @@ class BBCode $text = preg_replace("/\[share(.*?)avatar\s?=\s?'.*?'\s?(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism", "\n[share$1$2]$3[/share]", $text); } - // Convert new line chars to html
tags - - // nlbr seems to be hopelessly messed up - // $Text = nl2br($Text); - - // We'll emulate it. - - $text = trim($text); - $text = str_replace("\r\n", "\n", $text); - // Remove linefeeds inside of the table elements. See issue #6799 $search = ["\n[th]", "[th]\n", " [th]", "\n[/th]", "[/th]\n", "[/th] ", "\n[td]", "[td]\n", " [td]", "\n[/td]", "[/td]\n", "[/td] ", @@ -1367,11 +1363,14 @@ class BBCode $replace = ["[table]", "[/table]"]; $text = str_replace($search, $replace, $text); + // Trim new lines regardless of the system.remove_multiplicated_lines config value + $text = trim($text, "\n"); + // removing multiplicated newlines if (DI::config()->get('system', 'remove_multiplicated_lines')) { - $search = ["\n\n\n", "\n ", " \n", "[/quote]\n\n", "\n[/quote]", "[/li]\n", "\n[li]", "\n[ul]", "[/ul]\n", "\n\n[share ", "[/attachment]\n", + $search = ["\n\n\n", "\n ", " \n", "[/quote]\n\n", "\n[/quote]", "[/li]\n", "\n[li]", "\n[*]", "\n[ul]", "[/ul]\n", "\n\n[share ", "[/attachment]\n", "\n[h1]", "[/h1]\n", "\n[h2]", "[/h2]\n", "\n[h3]", "[/h3]\n", "\n[h4]", "[/h4]\n", "\n[h5]", "[/h5]\n", "\n[h6]", "[/h6]\n"]; - $replace = ["\n\n", "\n", "\n", "[/quote]\n", "[/quote]", "[/li]", "[li]", "[ul]", "[/ul]", "\n[share ", "[/attachment]", + $replace = ["\n\n", "\n", "\n", "[/quote]\n", "[/quote]", "[/li]", "[li]", "[*]", "[ul]", "[/ul]", "\n[share ", "[/attachment]", "[h1]", "[/h1]", "[h2]", "[/h2]", "[h3]", "[/h3]", "[h4]", "[/h4]", "[h5]", "[/h5]", "[h6]", "[/h6]"]; do { $oldtext = $text; @@ -1447,8 +1446,8 @@ class BBCode // Check for sized text // [size=50] --> font-size: 50px (with the unit). if ($simple_html != self::DIASPORA) { - $text = preg_replace("(\[size=(\d*?)\](.*?)\[\/size\])ism", "$2", $text); - $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", "$2", $text); + $text = preg_replace("(\[size=(\d*?)\](.*?)\[\/size\])ism", '$2', $text); + $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", '$2', $text); } else { // Issue 2199: Diaspora doesn't interpret the construct above, nor the or element $text = preg_replace("(\[size=(.*?)\](.*?)\[\/size\])ism", "$2", $text); @@ -1456,28 +1455,16 @@ class BBCode // Check for centered text - $text = preg_replace("(\[center\](.*?)\[\/center\])ism", "
$1
", $text); + $text = preg_replace("(\[center\](.*?)\[\/center\])ism", '
$1
', $text); // Check for list text $text = str_replace("[*]", "
  • ", $text); // Check for style sheet commands - $text = preg_replace_callback( - "(\[style=(.*?)\](.*?)\[\/style\])ism", - function ($match) { - return "" . $match[2] . ""; - }, - $text - ); + $text = preg_replace("(\[style=(.*?)\](.*?)\[\/style\])ism", '$2', $text); // Check for CSS classes - $text = preg_replace_callback( - "(\[class=(.*?)\](.*?)\[\/class\])ism", - function ($match) { - return "" . $match[2] . ""; - }, - $text - ); + $text = preg_replace("(\[class=(.*?)\](.*?)\[\/class\])ism", '$2', $text); // handle nested lists $endlessloop = 0; @@ -1608,20 +1595,20 @@ class BBCode $text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '' . DI::l10n()->t('Image/photo') . '', $text); $text = preg_replace("/\[zmg\](.*?)\[\/zmg\]/ism", '' . DI::l10n()->t('Image/photo') . '', $text); - $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '
    ' . DI::l10n()->t('Encrypted content') . '
    ', $text); - $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '
    ' . DI::l10n()->t('Encrypted content') . '
    ', $text); - //$Text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '
    ' . DI::l10n()->t('Encrypted content') . '
    ', $Text); + $text = preg_replace("/\[crypt\](.*?)\[\/crypt\]/ism", '
    ' . DI::l10n()->t('Encrypted content') . '
    ', $text); + $text = preg_replace("/\[crypt(.*?)\](.*?)\[\/crypt\]/ism", '
    ' . DI::l10n()->t('Encrypted content') . '
    ', $text); + //$Text = preg_replace("/\[crypt=(.*?)\](.*?)\[\/crypt\]/ism", '
    ' . DI::l10n()->t('Encrypted content') . '
    ', $Text); // Simplify "video" element $text = preg_replace('(\[video.*?\ssrc\s?=\s?([^\s\]]+).*?\].*?\[/video\])ism', '[video]$1[/video]', $text); - // Try to Oembed if ($try_oembed) { + // html5 video and audio $text = preg_replace("/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4).*?)\[\/video\]/ism", - '', $text); + '', $text); $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '$1', $text); - $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '', $text); + $text = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", '', $text); $text = preg_replace_callback("/\[video\](.*?)\[\/video\]/ism", $try_oembed_callback, $text); $text = preg_replace_callback("/\[audio\](.*?)\[\/audio\]/ism", $try_oembed_callback, $text); @@ -1632,9 +1619,6 @@ class BBCode '$1', $text); } - // html5 video and audio - - if ($try_oembed) { $text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '', $text); } else { @@ -1678,7 +1662,7 @@ class BBCode $text = OEmbed::BBCode2HTML($text); // Avoid triple linefeeds through oembed - $text = str_replace("


    ", "

    ", $text); + $text = str_replace("


    ", "

    ", $text); // If we found an event earlier, strip out all the event code and replace with a reformatted version. // Replace the event-start section with the entire formatted event. The other bbcode is stripped. @@ -1708,7 +1692,7 @@ class BBCode $conv = html_entity_decode(str_replace([' ', "\n", "\r"], '', $text)); // Emojis are always 4 byte Unicode characters if (!empty($conv) && (strlen($conv) / mb_strlen($conv) == 4)) { - $text = '' . $text . ''; + $text = '' . $text . ''; } } @@ -1722,8 +1706,6 @@ class BBCode $text = preg_replace_callback("&\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]&Usi", 'self::removePictureLinksCallback', $text); } - $text = str_replace(["\r","\n"], ['
    ', '
    '], $text); - // Remove all hashtag addresses if ($simple_html && !in_array($simple_html, [self::DIASPORA, self::OSTATUS, self::ACTIVITYPUB])) { $text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); @@ -1872,18 +1854,21 @@ class BBCode // Additionally, [pre] tags preserve spaces $text = preg_replace_callback("/\[pre\](.*?)\[\/pre\]/ism", function ($match) { - return str_replace(' ', ' ', $match[1]); + return str_replace(' ', ' ', htmlentities($match[1], ENT_NOQUOTES,'UTF-8')); }, $text); + // Add HTML new lines + $text = str_replace("\n", '
    ', $text); + return $text; }); // Escaped code $text = preg_replace_callback("#\[code(?:=([^\]]*))?\](.*?)\[\/code\]#ism", function ($matches) { if (strpos($matches[2], "\n") !== false) { - $return = '
    ' . htmlspecialchars(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '
    '; + $return = '
    ' . htmlentities(trim($matches[2], "\n\r"), ENT_NOQUOTES, 'UTF-8') . '
    '; } else { - $return = '' . htmlspecialchars($matches[2], ENT_NOQUOTES, 'UTF-8') . ''; + $return = '' . htmlentities($matches[2], ENT_NOQUOTES, 'UTF-8') . ''; } return $return; @@ -1891,37 +1876,20 @@ class BBCode $text ); - // Clean up the HTML by loading and saving the HTML with the DOM. - // Bad structured html can break a whole page. - // For performance reasons do it only with activated item cache or at export. - if (!$try_oembed || (get_itemcachepath() != '')) { - $doc = new DOMDocument(); - $doc->preserveWhiteSpace = false; + $config = \HTMLPurifier_HTML5Config::createDefault(); + $config->set('HTML.Doctype', 'HTML5'); + $config->set('Attr.AllowedRel', [ + 'noreferrer' => true, + 'noopener' => true, + ]); + $config->set('Attr.AllowedFrameTargets', [ + '_blank' => true, + ]); - $text = mb_convert_encoding($text, 'HTML-ENTITIES', "UTF-8"); + $HTMLPurifier = new \HTMLPurifier($config); + $text = $HTMLPurifier->purify($text); - $doctype = ''; - $encoding = ''; - @$doc->loadHTML($encoding . $doctype . '' . $text . ''); - $doc->encoding = 'UTF-8'; - $text = $doc->saveHTML(); - $text = str_replace(['', '', $doctype, $encoding], ['', '', '', ''], $text); - - $text = str_replace('
  • ', '', $text); - - //$Text = mb_convert_encoding($Text, "UTF-8", 'HTML-ENTITIES'); - } - - // Clean up some useless linebreaks in lists - //$Text = str_replace('

    ', '', $Text); - //$Text = str_replace('
    ', '', $Text); - //$Text = str_replace('
  • ', '
  • ', $Text); - //$Text = str_replace('
    = "a") && ($char <= "z")) { - $cleaned .= $char; - } - - if (!(strpos(" #;:0123456789-_.%", $char) === false)) { - $cleaned .= $char; - } - } - - return $cleaned; - } - /** * Search all instances of a specific HTML tag node in the provided DOM document and replaces them with BBCode text nodes. * diff --git a/src/Model/Item.php b/src/Model/Item.php index a8402a283..d41e84c5b 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -3529,20 +3529,21 @@ class Item */ public static function putInCache(&$item, $update = false) { - $body = $item["body"]; + // Save original body to prevent addons to modify it + $body = $item['body']; $rendered_hash = $item['rendered-hash'] ?? ''; $rendered_html = $item['rendered-html'] ?? ''; if ($rendered_hash == '' - || $rendered_html == "" - || $rendered_hash != hash("md5", $item["body"]) - || DI::config()->get("system", "ignore_cache") + || $rendered_html == '' + || $rendered_hash != hash('md5', BBCode::VERSION . '::' . $body) + || DI::config()->get('system', 'ignore_cache') ) { self::addRedirToImageTags($item); - $item["rendered-html"] = BBCode::convert($item["body"]); - $item["rendered-hash"] = hash("md5", $item["body"]); + $item['rendered-html'] = BBCode::convert($item['body']); + $item['rendered-hash'] = hash('md5', BBCode::VERSION . '::' . $body); $hook_data = ['item' => $item, 'rendered-html' => $item['rendered-html'], 'rendered-hash' => $item['rendered-hash']]; Hook::callAll('put_item_in_cache', $hook_data); @@ -3551,27 +3552,27 @@ class Item unset($hook_data); // Force an update if the generated values differ from the existing ones - if ($rendered_hash != $item["rendered-hash"]) { + if ($rendered_hash != $item['rendered-hash']) { $update = true; } // Only compare the HTML when we forcefully ignore the cache - if (DI::config()->get("system", "ignore_cache") && ($rendered_html != $item["rendered-html"])) { + if (DI::config()->get('system', 'ignore_cache') && ($rendered_html != $item['rendered-html'])) { $update = true; } - if ($update && !empty($item["id"])) { + if ($update && !empty($item['id'])) { self::update( [ - 'rendered-html' => $item["rendered-html"], - 'rendered-hash' => $item["rendered-hash"] + 'rendered-html' => $item['rendered-html'], + 'rendered-hash' => $item['rendered-hash'] ], - ['id' => $item["id"]] + ['id' => $item['id']] ); } } - $item["body"] = $body; + $item['body'] = $body; } /** diff --git a/src/Module/Debug/Babel.php b/src/Module/Debug/Babel.php index 5b89c5301..e33f03214 100644 --- a/src/Module/Debug/Babel.php +++ b/src/Module/Debug/Babel.php @@ -49,7 +49,7 @@ class Babel extends BaseModule if (!empty($_REQUEST['text'])) { switch (($_REQUEST['type'] ?? '') ?: 'bbcode') { case 'bbcode': - $bbcode = trim($_REQUEST['text']); + $bbcode = $_REQUEST['text']; $results[] = [ 'title' => DI::l10n()->t('Source input'), 'content' => visible_whitespace($bbcode) @@ -67,6 +67,11 @@ class Babel extends BaseModule 'content' => visible_whitespace($html) ]; + $results[] = [ + 'title' => DI::l10n()->t('BBCode::convert (hex)'), + 'content' => visible_whitespace(bin2hex($html)), + ]; + $results[] = [ 'title' => DI::l10n()->t('BBCode::convert'), 'content' => $html @@ -178,6 +183,25 @@ class Babel extends BaseModule 'content' => $html ]; + $config = \HTMLPurifier_Config::createDefault(); + $HTMLPurifier = new \HTMLPurifier($config); + $purified = $HTMLPurifier->purify($html); + + $results[] = [ + 'title' => DI::l10n()->t('HTML Purified (raw)'), + 'content' => visible_whitespace($purified), + ]; + + $results[] = [ + 'title' => DI::l10n()->t('HTML Purified (hex)'), + 'content' => visible_whitespace(bin2hex($purified)), + ]; + + $results[] = [ + 'title' => DI::l10n()->t('HTML Purified'), + 'content' => $purified, + ]; + $bbcode = Text\HTML::toBBCode($html); $results[] = [ 'title' => DI::l10n()->t('HTML::toBBCode'), diff --git a/tests/src/Content/Text/BBCodeTest.php b/tests/src/Content/Text/BBCodeTest.php index 1a1e05f45..1769e6fd7 100644 --- a/tests/src/Content/Text/BBCodeTest.php +++ b/tests/src/Content/Text/BBCodeTest.php @@ -75,6 +75,18 @@ class BBCodeTest extends MockedTest $this->dice->shouldReceive('create') ->with(BaseURL::class) ->andReturn($baseUrlMock); + + $config = \HTMLPurifier_HTML5Config::createDefault(); + $config->set('HTML.Doctype', 'HTML5'); + $config->set('Attr.AllowedRel', [ + 'noreferrer' => true, + 'noopener' => true, + ]); + $config->set('Attr.AllowedFrameTargets', [ + '_blank' => true, + ]); + + $this->HTMLPurifier = new \HTMLPurifier($config); } public function dataLinks() @@ -171,7 +183,7 @@ class BBCodeTest extends MockedTest public function testAutoLinking(string $data, bool $assertHTML) { $output = BBCode::convert($data); - $assert = '' . $data . ''; + $assert = $this->HTMLPurifier->purify('' . $data . ''); if ($assertHTML) { self::assertEquals($assert, $output); } else { @@ -183,31 +195,31 @@ class BBCodeTest extends MockedTest { return [ 'bug-7271-condensed-space' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ol][*] http://example.com/[/ol]', ], 'bug-7271-condensed-nospace' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ol][*]http://example.com/[/ol]', ], 'bug-7271-indented-space' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ul] [*] http://example.com/ [/ul]', ], 'bug-7271-indented-nospace' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[ul] [*]http://example.com/ [/ul]', ], 'bug-2199-named-size' => [ - 'expectedHtml' => 'Test text', + 'expectedHtml' => 'Test text', 'text' => '[size=xx-large]Test text[/size]', ], 'bug-2199-numeric-size' => [ - 'expectedHtml' => 'Test text', + 'expectedHtml' => 'Test text', 'text' => '[size=24]Test text[/size]', ], 'bug-2199-diaspora-no-named-size' => [ @@ -225,7 +237,7 @@ class BBCodeTest extends MockedTest 'simpleHtml' => 3, ], 'bug-7665-audio-tag' => [ - 'expectedHtml' => '', + 'expectedHtml' => '', 'text' => '[audio]http://www.cendrones.fr/colloque2017/jonathanbocquet.mp3[/audio]', 'try_oembed' => true, ], @@ -246,9 +258,25 @@ class BBCodeTest extends MockedTest 'text' => '[test] Space', ], 'task-8800-pre-spaces' => [ - 'expectedHtml' => '    Spaces', + 'expectedHtml' => '    Spaces', 'text' => '[pre] Spaces[/pre]', ], + 'bug-9611-purify-xss-nobb' => [ + 'expectedHTML' => 'dare to move your mouse here', + 'text' => '[nobb]dare to move your mouse here[/nobb]' + ], + 'bug-9611-purify-xss-noparse' => [ + 'expectedHTML' => 'dare to move your mouse here', + 'text' => '[noparse]dare to move your mouse here[/noparse]' + ], + 'bug-9611-purify-xss-attributes' => [ + 'expectedHTML' => 'dare to move your mouse here', + 'text' => '[color="onmouseover=alert(0) style="]dare to move your mouse here[/color]' + ], + 'bug-9611-purify-attributes-correct' => [ + 'expectedHTML' => 'dare to move your mouse here', + 'text' => '[color=FFFFFF]dare to move your mouse here[/color]' + ], ]; } diff --git a/view/templates/babel.tpl b/view/templates/babel.tpl index 57d17fea9..9b5a3d62a 100644 --- a/view/templates/babel.tpl +++ b/view/templates/babel.tpl @@ -24,9 +24,7 @@

    {{$result.title}}

    -
    - {{$result.content nofilter}} -
    +
    {{$result.content nofilter}}
    {{/foreach}}