diff --git a/CHANGELOG b/CHANGELOG index 0546900f7..9b354a962 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,7 @@ CHANGELOG Roundcube Webmail - Localized timezone selector (#4983) - Use 7bit encoding for ISO-2022-* charsets in sent mail (#5640) - 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 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) diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index 679e2924c..128578012 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -373,10 +373,11 @@ class rcube_utils * @param string CSS source code * @param string Container ID to use as prefix * @param bool Allow remote content + * @param string Prefix to be added to id/class identifier * * @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; $replacements = new rcube_string_replacer; @@ -432,23 +433,37 @@ class rcube_utils $last_pos = $pos2 - ($length - strlen($repl)); } - // remove html comments and add #container to each tag selector. - // also replace body definition because we also stripped off the tag - $source = preg_replace( - array( - '/(^\s*<\!--)|(-->\s*$)/m', - // (?!##str) below is to not match with ##str_replacement_0## - // from rcube_string_replacer used above, this is needed for - // cases like @media { body { position: fixed; } } (#5811) - '/(^\s*|,\s*|\}\s*|\{\s*)((?!##str)[a-z0-9\._#\*][a-z0-9\.\-_]*)/im', - '/'.preg_quote($container_id, '/').'\s+body/i', - ), - array( - '', - "\\1#$container_id \\2", - $container_id, - ), - $source); + // remove html comments + $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source); + + // add #container to each tag selector and prefix to id/class identifiers + if ($container_id || $prefix) { + // (?!##str) below is to not match with ##str_replacement_0## + // from rcube_string_replacer used above, this is needed for + // cases like @media { body { position: fixed; } } (#5811) + $regexp = '/(^\s*|,\s*|\}\s*|\{\s*)((?!##str)[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im'; + $callback = function($matches) use ($container_id, $prefix) { + $replace = $matches[2]; + + if ($prefix) { + $replace = str_replace(array('.', '#'), array(".$prefix", "#$prefix"), $replace); + } + + 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 tag + if ($container_id) { + $regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i'; + $source = preg_replace($regexp, "#$container_id", $source); + } // put block contents back in $source = $replacements->resolve($source); diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php index 0880764a0..d45a88087 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -132,9 +132,9 @@ class rcube_washtml 'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border', 'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace', 'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media', - 'background', 'src', 'poster', 'href', + 'background', 'src', 'poster', 'href', 'headers', // attributes of form elements - 'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value', + 'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value', 'for', // SVG 'accent-height', 'accumulate', 'additive', 'alignment-baseline', 'alphabetic', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseprofile', @@ -203,6 +203,9 @@ class rcube_washtml /* Allowed HTML attributes */ private $_html_attribs = array(); + /* A prefix to be added to id/class/for attribute values */ + private $_css_prefix; + /* 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->_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->_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']); @@ -338,6 +342,9 @@ class rcube_washtml $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) { $out = $value; } diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index eb6afd327..5e302c97b 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -875,6 +875,8 @@ function rcmail_wash_html($html, $p, $cid_replaces = array()) 'charset' => RCUBE_CHARSET, 'cid_map' => $cid_replaces, 'html_elements' => array('body'), + 'css_prefix' => $p['css_prefix'], + 'container_id' => $p['container_id'], ); if (!$p['inline_html']) { @@ -886,10 +888,12 @@ function rcmail_wash_html($html, $p, $cid_replaces = array()) } // overwrite washer options with options from plugins - if (isset($p['html_elements'])) + if (isset($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']; + } // initialize HTML washer $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('area', 'rcmail_washtml_link_callback'); - if ($p['safe']) + if ($p['safe']) { $washer->add_callback('link', 'rcmail_washtml_link_callback'); + } } // Remove non-UTF8 characters (#1487813) @@ -1314,11 +1319,17 @@ function rcmail_message_body($attrib) $container_id = $container_class . (++$part_no); $container_attrib = array('class' => $container_class, 'id' => $container_id); - // Assign container ID to a global variable for use in rcmail_washtml_link_callback() - $GLOBALS['rcmail_html_container_id'] = $container_id; + $body_args = array( + '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 - $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 if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) { @@ -1326,7 +1337,7 @@ function rcmail_message_body($attrib) } 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); @@ -1469,22 +1480,22 @@ function rcmail_part_image_type($part) /** * 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; - $cont_id = $container_id . ($body_class ? ' div.'.$body_class : ''); + $last_pos = 0; + $cont_id = $args['container_id'] . ($args['body_class'] ? ' div.' . $args['body_class'] : ''); // find STYLE tags - while (($pos = stripos($body, '', $pos))) { + while (($pos = stripos($body, '', $pos))) { $pos = strpos($body, '>', $pos) + 1; $len = $pos2 - $pos; // replace all css definitions with #container [def] $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); - $last_style_pos = $pos2 + strlen($styles) - $len; + $body = substr_replace($body, $styles, $pos, $len); + $last_pos = $pos2 + strlen($styles) - $len; } $body = preg_replace(array( @@ -1511,13 +1522,13 @@ function rcmail_html4inline($body, $container_id, $body_class='', &$attributes=a '', '<?', '?>', - '
', + '
', '
', ), $body); // Handle body attributes that doesn't play nicely with div elements - $regexp = '/
]*)/'; + $regexp = '/
]*)/'; if (preg_match($regexp, $body, $m)) { $style = array(); $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 // its name is hardcoded in rcmail_message_body() also else { - $body = '
' . $body . '
'; + $body = '
' . $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'])) { $tempurl = 'tmp-' . md5($attrib['href']) . '.css'; $_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 = ' />'; + $content = null; } else if (preg_match('/^mailto:(.+)/i', $attrib['href'], $mailto)) { list($mailto, $url) = explode('?', html_entity_decode($mailto[1], ENT_QUOTES, 'UTF-8'), 2); diff --git a/program/steps/utils/modcss.inc b/program/steps/utils/modcss.inc index 09e7c93fe..19fefe05e 100644 --- a/program/steps/utils/modcss.inc +++ b/program/steps/utils/modcss.inc @@ -72,10 +72,12 @@ else { } $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)) { 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; } diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php index 2a5c090d9..4022a3440 100644 --- a/tests/Framework/Utils.php +++ b/tests/Framework/Utils.php @@ -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]"); } + /** + * 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() { $mod = rcube_utils::xss_entity_decode("<img/src=x onerror=alert(1)// "); diff --git a/tests/Framework/Washtml.php b/tests/Framework/Washtml.php index 90cc477df..cddbf4bb6 100644 --- a/tests/Framework/Washtml.php +++ b/tests/Framework/Washtml.php @@ -369,4 +369,19 @@ class Framework_Washtml extends PHPUnit_Framework_TestCase $this->assertNotContains('onerror=alert(1)>', $washed); $this->assertContains('<p style="x:', $washed); } + + /** + * Test css_prefix feature + */ + function test_css_prefix() + { + $washer = new rcube_washtml(array('css_prefix' => 'test')); + + $html = '

'; + $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); + } } diff --git a/tests/MailFunc.php b/tests/MailFunc.php index 69b8a3517..8f951863a 100644 --- a/tests/MailFunc.php +++ b/tests/MailFunc.php @@ -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'); // 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('#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"); // 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('/