Fix css conflicts in user interface and e-mail content (#5891)

... by adding prefix to element/class identifiers
Also cleaned up some code and removed global variable use.
pull/6002/head
Aleksander Machniak 8 years ago
parent 403d8453c8
commit 3196d656db

@ -41,6 +41,7 @@ CHANGELOG Roundcube Webmail
- Localized timezone selector (#4983) - Localized timezone selector (#4983)
- Use 7bit encoding for ISO-2022-* charsets in sent mail (#5640) - Use 7bit encoding for ISO-2022-* charsets in sent mail (#5640)
- Handle inline images also inside multipart/mixed messages (#5905) - Handle inline images also inside multipart/mixed messages (#5905)
- Fix css conflicts in user interface and e-mail content (#5891)
- Fix duplicated signature when using Back button in Chrome (#5809) - Fix duplicated signature when using Back button in Chrome (#5809)
- Fix touch event issue on messages list in IE/Edge (#5781) - Fix touch event issue on messages list in IE/Edge (#5781)
- Fix so links over images are not removed in plain text signatures converted from HTML (#4473) - Fix so links over images are not removed in plain text signatures converted from HTML (#4473)

@ -373,10 +373,11 @@ class rcube_utils
* @param string CSS source code * @param string CSS source code
* @param string Container ID to use as prefix * @param string Container ID to use as prefix
* @param bool Allow remote content * @param bool Allow remote content
* @param string Prefix to be added to id/class identifier
* *
* @return string Modified CSS source * @return string Modified CSS source
*/ */
public static function mod_css_styles($source, $container_id, $allow_remote = false) public static function mod_css_styles($source, $container_id, $allow_remote = false, $prefix = '')
{ {
$last_pos = 0; $last_pos = 0;
$replacements = new rcube_string_replacer; $replacements = new rcube_string_replacer;
@ -432,23 +433,37 @@ class rcube_utils
$last_pos = $pos2 - ($length - strlen($repl)); $last_pos = $pos2 - ($length - strlen($repl));
} }
// remove html comments and add #container to each tag selector. // remove html comments
// also replace body definition because we also stripped off the <body> tag $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source);
$source = preg_replace(
array( // add #container to each tag selector and prefix to id/class identifiers
'/(^\s*<\!--)|(-->\s*$)/m', if ($container_id || $prefix) {
// (?!##str) below is to not match with ##str_replacement_0## // (?!##str) below is to not match with ##str_replacement_0##
// from rcube_string_replacer used above, this is needed for // from rcube_string_replacer used above, this is needed for
// cases like @media { body { position: fixed; } } (#5811) // cases like @media { body { position: fixed; } } (#5811)
'/(^\s*|,\s*|\}\s*|\{\s*)((?!##str)[a-z0-9\._#\*][a-z0-9\.\-_]*)/im', $regexp = '/(^\s*|,\s*|\}\s*|\{\s*)((?!##str)[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im';
'/'.preg_quote($container_id, '/').'\s+body/i', $callback = function($matches) use ($container_id, $prefix) {
), $replace = $matches[2];
array(
'', if ($prefix) {
"\\1#$container_id \\2", $replace = str_replace(array('.', '#'), array(".$prefix", "#$prefix"), $replace);
$container_id, }
),
$source); if ($container_id) {
$replace = "#$container_id " . $replace;
}
return str_replace($matches[2], $replace, $matches[0]);
};
$source = preg_replace_callback($regexp, $callback, $source);
}
// replace body definition because we also stripped off the <body> tag
if ($container_id) {
$regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i';
$source = preg_replace($regexp, "#$container_id", $source);
}
// put block contents back in // put block contents back in
$source = $replacements->resolve($source); $source = $replacements->resolve($source);

@ -132,9 +132,9 @@ class rcube_washtml
'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border', 'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border',
'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace', 'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace',
'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media', 'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media',
'background', 'src', 'poster', 'href', 'background', 'src', 'poster', 'href', 'headers',
// attributes of form elements // attributes of form elements
'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value', 'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value', 'for',
// SVG // SVG
'accent-height', 'accumulate', 'additive', 'alignment-baseline', 'alphabetic', 'accent-height', 'accumulate', 'additive', 'alignment-baseline', 'alphabetic',
'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseprofile', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseprofile',
@ -203,6 +203,9 @@ class rcube_washtml
/* Allowed HTML attributes */ /* Allowed HTML attributes */
private $_html_attribs = array(); private $_html_attribs = array();
/* A prefix to be added to id/class/for attribute values */
private $_css_prefix;
/* Max nesting level */ /* Max nesting level */
private $max_nesting_level; private $max_nesting_level;
@ -218,6 +221,7 @@ class rcube_washtml
$this->_html_attribs = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs); $this->_html_attribs = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs);
$this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements); $this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements);
$this->_void_elements = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements); $this->_void_elements = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements);
$this->_css_prefix = is_string($p['css_prefix']) && strlen($p['css_prefix']) ? $p['css_prefix'] : null;
unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']); unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']);
@ -338,6 +342,9 @@ class rcube_washtml
$out = $value; $out = $value;
} }
} }
else if ($this->_css_prefix !== null && in_array($key, array('id', 'class', 'for'))) {
$out = preg_replace('/(\S+)/', $this->_css_prefix . '\1', $value);
}
else if ($key) { else if ($key) {
$out = $value; $out = $value;
} }

@ -875,6 +875,8 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
'charset' => RCUBE_CHARSET, 'charset' => RCUBE_CHARSET,
'cid_map' => $cid_replaces, 'cid_map' => $cid_replaces,
'html_elements' => array('body'), 'html_elements' => array('body'),
'css_prefix' => $p['css_prefix'],
'container_id' => $p['container_id'],
); );
if (!$p['inline_html']) { if (!$p['inline_html']) {
@ -886,10 +888,12 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
} }
// overwrite washer options with options from plugins // overwrite washer options with options from plugins
if (isset($p['html_elements'])) if (isset($p['html_elements'])) {
$wash_opts['html_elements'] = $p['html_elements']; $wash_opts['html_elements'] = $p['html_elements'];
if (isset($p['html_attribs'])) }
if (isset($p['html_attribs'])) {
$wash_opts['html_attribs'] = $p['html_attribs']; $wash_opts['html_attribs'] = $p['html_attribs'];
}
// initialize HTML washer // initialize HTML washer
$washer = new rcube_washtml($wash_opts); $washer = new rcube_washtml($wash_opts);
@ -908,8 +912,9 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
$washer->add_callback('a', 'rcmail_washtml_link_callback'); $washer->add_callback('a', 'rcmail_washtml_link_callback');
$washer->add_callback('area', 'rcmail_washtml_link_callback'); $washer->add_callback('area', 'rcmail_washtml_link_callback');
if ($p['safe']) if ($p['safe']) {
$washer->add_callback('link', 'rcmail_washtml_link_callback'); $washer->add_callback('link', 'rcmail_washtml_link_callback');
}
} }
// Remove non-UTF8 characters (#1487813) // Remove non-UTF8 characters (#1487813)
@ -1314,11 +1319,17 @@ function rcmail_message_body($attrib)
$container_id = $container_class . (++$part_no); $container_id = $container_class . (++$part_no);
$container_attrib = array('class' => $container_class, 'id' => $container_id); $container_attrib = array('class' => $container_class, 'id' => $container_id);
// Assign container ID to a global variable for use in rcmail_washtml_link_callback() $body_args = array(
$GLOBALS['rcmail_html_container_id'] = $container_id; 'safe' => $safe_mode,
'plain' => !$RCMAIL->config->get('prefer_html'),
'css_prefix' => 'v' . $part_no,
'body_class' => 'rcmBody',
'container_id' => $container_id,
'container_attrib' => $container_attrib,
);
// Parse the part content for display // Parse the part content for display
$body = rcmail_print_body($body, $part, array('safe' => $safe_mode, 'plain' => !$RCMAIL->config->get('prefer_html'))); $body = rcmail_print_body($body, $part, $body_args);
// check if the message body is PGP encrypted // check if the message body is PGP encrypted
if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) { if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) {
@ -1326,7 +1337,7 @@ function rcmail_message_body($attrib)
} }
if ($part->ctype_secondary == 'html') { if ($part->ctype_secondary == 'html') {
$body = rcmail_html4inline($body, $container_id, 'rcmBody', $container_attrib, $safe_mode); $body = rcmail_html4inline($body, $body_args);
} }
$out .= html::div($container_attrib, $plugin['prefix'] . $body); $out .= html::div($container_attrib, $plugin['prefix'] . $body);
@ -1469,22 +1480,22 @@ function rcmail_part_image_type($part)
/** /**
* Modify a HTML message that it can be displayed inside a HTML page * Modify a HTML message that it can be displayed inside a HTML page
*/ */
function rcmail_html4inline($body, $container_id, $body_class='', &$attributes=array(), $allow_remote=false) function rcmail_html4inline($body, $args)
{ {
$last_style_pos = 0; $last_pos = 0;
$cont_id = $container_id . ($body_class ? ' div.'.$body_class : ''); $cont_id = $args['container_id'] . ($args['body_class'] ? ' div.' . $args['body_class'] : '');
// find STYLE tags // find STYLE tags
while (($pos = stripos($body, '<style', $last_style_pos)) && ($pos2 = stripos($body, '</style>', $pos))) { while (($pos = stripos($body, '<style', $last_pos)) && ($pos2 = stripos($body, '</style>', $pos))) {
$pos = strpos($body, '>', $pos) + 1; $pos = strpos($body, '>', $pos) + 1;
$len = $pos2 - $pos; $len = $pos2 - $pos;
// replace all css definitions with #container [def] // replace all css definitions with #container [def]
$styles = substr($body, $pos, $len); $styles = substr($body, $pos, $len);
$styles = rcube_utils::mod_css_styles($styles, $cont_id, $allow_remote); $styles = rcube_utils::mod_css_styles($styles, $cont_id, $args['safe'], $args['css_prefix']);
$body = substr_replace($body, $styles, $pos, $len); $body = substr_replace($body, $styles, $pos, $len);
$last_style_pos = $pos2 + strlen($styles) - $len; $last_pos = $pos2 + strlen($styles) - $len;
} }
$body = preg_replace(array( $body = preg_replace(array(
@ -1511,13 +1522,13 @@ function rcmail_html4inline($body, $container_id, $body_class='', &$attributes=a
'<!--\\1-->', '<!--\\1-->',
'&lt;?', '&lt;?',
'?&gt;', '?&gt;',
'<div class="' . $body_class . '"\\1>', '<div class="' . $args['body_class'] . '"\\1>',
'</div>', '</div>',
), ),
$body); $body);
// Handle body attributes that doesn't play nicely with div elements // Handle body attributes that doesn't play nicely with div elements
$regexp = '/<div class="' . preg_quote($body_class, '/') . '"([^>]*)/'; $regexp = '/<div class="' . preg_quote($args['body_class'], '/') . '"([^>]*)/';
if (preg_match($regexp, $body, $m)) { if (preg_match($regexp, $body, $m)) {
$style = array(); $style = array();
$attrs = $m[0]; $attrs = $m[0];
@ -1563,7 +1574,7 @@ function rcmail_html4inline($body, $container_id, $body_class='', &$attributes=a
// make sure there's 'rcmBody' div, we need it for proper css modification // make sure there's 'rcmBody' div, we need it for proper css modification
// its name is hardcoded in rcmail_message_body() also // its name is hardcoded in rcmail_message_body() also
else { else {
$body = '<div class="' . $body_class . '">' . $body . '</div>'; $body = '<div class="' . $args['body_class'] . '">' . $body . '</div>';
} }
return $body; return $body;
@ -1586,8 +1597,15 @@ function rcmail_washtml_link_callback($tag, $attribs, $content, $washtml)
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) { if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) {
$tempurl = 'tmp-' . md5($attrib['href']) . '.css'; $tempurl = 'tmp-' . md5($attrib['href']) . '.css';
$_SESSION['modcssurls'][$tempurl] = $attrib['href']; $_SESSION['modcssurls'][$tempurl] = $attrib['href'];
$attrib['href'] = $RCMAIL->url(array('task' => 'utils', 'action' => 'modcss', 'u' => $tempurl, 'c' => $GLOBALS['rcmail_html_container_id'])); $attrib['href'] = $RCMAIL->url(array(
'task' => 'utils',
'action' => 'modcss',
'u' => $tempurl,
'c' => $washtml->get_config('container_id'),
'p' => $washtml->get_config('css_prefix'),
));
$end = ' />'; $end = ' />';
$content = null;
} }
else if (preg_match('/^mailto:(.+)/i', $attrib['href'], $mailto)) { else if (preg_match('/^mailto:(.+)/i', $attrib['href'], $mailto)) {
list($mailto, $url) = explode('?', html_entity_decode($mailto[1], ENT_QUOTES, 'UTF-8'), 2); list($mailto, $url) = explode('?', html_entity_decode($mailto[1], ENT_QUOTES, 'UTF-8'), 2);

@ -72,10 +72,12 @@ else {
} }
$ctype_regexp = '~Content-Type:\s+text/(css|plain)~i'; $ctype_regexp = '~Content-Type:\s+text/(css|plain)~i';
$container_id = preg_replace('/[^a-z0-9]/i', '', $_GET['_c']);
$css_prefix = preg_replace('/[^a-z0-9]/i', '', $_GET['_p']);
if ($source !== false && preg_match($ctype_regexp, $headers)) { if ($source !== false && preg_match($ctype_regexp, $headers)) {
header('Content-Type: text/css'); header('Content-Type: text/css');
echo rcube_utils::mod_css_styles($source, preg_replace('/[^a-z0-9]/i', '', $_GET['_c'])); echo rcube_utils::mod_css_styles($source, $container, false, $css_prefix);
exit; exit;
} }

@ -223,6 +223,37 @@ class Framework_Utils extends PHPUnit_Framework_TestCase
$this->assertContains("#rcmbody { background-image: url(data:image/png;base64,123);", $mod, "Data URIs in url() allowed [2]"); $this->assertContains("#rcmbody { background-image: url(data:image/png;base64,123);", $mod, "Data URIs in url() allowed [2]");
} }
/**
* rcube_utils::mod_css_styles()'s prefix argument handling
*/
function test_mod_css_styles_prefix()
{
$css = '
.one { font-size: 10pt; }
.three.four { font-weight: bold; }
#id1 { color: red; }
#id2.class:focus { color: white; }
.five:not(.test), { background: transparent; }
div .six { position: absolute; }
p > i { font-size: 120%; }
div#some { color: yellow; }
@media screen and (max-width: 699px) and (min-width: 520px) {
li a.button { padding-left: 30px; }
}
';
$mod = rcube_utils::mod_css_styles($css, 'rc', true, 'test');
$this->assertContains('#rc .testone', $mod);
$this->assertContains('#rc .testthree.testfour', $mod);
$this->assertContains('#rc #testid1', $mod);
$this->assertContains('#rc #testid2.testclass:focus', $mod);
$this->assertContains('#rc .testfive:not(.testtest)', $mod);
$this->assertContains('#rc div .testsix', $mod);
$this->assertContains('#rc p > i ', $mod);
$this->assertContains('#rc div#testsome', $mod);
$this->assertContains('#rc li a.testbutton', $mod);
}
function test_xss_entity_decode() function test_xss_entity_decode()
{ {
$mod = rcube_utils::xss_entity_decode("&lt;img/src=x onerror=alert(1)// </b>"); $mod = rcube_utils::xss_entity_decode("&lt;img/src=x onerror=alert(1)// </b>");

@ -369,4 +369,19 @@ class Framework_Washtml extends PHPUnit_Framework_TestCase
$this->assertNotContains('onerror=alert(1)>', $washed); $this->assertNotContains('onerror=alert(1)>', $washed);
$this->assertContains('&lt;p style=&quot;x:', $washed); $this->assertContains('&lt;p style=&quot;x:', $washed);
} }
/**
* Test css_prefix feature
*/
function test_css_prefix()
{
$washer = new rcube_washtml(array('css_prefix' => 'test'));
$html = '<p id="my-id"><label for="my-other-id" class="my-class1 my-class2">test</label></p>';
$washed = $washer->wash($html);
$this->assertContains('id="testmy-id"', $washed);
$this->assertContains('for="testmy-other-id"', $washed);
$this->assertContains('class="testmy-class1 testmy-class2"', $washed);
}
} }

@ -42,7 +42,7 @@ class MailFunc extends PHPUnit_Framework_TestCase
$part->replaces = array('ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg'); $part->replaces = array('ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg');
// render HTML in normal mode // render HTML in normal mode
$html = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => false)), 'foo'); $html = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => false)), array('container_id' => 'foo'));
$this->assertRegExp('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image"); $this->assertRegExp('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image");
$this->assertRegExp('#background="program/resources/blocked.gif"#', $html, "Replace external background image"); $this->assertRegExp('#background="program/resources/blocked.gif"#', $html, "Replace external background image");
@ -56,7 +56,7 @@ class MailFunc extends PHPUnit_Framework_TestCase
$this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected"); $this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected");
// render HTML in safe mode // render HTML in safe mode
$html2 = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => true)), 'foo'); $html2 = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => true)), array('container_id' => 'foo'));
$this->assertRegExp('/<style [^>]+>/', $html2, "Allow styles in safe mode"); $this->assertRegExp('/<style [^>]+>/', $html2, "Allow styles in safe mode");
$this->assertRegExp('#src="http://evilsite.net/mailings/ex3.jpg"#', $html2, "Allow external images in HTML (safe mode)"); $this->assertRegExp('#src="http://evilsite.net/mailings/ex3.jpg"#', $html2, "Allow external images in HTML (safe mode)");
@ -76,7 +76,7 @@ class MailFunc extends PHPUnit_Framework_TestCase
$this->assertNotRegExp('/src="skins/', $washed, "Remove local references"); $this->assertNotRegExp('/src="skins/', $washed, "Remove local references");
$this->assertNotRegExp('/\son[a-z]+/', $washed, "Remove on* attributes"); $this->assertNotRegExp('/\son[a-z]+/', $washed, "Remove on* attributes");
$html = rcmail_html4inline($washed, 'foo'); $html = rcmail_html4inline($washed, array('container_id' => 'foo'));
$this->assertNotRegExp('/onclick="return rcmail.command(\'compose\',\'xss@somehost.net\',this)"/', $html, "Clean mailto links"); $this->assertNotRegExp('/onclick="return rcmail.command(\'compose\',\'xss@somehost.net\',this)"/', $html, "Clean mailto links");
$this->assertNotRegExp('/alert/', $html, "Remove alerts"); $this->assertNotRegExp('/alert/', $html, "Remove alerts");
} }
@ -88,7 +88,8 @@ class MailFunc extends PHPUnit_Framework_TestCase
function test_html_xss2() function test_html_xss2()
{ {
$part = $this->get_html_part('src/BID-26800.txt'); $part = $this->get_html_part('src/BID-26800.txt');
$washed = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => true)), 'dabody', '', $attr, true); $washed = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => true)),
array('container_id' => 'dabody', 'safe' => true));
$this->assertNotRegExp('/alert|expression|javascript|xss/', $washed, "Remove evil style blocks"); $this->assertNotRegExp('/alert|expression|javascript|xss/', $washed, "Remove evil style blocks");
$this->assertNotRegExp('/font-style:italic/', $washed, "Allow valid styles"); $this->assertNotRegExp('/font-style:italic/', $washed, "Allow valid styles");
@ -145,7 +146,7 @@ class MailFunc extends PHPUnit_Framework_TestCase
$part = $this->get_html_part('src/mailto.txt'); $part = $this->get_html_part('src/mailto.txt');
// render HTML in normal mode // render HTML in normal mode
$html = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => false)), 'foo'); $html = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => false)), array('container_id' => 'foo'));
$mailto = '<a href="mailto:me@me.com"' $mailto = '<a href="mailto:me@me.com"'
.' onclick="return rcmail.command(\'compose\',\'me@me.com?subject=this is the subject&amp;body=this is the body\',this)" rel="noreferrer">e-mail</a>'; .' onclick="return rcmail.command(\'compose\',\'me@me.com?subject=this is the subject&amp;body=this is the body\',this)" rel="noreferrer">e-mail</a>';

Loading…
Cancel
Save