Merge branch 'master' into dev-elastic

pull/6040/head
Aleksander Machniak 7 years ago
commit d815525c6a

@ -1,10 +1,13 @@
CHANGELOG Roundcube Webmail
===========================
- Handle remote stylesheets the same as remote images, ask the user to allow them (#5994)
- Add Message-ID to the sendmail log (#5871)
- Managesieve: Add ability to disable filter sets and other actions (#5496, #5898)
- Changed defaults for smtp_user (%u), smtp_pass (%p) and smtp_port (587)
- Composer: Fix certificate validation errors by using packagist only (#5148)
- Enigma: Add button to send mail unencrypted if no key was found (#5913)
- Enigma: Add options to set PGP cipher/digest algorithms (#5645)
- Add --get and --extract arguments and CACHEDIR env-variable support to install-jsdeps.sh (#5882)
- Update to jquery-minicolors 2.2.6
- Support _filter and _scope as GET arguments for opening mail UI (#5825)
@ -39,6 +42,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)
@ -51,6 +55,15 @@ CHANGELOG Roundcube Webmail
- Enigma: Fix decryption of messages encoded with non-ascii charset (#5962)
- Fix missing cursor in HTML editor on mail reply (#5969)
- Fix (again) bug where image data URIs in css style were treated as evil/remote in mail preview (#5580)
- Fix bug where mail search could return empty result on servers without SORT capability (#5973)
- Fix bug where assets_path wasn't added to some watermark frames
- Fix so untagged COPYUID responses are also supported according to RFC6851 (#5982)
- Fix issue caused by non-default session.cookie_lifetime setting (#5961)
- Fix Edge encoding bug when pasting text into the HTML editor, update to TinyMCE 4.5.8 (#5885)
- Fix handling of unknown Content-Disposition type (#6002)
- Fix truncated folder name on messages list in multi-folder mode, for folders with non-ascii characters (#6004)
- Fix bug where removing the last subfolder did not hide toggle button on its parent record (#6007)
- Fix bug where ghost messages could be added to the list after fast delete (#5941)
RELEASE 1.3.1
-------------

@ -24,7 +24,7 @@ REQUIREMENTS
- Net_IDNA2 0.1.1 or newer
- Auth_SASL 1.0.6 or newer
- Net_Sieve 1.3.2 or newer (for managesieve plugin)
- Crypt_GPG 1.6.0 or newer (for enigma plugin)
- Crypt_GPG 1.6.2 or newer (for enigma plugin)
- Endroid/QrCode 1.6.0 or newer (https://github.com/endroid/QrCode)
* php.ini options (see .htaccess file):
- error_reporting E_ALL & ~E_NOTICE & ~E_STRICT

@ -16,7 +16,7 @@
"pear/net_idna2": "~0.2.0",
"pear/mail_mime": "~1.10.0",
"pear/net_smtp": "~1.7.1",
"pear/crypt_gpg": "~1.6.0",
"pear/crypt_gpg": "~1.6.2",
"pear/net_sieve": "~1.4.0",
"roundcube/plugin-installer": "~0.1.6",
"endroid/qrcode": "~1.6.5"

@ -1064,10 +1064,10 @@ $config['timezone'] = 'auto';
// prefer displaying HTML messages
$config['prefer_html'] = true;
// display remote inline images
// display remote resources (inline images, styles)
// 0 - Never, always ask
// 1 - Ask if sender is not in address book
// 2 - Always show inline images
// 2 - Always allow
$config['show_images'] = 0;
// open messages in new window

@ -35,10 +35,10 @@
},
{
"lib": "tinymce",
"version": "4.5.7",
"url": "http://download.ephox.com/tinymce/community/tinymce_4.5.7.zip",
"version": "4.5.8",
"url": "http://download.ephox.com/tinymce/community/tinymce_4.5.8.zip",
"dest": "program/js",
"sha1": "4e86907c4748f7f75072e173ff3eee08700b9594",
"sha1": "08b0757264adb86066940bbafb7aa9ec0c7c6685",
"license": "LGPL",
"copyright": "Copyright (c) 1999-2015 Ephox Corp. All rights reserved",
"rm": "program/js/tinymce",
@ -55,7 +55,7 @@
},
{
"lib": "tinymce-langs",
"version": "4.5.7",
"version": "4.5.8",
"url": "https://tinymce-services.azurewebsites.net/1/i18n/download?langs=ar,hy,az,eu,be,bs,bg_BG,ca,zh_CN,zh_TW,hr,cs,cs_CZ,da,nl,en_CA,en_GB,eo,et,fo,fi,fr_FR,fr_CH,gd,gl,ka_GE,de,de_AT,el,he_IL,hi_IN,hu_HU,is_IS,id,ga,it,ja,kab,km_KH,ko_KR,ku,ku_IQ,lv,lt,lb,mk_MK,ml_IN,nb_NO,oc,fa,fa_IR,pl,pt_BR,pt_PT,ro,ru,sk,sl_SI,es,es_MX,sv_SE,tg,ta,ta_IN,tt,th_TH,tr,tr_TR,ug,uk,uk_UA,vi,vi_VN,cy",
"dest": "program/js/tinymce"
},

@ -20,6 +20,6 @@
"require": {
"php": ">=5.3.0",
"roundcube/plugin-installer": "~0.1.6",
"pear/crypt_gpg": "~1.6.0"
"pear/crypt_gpg": "~1.6.2"
}
}

@ -28,6 +28,14 @@ $config['enigma_pgp_agent'] = '';
// It's used with GnuPG >= 2.1.
$config['enigma_pgp_gpgconf'] = '';
// Name of the PGP symmetric cipher algorithm.
// Run gpg --version to see the list of supported algorithms
$config['enigma_pgp_cipher_algo'] = null;
// Name of the PGP digest (hash) algorithm.
// Run gpg --version to see the list of supported algorithms
$config['enigma_pgp_digest_algo'] = null;
// Enables signatures verification feature.
$config['enigma_signatures'] = true;

@ -94,6 +94,9 @@ class enigma_driver_gnupg extends enigma_driver
$options['gpgconf'] = $gpgconf;
}
$options['cipher-algo'] = $this->rc->config->get('enigma_pgp_cipher_algo');
$options['digest-algo'] = $this->rc->config->get('enigma_pgp_digest_algo');
// Create Crypt_GPG object
try {
$this->gpg = new Crypt_GPG($options);

@ -1156,10 +1156,13 @@ EOF;
// we are calling a class/method
if (($handler = $this->object_handlers[$object]) && is_array($handler)) {
if ((is_object($handler[0]) && method_exists($handler[0], $handler[1])) ||
(is_string($handler[0]) && class_exists($handler[0])))
$content = call_user_func($handler, $attrib);
$external = true;
if (is_callable($handler)) {
// We assume that objects with src attribute are internal (in most
// cases this is a watermark frame). We need this to make sure assets_path
// is added to the internal assets paths
$external = empty($attrib['src']);
$content = call_user_func($handler, $attrib);
}
}
// execute object handler function
else if (function_exists($handler)) {

@ -284,7 +284,7 @@ function rcube_webmail()
if (this.env.blockedobjects) {
$(this.gui_objects.remoteobjectsmsg).show();
this.enable_command('load-images', 'always-load', true);
this.enable_command('load-remote', true);
}
// make preview/message frame visible
@ -1028,16 +1028,15 @@ function rcube_webmail()
break;
case 'always-load':
if (this.env.uid && this.env.sender) {
this.add_contact(this.env.sender);
setTimeout(function(){ ref.command('load-images'); }, 300);
break;
}
case 'load-remote':
if (this.env.uid) {
if (props && this.env.sender) {
this.add_contact(this.env.sender, true);
break;
}
case 'load-images':
if (this.env.uid)
this.show_message(this.env.uid, true, this.env.action=='preview');
this.show_message(this.env.uid, true, this.env.action == 'preview');
}
break;
case 'load-attachment':
@ -2084,6 +2083,11 @@ function rcube_webmail()
if (flags.mbox != this.env.mailbox && !flags.skip_mbox_check)
return false;
// When deleting messages fast it may happen that the same message
// from the next page could be added many times, we prevent this here
if (this.message_list.rows[uid])
return false;
if (!this.env.messages[uid])
this.env.messages[uid] = {};
@ -2850,13 +2854,14 @@ function rcube_webmail()
// update thread indicators for all messages in a thread below the specified message
// return number of removed/added root level messages
this.update_thread = function (uid)
this.update_thread = function(uid)
{
if (!this.env.threading)
if (!this.env.threading || !this.message_list.rows[uid])
return 0;
var r, parent, count = 0,
rows = this.message_list.rows,
list = this.message_list,
rows = list.rows,
row = rows[uid],
depth = rows[uid].depth,
roots = [];
@ -2866,14 +2871,14 @@ function rcube_webmail()
// update unread_children for thread root
if (row.depth && row.unread) {
parent = this.message_list.find_root(uid);
parent = list.find_root(uid);
rows[parent].unread_children--;
this.set_unread_children(parent);
}
// update unread_children for thread root
if (row.depth && row.flagged) {
parent = this.message_list.find_root(uid);
parent = list.find_root(uid);
rows[parent].flagged_children--;
this.set_flagged_children(parent);
}
@ -5282,10 +5287,10 @@ function rcube_webmail()
};
// send remote request to add a new contact
this.add_contact = function(value)
this.add_contact = function(value, reload)
{
if (value)
this.http_post('addcontact', {_address: value});
this.http_post('addcontact', {_address: value, _reload: reload});
return true;
};
@ -5293,7 +5298,10 @@ function rcube_webmail()
// send remote request to search mail or contacts
this.qsearch = function(value)
{
if (value || $(this.gui_objects.qsearchbox).val() || $(this.gui_objects.search_interval).val()) {
// Note: Some plugins would like to do search without value,
// so we keep value != '' check to allow that use-case. Which means
// e.g. that qsearch() with no argument will execute the search.
if (value != '' || $(this.gui_objects.qsearchbox).val() || $(this.gui_objects.search_interval).val()) {
var r, lock = this.set_busy(true, 'searching'),
url = this.search_params(value),
action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';

@ -39,7 +39,7 @@ function rcube_text_editor(config, id)
abs_url = location.href.replace(/[?#].*$/, '').replace(/\/$/, ''),
conf = {
selector: '#' + ($('#' + id).is('.mce_editor') ? id : 'fake-editor-id'),
cache_suffix: 's=4050700',
cache_suffix: 's=4050800',
theme: 'modern',
language: config.lang,
content_css: rcmail.assets_path('program/resources/tinymce/content.css'),

@ -467,10 +467,11 @@ function rcube_treelist_widget(node, p)
*/
function remove(id)
{
var node, li;
var node, li, parent;
if (node = indexbyid[id]) {
li = id2dom(id, true);
parent = li.parent();
li.remove();
node.deleted = true;
@ -480,6 +481,12 @@ function rcube_treelist_widget(node, p)
id2dom(id, false).remove();
}
// remove tree-toggle button and children list
if (!parent.children().length) {
parent.parent().find('div.treetoggle').remove();
parent.remove();
}
return true;
}

@ -515,6 +515,8 @@ class rcube
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
// set session cookie lifetime so it never expires (#5961)
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_secure', $is_secure);
ini_set('session.name', $sess_name ?: 'roundcube_sessid');
ini_set('session.use_cookies', 1);
@ -1686,9 +1688,10 @@ class rcube
$mailto = implode(',', $a_recipients);
$mailto = rcube_mime::decode_address_list($mailto, null, false, null, true);
self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
self::write_log('sendmail', sprintf("User %s [%s]; Message %s for %s; %s",
$this->user->get_username(),
rcube_utils::remote_addr(),
$headers['Message-ID'],
implode(', ', $mailto),
!empty($response) ? join('; ', $response) : ''));
}

@ -1390,8 +1390,7 @@ class rcube_imap extends rcube_storage
$index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
}
else {
$index = $this->index_direct($folder, $this->search_charset,
$this->sort_field, $this->search_set);
$index = $this->index_direct($folder, $this->sort_field, $this->sort_order, $this->search_set);
}
}
@ -2110,7 +2109,10 @@ class rcube_imap extends rcube_storage
if (is_array($part[$di]) && count($part[$di]) == 2) {
$struct->disposition = strtolower($part[$di][0]);
if ($struct->disposition && $struct->disposition !== 'inline' && $struct->disposition !== 'attachment') {
// RFC2183, Section 2.8 - unrecognized type should be treated as "attachment"
$struct->disposition = 'attachment';
}
if (is_array($part[$di][1])) {
for ($n=0; $n<count($part[$di][1]); $n+=2) {
$struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];

@ -3709,9 +3709,17 @@ class rcube_imap_generic
// Parse response
do {
$line = $this->readLine(4096);
if ($response !== null) {
$response .= $line;
}
// parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851)
if ($line && $command == 'UID MOVE' && substr_compare($line, '* OK', 0, 4, true)) {
if (preg_match("/^\* OK \[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $line, $m)) {
$this->data['COPYUID'] = array($m[1], $m[2]);
}
}
}
while (!$this->startsWith($line, $tag . ' ', true, true));

@ -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 <body> 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 <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
$source = $replacements->resolve($source);

@ -55,7 +55,7 @@
* It return a sanityzed string of the $html parameter without html and head tags.
* $html is a string containing the html code to wash.
* $config is an array containing options:
* $config['allow_remote'] is a boolean to allow link to remote images.
* $config['allow_remote'] is a boolean to allow link to remote resources (images/css).
* $config['blocked_src'] string with image-src to be used for blocked remote images
* $config['show_washed'] is a boolean to include washed out attributes as x-washed
* $config['cid_map'] is an array where cid urls index urls to replace them.
@ -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;
}
@ -364,7 +371,7 @@ class rcube_washtml
/**
* Wash URI value
*/
private function wash_uri($uri, $blocked_source = false)
private function wash_uri($uri, $blocked_source = false, $is_image = true)
{
if (($src = $this->config['cid_map'][$uri])
|| ($src = $this->config['cid_map'][$this->config['base_url'].$uri])
@ -383,11 +390,11 @@ class rcube_washtml
}
$this->extlinks = true;
if ($blocked_source && $this->config['blocked_src']) {
if ($is_image && $blocked_source && $this->config['blocked_src']) {
return $this->config['blocked_src'];
}
}
else if (preg_match('/^data:image.+/i', $uri)) { // RFC2397
else if ($is_image && preg_match('/^data:image.+/i', $uri)) { // RFC2397
return $uri;
}
}
@ -455,6 +462,17 @@ class rcube_washtml
switch ($node->nodeType) {
case XML_ELEMENT_NODE: //Check element
$tagName = strtolower($node->nodeName);
if ($tagName == 'link') {
$uri = $this->wash_uri($node->getAttribute('href'), false, false);
if (!$uri) {
$dump .= '<!-- link ignored -->';
break;
}
$node->setAttribute('href', (string) $uri);
}
if ($callback = $this->handlers[$tagName]) {
$dump .= call_user_func($callback, $tagName,
$this->wash_attribs($node), $this->dumpHtml($node, $level), $this);

@ -324,7 +324,9 @@ $labels['highest'] = 'Highest';
$labels['nosubject'] = '(no subject)';
$labels['showimages'] = 'Display images';
$labels['allow'] = 'Allow';
$labels['alwaysshow'] = 'Always show images from $sender';
$labels['alwaysallow'] = 'Always allow from $sender';
$labels['isdraft'] = 'This is a draft message.';
$labels['andnmore'] = '$nr more...';
$labels['togglemoreheaders'] = 'Show more message headers';
@ -513,6 +515,7 @@ $labels['skipdeleted'] = 'Do not show deleted messages';
$labels['deletealways'] = 'If moving messages to Trash fails, delete them';
$labels['deletejunk'] = 'Directly delete messages in Junk';
$labels['showremoteimages'] = 'Display remote inline images';
$labels['allowremoteresources'] = 'Allow remote resources (images, styles)';
$labels['fromknownsenders'] = 'from known senders';
$labels['always'] = 'always';
$labels['alwaysbutplain'] = 'always, except when replying to plain text';

@ -57,6 +57,7 @@ $messages['addedsuccessfully'] = 'Contact added successfully to address book.';
$messages['contactexists'] = 'A contact with the same email address already exists.';
$messages['contactnameexists'] = 'A contact with the same name already exists.';
$messages['blockedimages'] = 'To protect your privacy, remote images are blocked in this message.';
$messages['blockedresources'] = 'To protect your privacy remote resources have been blocked.';
$messages['encryptedmessage'] = 'This is an encrypted message and can not be displayed. Sorry!';
$messages['externalmessagedecryption'] = 'This is an encrypted message and can be decrypted with your browser extension.';
$messages['nopubkeyfor'] = 'No valid public key found for $email';

@ -81,6 +81,10 @@ if (!empty($_POST['_address']) && is_object($CONTACTS)) {
if ($done) {
$OUTPUT->show_message('addedsuccessfully', 'confirmation');
if (!empty($_POST['_reload'])) {
$OUTPUT->command('command', 'load-remote');
}
}
}
}

@ -487,8 +487,9 @@ function rcmail_js_message_list($a_headers, $insert_top=false, $a_show_cols=null
// loop through message headers
foreach ($a_headers as $header) {
if (empty($header))
if (empty($header) || !$header->size) {
continue;
}
// make message UIDs unique by appending the folder name
if ($multifolder) {
@ -521,8 +522,7 @@ function rcmail_js_message_list($a_headers, $insert_top=false, $a_show_cols=null
else if ($col == 'folder') {
if ($last_folder !== $header->folder) {
$last_folder = $header->folder;
$last_folder_name = rcube_charset::convert($last_folder, 'UTF7-IMAP');
$last_folder_name = $RCMAIL->localize_foldername($last_folder_name, true);
$last_folder_name = $RCMAIL->localize_foldername($last_folder, true);
$last_folder_name = str_replace($delimiter, " \xC2\xBB ", $last_folder_name);
}
@ -872,21 +872,24 @@ 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']) {
$wash_opts['html_elements'] = array('html','head','title','body');
$wash_opts['html_elements'] = array('html','head','title','body','link');
}
if ($p['safe']) {
$wash_opts['html_elements'][] = 'link';
$wash_opts['html_attribs'] = array('rel','type');
}
// 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);
@ -904,9 +907,7 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
if (!$p['skip_washer_link_callback']) {
$washer->add_callback('a', 'rcmail_washtml_link_callback');
$washer->add_callback('area', 'rcmail_washtml_link_callback');
if ($p['safe'])
$washer->add_callback('link', 'rcmail_washtml_link_callback');
$washer->add_callback('link', 'rcmail_washtml_link_callback');
}
// Remove non-UTF8 characters (#1487813)
@ -1089,22 +1090,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, '<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;
$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(
@ -1131,13 +1132,13 @@ function rcmail_html4inline($body, $container_id, $body_class='', &$attributes=a
'<!--\\1-->',
'&lt;?',
'?&gt;',
'<div class="' . $body_class . '"\\1>',
'<div class="' . $args['body_class'] . '"\\1>',
'</div>',
),
$body);
// 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)) {
$style = array();
$attrs = $m[0];
@ -1183,7 +1184,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 = '<div class="' . $body_class . '">' . $body . '</div>';
$body = '<div class="' . $args['body_class'] . '">' . $body . '</div>';
}
return $body;
@ -1206,8 +1207,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);

@ -246,21 +246,21 @@ function rcmail_remote_objects_msg()
$attrib['class'] = 'notice';
$attrib['style'] = 'display: none';
$msg = html::span('', rcube::Q($RCMAIL->gettext('blockedimages'))) . '&nbsp;';
$msg .= html::a(array(
'href' => "#loadimages",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('load-images')"
$msg = html::span('', rcube::Q($RCMAIL->gettext('blockedresources'))) . '&nbsp;'
. html::a(array(
'href' => "#loadremote",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('load-remote')"
),
rcube::Q($RCMAIL->gettext('showimages')));
rcube::Q($RCMAIL->gettext('allow')));
// add link to save sender in addressbook and reload message
if ($MESSAGE->sender['mailto'] && $RCMAIL->config->get('show_images') == 1) {
$msg .= ' ' . html::a(array(
'href' => "#alwaysload",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('always-load')",
'href' => "#loadremotealways",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('load-remote', true)",
'style' => "white-space:nowrap"
),
rcube::Q($RCMAIL->gettext(array('name' => 'alwaysshow', 'vars' => array('sender' => $MESSAGE->sender['mailto'])))));
rcube::Q($RCMAIL->gettext(array('name' => 'alwaysallow', 'vars' => array('sender' => $MESSAGE->sender['mailto'])))));
}
$RCMAIL->output->add_gui_object('remoteobjectsmsg', $attrib['id']);

@ -607,7 +607,7 @@ function rcmail_user_prefs($current = null)
$input->add($RCMAIL->gettext('always'), 2);
$blocks['main']['options']['show_images'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('showremoteimages'))),
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('allowremoteresources'))),
'content' => $input->show($config['prefer_html'] ? $config['show_images'] : 0),
);
}

@ -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;
}

@ -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("&lt;img/src=x onerror=alert(1)// </b>");

@ -359,6 +359,33 @@ class Framework_Washtml extends PHPUnit_Framework_TestCase
$this->assertNotContains('TRACKING', $washed, "Src attribute of <video> tag (#5583)");
}
/**
* Test external links
*/
function test_extlinks()
{
$html = array(
array("<link href=\"http://TRACKING_URL/\">", true),
array("<link href=\"src:abc\">", false),
array("<img src=\"http://TRACKING_URL/\">", true),
array("<img src=\"data:image\">", false),
);
foreach ($html as $item) {
$washer = new rcube_washtml;
$washed = $washer->wash($item[0]);
$this->assertSame($item[1], $washer->extlinks);
}
foreach ($html as $item) {
$washer = new rcube_washtml(array('allow_remote' => true));
$washed = $washer->wash($item[0]);
$this->assertFalse($washer->extlinks);
}
}
function test_textarea_content_escaping()
{
$html = '<textarea><p style="x:</textarea><img src=x onerror=alert(1)>">';
@ -369,4 +396,19 @@ class Framework_Washtml extends PHPUnit_Framework_TestCase
$this->assertNotContains('onerror=alert(1)>', $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');
// 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('/<style [^>]+>/', $html2, "Allow styles in 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('/\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('/alert/', $html, "Remove alerts");
}
@ -88,7 +88,8 @@ class MailFunc extends PHPUnit_Framework_TestCase
function test_html_xss2()
{
$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('/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');
// 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"'
.' 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