diff --git a/CHANGELOG b/CHANGELOG index 71e87b155..dd86af6ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 ------------- diff --git a/INSTALL b/INSTALL index 54d4eee9a..2d8aee18f 100644 --- a/INSTALL +++ b/INSTALL @@ -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 diff --git a/composer.json-dist b/composer.json-dist index 2df69dc80..59cbefc5f 100644 --- a/composer.json-dist +++ b/composer.json-dist @@ -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" diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 70cbcb268..f960945ce 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -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 diff --git a/jsdeps.json b/jsdeps.json index d0acecbd4..12a6fe7cc 100644 --- a/jsdeps.json +++ b/jsdeps.json @@ -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" }, diff --git a/plugins/enigma/composer.json b/plugins/enigma/composer.json index 7357c01d6..348b39983 100644 --- a/plugins/enigma/composer.json +++ b/plugins/enigma/composer.json @@ -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" } } diff --git a/plugins/enigma/config.inc.php.dist b/plugins/enigma/config.inc.php.dist index aa4280f41..852c999f5 100644 --- a/plugins/enigma/config.inc.php.dist +++ b/plugins/enigma/config.inc.php.dist @@ -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; diff --git a/plugins/enigma/lib/enigma_driver_gnupg.php b/plugins/enigma/lib/enigma_driver_gnupg.php index a8d84ebea..c2bf016ae 100644 --- a/plugins/enigma/lib/enigma_driver_gnupg.php +++ b/plugins/enigma/lib/enigma_driver_gnupg.php @@ -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); diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php index 22056a25e..91419a8a6 100644 --- a/program/include/rcmail_output_html.php +++ b/program/include/rcmail_output_html.php @@ -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)) { diff --git a/program/js/app.js b/program/js/app.js index bd1718fec..42b1367fe 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -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'; diff --git a/program/js/editor.js b/program/js/editor.js index 8fba63ec8..da92a5b8e 100644 --- a/program/js/editor.js +++ b/program/js/editor.js @@ -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'), diff --git a/program/js/treelist.js b/program/js/treelist.js index a3a4011e9..0326e41ab 100644 --- a/program/js/treelist.js +++ b/program/js/treelist.js @@ -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; } diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index 9330083ac..fc5cc0837 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -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) : '')); } diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index cf84f84c5..1abbdb61c 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -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; $nd_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1]; diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index c46f9d36d..f1c87d562 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -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)); 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..3b17a3d41 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -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 .= ''; + 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); diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index a6aece2cc..ae92d07ac 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -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'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index 5cdb40e8a..2dbcd5d2a 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -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'; diff --git a/program/steps/mail/addcontact.inc b/program/steps/mail/addcontact.inc index 64a01d2f4..1f54c6dc6 100644 --- a/program/steps/mail/addcontact.inc +++ b/program/steps/mail/addcontact.inc @@ -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'); + } } } } diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index cc0140401..77f8929b6 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -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, '', $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( @@ -1131,13 +1132,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]; @@ -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 = '
' . $body . '
'; + $body = '
' . $body . '
'; } 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); diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc index 075b45417..59bd2e7f4 100644 --- a/program/steps/mail/show.inc +++ b/program/steps/mail/show.inc @@ -246,21 +246,21 @@ function rcmail_remote_objects_msg() $attrib['class'] = 'notice'; $attrib['style'] = 'display: none'; - $msg = html::span('', rcube::Q($RCMAIL->gettext('blockedimages'))) . ' '; - $msg .= html::a(array( - 'href' => "#loadimages", - 'onclick' => rcmail_output::JS_OBJECT_NAME.".command('load-images')" + $msg = html::span('', rcube::Q($RCMAIL->gettext('blockedresources'))) . ' ' + . 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']); diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc index 690cb0107..52797e7d7 100644 --- a/program/steps/settings/func.inc +++ b/program/steps/settings/func.inc @@ -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), ); } 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..7a6017644 100644 --- a/tests/Framework/Washtml.php +++ b/tests/Framework/Washtml.php @@ -359,6 +359,33 @@ class Framework_Washtml extends PHPUnit_Framework_TestCase $this->assertNotContains('TRACKING', $washed, "Src attribute of