diff options
Diffstat (limited to 'program/lib/Roundcube')
45 files changed, 5188 insertions, 3280 deletions
diff --git a/program/lib/Roundcube/README.md b/program/lib/Roundcube/README.md new file mode 100644 index 000000000..88f2d076e --- /dev/null +++ b/program/lib/Roundcube/README.md @@ -0,0 +1,102 @@ +Roundcube Framework +=================== + +INTRODUCTION +------------ +The Roundcube Framework is the basic library used for the Roundcube Webmail +application. It is an extract of classes providing the core functionality for +an email system. They can be used individually or as package for the following +tasks: + +- IMAP mailbox access with optional caching +- MIME message handling +- Email message creation and sending through SMTP +- General caching utilities using the local database +- Database abstraction using PDO +- VCard parsing and writing + + +INSTALLATION +------------ +Copy all files of this directory to your project or install it in the default +include_path directory of your webserver. Some classes of the framework require +one or multiple of the following [PEAR][pear] libraries: + +- Mail_Mime 1.8.1 or newer +- Mail_mimeDecode 1.5.5 or newer +- Net_SMTP (latest from https://github.com/pear/Net_SMTP/) +- Net_IDNA2 0.1.1 or newer +- Auth_SASL 1.0.6 or newer + + +USAGE +----- +The Roundcube Framework provides a bootstrapping file which registers an +autoloader and sets up the environment necessary for the Roundcube classes. +In order to make use of the framework, simply include the bootstrap.php file +from this directory in your application and start using the classes by simply +instantiating them. + +If you wanna use more complex functionality like IMAP access with database +caching or plugins, the rcube singleton helps you loading the necessary files: + +```php +<?php + +define('RCUBE_CONFIG_DIR', '<path-to-config-directory>'); +define('RCUBE_PLUGINS_DIR', '<path-to-roundcube-plugins-directory'); + +require_once '<path-to-roundcube-framework/bootstrap.php'; + +$rcube = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS); +$imap = $rcube->get_storage(); + +// do cool stuff here... + +?> +``` + +LICENSE +------- +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License (**with exceptions +for plugins**) as published by the Free Software Foundation, either +version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see [www.gnu.org/licenses/][gpl]. + +This file forms part of the Roundcube Webmail Framework for which the +following exception is added: Plugins which merely make function calls to the +Roundcube Webmail Framework, and for that purpose include it by reference +shall not be considered modifications of the software. + +If you wish to use this file in another project or create a modified +version that will not be part of the Roundcube Webmail Framework, you +may remove the exception above and use this source code under the +original version of the license. + +For more details about licensing and the exceptions for skins and plugins +see [roundcube.net/license][license] + + +CONTACT +------- +For any bug reports or feature requests please refer to the tracking system +at [trac.roundcube.net][tracreport] or subscribe to our mailing list. +See [roundcube.net/support][support] for details. + +You're always welcome to send a message to the project admins: +hello(at)roundcube(dot)net + + +[pear]: http://pear.php.net +[gpl]: http://www.gnu.org/licenses/ +[license]: http://roundcube.net/license +[support]: http://roundcube.net/support +[tracreport]: http://trac.roundcube.net/wiki/Howto_ReportIssues
\ No newline at end of file diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php index eed7db8c1..929a4ff79 100644 --- a/program/lib/Roundcube/bootstrap.php +++ b/program/lib/Roundcube/bootstrap.php @@ -2,10 +2,8 @@ /* +-----------------------------------------------------------------------+ - | program/include/bootstrap.php | - | | | This file is part of the Roundcube PHP suite | - | Copyright (C) 2005-2012, The Roundcube Dev Team | + | Copyright (C) 2005-2013, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -13,7 +11,6 @@ | | | CONTENTS: | | Roundcube Framework Initialization | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | @@ -34,12 +31,19 @@ $config = array( // critical PHP settings here. Only these, which doesn't provide // an error/warning in the logs later. See (#1486307). 'mbstring.func_overload' => 0, - 'suhosin.session.encrypt' => 0, - 'session.auto_start' => 0, - 'file_uploads' => 1, 'magic_quotes_runtime' => 0, 'magic_quotes_sybase' => 0, // #1488506 ); + +// check these additional ini settings if not called via CLI +if (php_sapi_name() != 'cli') { + $config += array( + 'suhosin.session.encrypt' => 0, + 'session.auto_start' => 0, + 'file_uploads' => 1, + ); +} + foreach ($config as $optname => $optval) { if ($optval != ini_get($optname) && @ini_set($optname, $optval) === false) { die("ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n" @@ -48,7 +52,7 @@ foreach ($config as $optname => $optval) { } // framework constants -define('RCUBE_VERSION', '0.9-git'); +define('RCUBE_VERSION', '1.0-git'); define('RCUBE_CHARSET', 'UTF-8'); if (!defined('RCUBE_LIB_DIR')) { @@ -361,6 +365,22 @@ function format_email($email) /** + * Fix version number so it can be used correctly in version_compare() + * + * @param string $version Version number string + * + * @param return Version number string + */ +function version_parse($version) +{ + return str_replace( + array('-stable', '-git'), + array('.0', '.99'), + $version); +} + + +/** * mbstring replacement functions */ if (!extension_loaded('mbstring')) diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php index 5fb574b97..592720308 100644 --- a/program/lib/Roundcube/html.php +++ b/program/lib/Roundcube/html.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/html.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2011, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | Helper class to create valid XHTML code | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ @@ -24,7 +21,7 @@ * Class for HTML code creation * * @package Framework - * @subpackage HTML + * @subpackage View */ class html { @@ -172,7 +169,7 @@ class html $attr = array('href' => $attr); } return self::tag('a', $attr, $cont, array_merge(self::$common_attrib, - array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup'))); + array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup'))); } /** @@ -290,7 +287,7 @@ class html } // attributes with no value - if (in_array($key, array('checked', 'multiple', 'disabled', 'selected'))) { + if (in_array($key, array('checked', 'multiple', 'disabled', 'selected', 'autofocus'))) { if ($value) { $attrib_arr[] = $key . '="' . $key . '"'; } @@ -343,7 +340,8 @@ class html /** * Class to create an HTML input field * - * @package HTML + * @package Framework + * @subpackage View */ class html_inputfield extends html { @@ -353,6 +351,7 @@ class html_inputfield extends html 'type','name','value','size','tabindex','autocapitalize', 'autocomplete','checked','onchange','onclick','disabled','readonly', 'spellcheck','results','maxlength','src','multiple','placeholder', + 'autofocus', ); /** @@ -398,7 +397,8 @@ class html_inputfield extends html /** * Class to create an HTML password field * - * @package HTML + * @package Framework + * @subpackage View */ class html_passwordfield extends html_inputfield { @@ -408,9 +408,9 @@ class html_passwordfield extends html_inputfield /** * Class to create an hidden HTML input field * - * @package HTML + * @package Framework + * @subpackage View */ - class html_hiddenfield extends html { protected $tagname = 'input'; @@ -458,7 +458,8 @@ class html_hiddenfield extends html /** * Class to create HTML radio buttons * - * @package HTML + * @package Framework + * @subpackage View */ class html_radiobutton extends html_inputfield { @@ -488,7 +489,8 @@ class html_radiobutton extends html_inputfield /** * Class to create HTML checkboxes * - * @package HTML + * @package Framework + * @subpackage View */ class html_checkbox extends html_inputfield { @@ -518,7 +520,8 @@ class html_checkbox extends html_inputfield /** * Class to create an HTML textarea * - * @package HTML + * @package Framework + * @subpackage View */ class html_textarea extends html { @@ -576,7 +579,8 @@ class html_textarea extends html * print $select->show('CH'); * </pre> * - * @package HTML + * @package Framework + * @subpackage View */ class html_select extends html { @@ -641,7 +645,8 @@ class html_select extends html /** * Class to build an HTML table * - * @package HTML + * @package Framework + * @subpackage View */ class html_table extends html { @@ -678,7 +683,7 @@ class html_table extends html } $cell = new stdClass; - $cell->attrib = $attr; + $cell->attrib = $attr; $cell->content = $cont; $this->rows[$this->rowindex]->cells[$this->colindex] = $cell; @@ -702,16 +707,16 @@ class html_table extends html } $cell = new stdClass; - $cell->attrib = $attr; - $cell->content = $cont; + $cell->attrib = $attr; + $cell->content = $cont; $this->header[] = $cell; } - /** + /** * Remove a column from a table * Useful for plugins making alterations - * - * @param string $class + * + * @param string $class */ public function remove_column($class) { @@ -791,8 +796,9 @@ class html_table extends html */ public function show($attrib = null) { - if (is_array($attrib)) + if (is_array($attrib)) { $this->attrib = array_merge($this->attrib, $attrib); + } $thead = $tbody = ""; @@ -834,7 +840,7 @@ class html_table extends html */ public function size() { - return count($this->rows); + return count($this->rows); } /** diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index c3aa8ffa5..77da83d8e 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -36,7 +34,7 @@ class rcube /** * Singleton instace of rcube * - * @var rcmail + * @var rcube */ static protected $instance; @@ -407,6 +405,7 @@ class rcube $sess_domain = $this->config->get('session_domain'); $sess_path = $this->config->get('session_path'); $lifetime = $this->config->get('session_lifetime', 0) * 60; + $is_secure = $this->config->get('use_https') || rcube_utils::https_check(); // set session domain if ($sess_domain) { @@ -421,7 +420,7 @@ class rcube ini_set('session.gc_maxlifetime', $lifetime * 2); } - ini_set('session.cookie_secure', rcube_utils::https_check()); + ini_set('session.cookie_secure', $is_secure); ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid'); ini_set('session.use_cookies', 1); ini_set('session.use_only_cookies', 1); @@ -1075,14 +1074,17 @@ class rcube { // handle PHP exceptions if (is_object($arg) && is_a($arg, 'Exception')) { - $err = array( + $arg = array( 'type' => 'php', 'code' => $arg->getCode(), 'line' => $arg->getLine(), 'file' => $arg->getFile(), 'message' => $arg->getMessage(), ); - $arg = $err; + } + + if (empty($arg['code'])) { + $arg['code'] = 500; } // installer @@ -1260,13 +1262,30 @@ class rcube return $this->decrypt($_SESSION['password']); } } + + + /** + * Getter for logged user language code. + * + * @return string User language code + */ + public function get_user_language() + { + if (is_object($this->user)) { + return $this->user->language; + } + else if (isset($_SESSION['language'])) { + return $_SESSION['language']; + } + } } /** * Lightweight plugin API class serving as a dummy if plugins are not enabled * - * @package Core + * @package Framework + * @subpackage Core */ class rcube_dummy_plugin_api { diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index d14fc587a..cbc3c6773 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_addressbook.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2006-2012, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | Interface to the local address book database | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ @@ -48,6 +45,7 @@ abstract class rcube_addressbook public $sort_col = 'name'; public $sort_order = 'ASC'; public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1)); + public $date_cols = array(); protected $error; @@ -141,7 +139,7 @@ abstract class rcube_addressbook */ function get_error() { - return $this->error; + return $this->error; } /** @@ -152,7 +150,7 @@ abstract class rcube_addressbook */ protected function set_error($type, $message) { - $this->error = array('type' => $type, 'message' => $message); + $this->error = array('type' => $type, 'message' => $message); } /** @@ -209,13 +207,13 @@ abstract class rcube_addressbook */ public function validate(&$save_data, $autofix = false) { - $rcmail = rcube::get_instance(); + $rcube = rcube::get_instance(); // check validity of email addresses foreach ($this->get_col_values('email', $save_data, true) as $email) { if (strlen($email)) { if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) { - $error = $rcmail->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email))); + $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email))); $this->set_error(self::ERROR_VALIDATE, $error); return false; } @@ -225,7 +223,6 @@ abstract class rcube_addressbook return true; } - /** * Create a new contact record * @@ -410,7 +407,6 @@ abstract class rcube_addressbook return array(); } - /** * Utility function to return all values of a certain data column * either as flat list or grouped by subtype @@ -443,7 +439,6 @@ abstract class rcube_addressbook return $out; } - /** * Normalize the given string for fulltext search. * Currently only optimized for Latin-1 characters; to be extended @@ -491,7 +486,6 @@ abstract class rcube_addressbook return $fn; } - /** * Compose the name to display in the contacts list for the given contact record. * This respects the settings parameter how to list conacts. @@ -529,5 +523,66 @@ abstract class rcube_addressbook return $fn; } + /** + * Create a unique key for sorting contacts + */ + public static function compose_contact_key($contact, $sort_col) + { + $key = $contact[$sort_col] . ':' . $row['sourceid']; + + // add email to a key to not skip contacts with the same name (#1488375) + if (!empty($contact['email'])) { + $key .= ':' . implode(':', (array)$contact['email']); + } + + return $key; + } + + + /** + * Compare search value with contact data + * + * @param string $colname Data name + * @param string|array $value Data value + * @param string $search Search value + * @param int $mode Search mode + * + * @return bool Comparision result + */ + protected function compare_search_value($colname, $value, $search, $mode) + { + // The value is a date string, for date we'll + // use only strict comparison (mode = 1) + // @TODO: partial search, e.g. match only day and month + if (in_array($colname, $this->date_cols)) { + return (($value = rcube_utils::strtotime($value)) + && ($search = rcube_utils::strtotime($search)) + && date('Ymd', $value) == date('Ymd', $search)); + } + + // composite field, e.g. address + foreach ((array)$value as $val) { + $val = mb_strtolower($val); + switch ($mode) { + case 1: + $got = ($val == $search); + break; + + case 2: + $got = ($search == substr($val, 0, strlen($search))); + break; + + default: + $got = (strpos($val, $search) !== false); + } + + if ($got) { + return true; + } + } + + return false; + } + } diff --git a/program/lib/Roundcube/rcube_base_replacer.php b/program/lib/Roundcube/rcube_base_replacer.php index b2a0fc13c..e41ccb1d9 100644 --- a/program/lib/Roundcube/rcube_base_replacer.php +++ b/program/lib/Roundcube/rcube_base_replacer.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_base_replacer.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | Provide basic functions for base URL replacement | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ @@ -24,7 +21,7 @@ * using a predefined base * * @package Framework - * @subpackage Core + * @subpackage Utils * @author Thomas Bruederli <roundcube@gmail.com> */ class rcube_base_replacer diff --git a/program/lib/Roundcube/rcube_browser.php b/program/lib/Roundcube/rcube_browser.php index 154e7ef4e..34128291b 100644 --- a/program/lib/Roundcube/rcube_browser.php +++ b/program/lib/Roundcube/rcube_browser.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_browser.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2007-2009, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | Class representing the client browser's properties | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ @@ -23,7 +20,7 @@ * Provide details about the client's browser based on the User-Agent header * * @package Framework - * @subpackage Core + * @subpackage Utils */ class rcube_browser { diff --git a/program/lib/Roundcube/rcube_cache.php b/program/lib/Roundcube/rcube_cache.php index 3e1ce4fc8..92f12a8bf 100644 --- a/program/lib/Roundcube/rcube_cache.php +++ b/program/lib/Roundcube/rcube_cache.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_cache.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2011, The Roundcube Dev Team | | Copyright (C) 2011, Kolab Systems AG | @@ -14,7 +12,6 @@ | | | PURPOSE: | | Caching engine | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | diff --git a/program/lib/Roundcube/rcube_charset.php b/program/lib/Roundcube/rcube_charset.php index 6135a5711..a7f26a3f4 100644 --- a/program/lib/Roundcube/rcube_charset.php +++ b/program/lib/Roundcube/rcube_charset.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_charset.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -15,7 +13,6 @@ | | | PURPOSE: | | Provide charset conversion functionality | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | @@ -649,12 +646,13 @@ class rcube_charset /** * A method to guess character set of a string. * - * @param string $string String. - * @param string $failover Default result for failover. + * @param string $string String + * @param string $failover Default result for failover + * @param string $language User language * * @return string Charset name */ - public static function detect($string, $failover='') + public static function detect($string, $failover = null, $language = null) { if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian @@ -669,38 +667,62 @@ class rcube_charset if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE'; if (function_exists('mb_detect_encoding')) { - // FIXME: the order is important, because sometimes - // iso string is detected as euc-jp and etc. - $enc = array( - 'UTF-8', 'SJIS', 'GB2312', - 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', - 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', - 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', - 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG5', - 'ISO-2022-KR', 'ISO-2022-JP', - ); + if (empty($language)) { + $rcube = rcube::get_instance(); + $language = $rcube->get_user_language(); + } + + // Prioritize charsets according to current language (#1485669) + switch ($language) { + case 'ja_JP': // for Japanese + $prio = array('ISO-2022-JP', 'JIS', 'UTF-8', 'EUC-JP', 'eucJP-win', 'SJIS', 'SJIS-win'); + break; + + case 'zh_CN': // for Chinese (Simplified) + case 'zh_TW': // for Chinese (Traditional) + $prio = array('UTF-8', 'BIG-5', 'GB2312', 'EUC-TW'); + break; + + case 'ko_KR': // for Korean + $prio = array('UTF-8', 'EUC-KR', 'ISO-2022-KR'); + break; - $result = mb_detect_encoding($string, join(',', $enc)); + case 'ru_RU': // for Russian + $prio = array('UTF-8', 'WINDOWS-1251', 'KOI8-R'); + break; + + default: + $prio = array('UTF-8', 'SJIS', 'GB2312', + 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', + 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', + 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', + 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG-5', + 'ISO-2022-KR', 'ISO-2022-JP', + ); + } + + $encodings = array_unique(array_merge($prio, mb_list_encodings())); + + return mb_detect_encoding($string, $encodings); } - else { - // No match, check for UTF-8 - // from http://w3.org/International/questions/qa-forms-utf-8.html - if (preg_match('/\A( - [\x09\x0A\x0D\x20-\x7E] - | [\xC2-\xDF][\x80-\xBF] - | \xE0[\xA0-\xBF][\x80-\xBF] - | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} - | \xED[\x80-\x9F][\x80-\xBF] - | \xF0[\x90-\xBF][\x80-\xBF]{2} - | [\xF1-\xF3][\x80-\xBF]{3} - | \xF4[\x80-\x8F][\x80-\xBF]{2} - )*\z/xs', substr($string, 0, 2048)) - ) { + + // No match, check for UTF-8 + // from http://w3.org/International/questions/qa-forms-utf-8.html + if (preg_match('/\A( + [\x09\x0A\x0D\x20-\x7E] + | [\xC2-\xDF][\x80-\xBF] + | \xE0[\xA0-\xBF][\x80-\xBF] + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} + | \xED[\x80-\x9F][\x80-\xBF] + | \xF0[\x90-\xBF][\x80-\xBF]{2} + | [\xF1-\xF3][\x80-\xBF]{3} + | \xF4[\x80-\x8F][\x80-\xBF]{2} + )*\z/xs', substr($string, 0, 2048)) + ) { return 'UTF-8'; - } } - return $result ? $result : $failover; + return $failover; } diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php index 615faf3ad..2190dc4c2 100644 --- a/program/lib/Roundcube/rcube_config.php +++ b/program/lib/Roundcube/rcube_config.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_config.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2012, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | Class to read configuration settings | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php index 5b4292a4c..c66e98687 100644 --- a/program/lib/Roundcube/rcube_contacts.php +++ b/program/lib/Roundcube/rcube_contacts.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_contacts.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2006-2012, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | Interface to the local address book database | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ @@ -63,6 +60,7 @@ class rcube_contacts extends rcube_addressbook 'jobtitle', 'organization', 'department', 'assistant', 'manager', 'gender', 'maidenname', 'spouse', 'email', 'phone', 'address', 'birthday', 'anniversary', 'website', 'im', 'notes', 'photo'); + public $date_cols = array('birthday', 'anniversary'); const SEPARATOR = ','; @@ -404,32 +402,16 @@ class rcube_contacts extends rcube_addressbook for ($i=0; $i<$pages; $i++) { $this->list_records(null, $i, true); while ($row = $this->result->next()) { - $id = $row[$this->primary_key]; + $id = $row[$this->primary_key]; $found = array(); foreach (preg_grep($regexp, array_keys($row)) as $col) { $pos = strpos($col, ':'); $colname = $pos ? substr($col, 0, $pos) : $col; $search = $post_search[$colname]; foreach ((array)$row[$col] as $value) { - // composite field, e.g. address - foreach ((array)$value as $val) { - $val = mb_strtolower($val); - switch ($mode) { - case 1: - $got = ($val == $search); - break; - case 2: - $got = ($search == substr($val, 0, strlen($search))); - break; - default: - $got = (strpos($val, $search) !== false); - break; - } - - if ($got) { - $found[$colname] = true; - break 2; - } + if ($this->compare_search_value($colname, $value, $search, $mode)) { + $found[$colname] = true; + break 2; } } } diff --git a/program/lib/Roundcube/rcube_content_filter.php b/program/lib/Roundcube/rcube_content_filter.php index 99916a300..ae6617d1b 100644 --- a/program/lib/Roundcube/rcube_content_filter.php +++ b/program/lib/Roundcube/rcube_content_filter.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_content_filter.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2011, The Roundcube Dev Team | | | @@ -13,7 +11,6 @@ | | | PURPOSE: | | PHP stream filter to detect evil content in mail attachments | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ @@ -23,7 +20,7 @@ * PHP stream filter to detect html/javascript code in attachments * * @package Framework - * @subpackage Core + * @subpackage Utils */ class rcube_content_filter extends php_user_filter { diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php index 850c0c4c3..0d3276b84 100644 --- a/program/lib/Roundcube/rcube_csv2vcard.php +++ b/program/lib/Roundcube/rcube_csv2vcard.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_csv2vcard.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2012, The Roundcube Dev Team | | | @@ -126,6 +124,12 @@ class rcube_csv2vcard //'work_address_2' => '', 'work_country' => 'country:work', 'work_zipcode' => 'zipcode:work', + 'last' => 'surname', + 'first' => 'firstname', + 'work_city' => 'locality:work', + 'work_state' => 'region:work', + 'home_city_short' => 'locality:home', + 'home_state_short' => 'region:home', ); /** @@ -273,13 +277,7 @@ class rcube_csv2vcard // Parse file foreach (preg_split("/[\r\n]+/", $csv) as $i => $line) { - $line = trim($line); - if (empty($line)) { - continue; - } - - $elements = rcube_utils::explode_quoted_string(',', $line); - + $elements = $this->parse_line($line); if (empty($elements)) { continue; } @@ -307,6 +305,35 @@ class rcube_csv2vcard } /** + * Parse CSV file line + */ + protected function parse_line($line) + { + $line = trim($line); + if (empty($line)) { + return null; + } + + $fields = rcube_utils::explode_quoted_string(',', $line); + + // remove quotes if needed + if (!empty($fields)) { + foreach ($fields as $idx => $value) { + if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') { + // remove surrounding quotes + $value = substr($value, 1, -1); + // replace doubled quotes inside the string with single quote + $value = str_replace('""', '"', $value); + + $fields[$idx] = $value; + } + } + } + + return $fields; + } + + /** * Parse CSV header line, detect fields mapping */ protected function parse_header($elements) @@ -369,6 +396,15 @@ class rcube_csv2vcard } } + // Convert address(es) to rcube_vcard data + foreach ($contact as $idx => $value) { + $name = explode(':', $idx); + if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) { + $contact['address:'.$name[1]][$name[0]] = $value; + unset($contact[$idx]); + } + } + // Create vcard object $vcard = new rcube_vcard(); foreach ($contact as $name => $value) { diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index 5d8c4a534..4e6684c51 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -13,19 +11,17 @@ | | | PURPOSE: | | Database wrapper class that implements PHP PDO functions | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface. * This is a wrapper for the PHP PDO. * * @package Framework - * @sbpackage Database + * @subpackage Database */ class rcube_db { @@ -37,12 +33,11 @@ class rcube_db protected $db_mode; // Connection mode protected $dbh; // Connection handle - protected $db_error = false; - protected $db_error_msg = ''; - protected $conn_failure = false; - protected $a_query_results = array('dummy'); - protected $last_res_id = 0; - protected $db_index = 0; + protected $db_error = false; + protected $db_error_msg = ''; + protected $conn_failure = false; + protected $db_index = 0; + protected $last_result; protected $tables; protected $variables; @@ -75,7 +70,7 @@ class rcube_db $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver; $class = "rcube_db_$driver"; - if (!class_exists($class)) { + if (!$driver || !class_exists($class)) { rcube::raise_error(array('code' => 600, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__, 'message' => "Configuration error. Unsupported database driver: $driver"), @@ -227,7 +222,7 @@ class rcube_db $this->db_connected = is_object($this->dbh); // use write-master when read-only fails - if (!$this->db_connected && $mode == 'r') { + if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) { $mode = 'w'; $this->dbh = $this->dsn_connect($this->db_dsnw_array); $this->db_connected = is_object($this->dbh); @@ -267,14 +262,14 @@ class rcube_db /** * Getter for error state * - * @param int $res_id Optional query result identifier + * @param mixed $result Optional query result * * @return string Error message */ - public function is_error($res_id = null) + public function is_error($result = null) { - if ($res_id !== null) { - return $this->_get_result($res_id) === false ? $this->db_error_msg : null; + if ($result !== null) { + return $result === false ? $this->db_error_msg : null; } return $this->db_error ? $this->db_error_msg : null; @@ -343,7 +338,7 @@ class rcube_db * @param int Number of rows for LIMIT statement * @param mixed Values to be inserted in query * - * @return int Query handle identifier + * @return PDOStatement|bool Query handle or False on error */ public function limitquery() { @@ -363,7 +358,7 @@ class rcube_db * @param int $numrows Number of rows for LIMIT statement * @param array $params Values to be inserted in query * - * @return int Query handle identifier + * @return PDOStatement|bool Query handle or False on error */ protected function _query($query, $offset, $numrows, $params) { @@ -374,7 +369,7 @@ class rcube_db // check connection before proceeding if (!$this->is_connected()) { - return null; + return $this->last_result = false; } if ($numrows || $offset) { @@ -405,6 +400,11 @@ class rcube_db $this->debug($query); + // destroy reference to previous result, required for SQLite driver (#1488874) + $this->last_result = null; + $this->db_error_msg = null; + + // send query $query = $this->dbh->query($query); if ($query === false) { @@ -417,20 +417,21 @@ class rcube_db 'message' => $this->db_error_msg), true, false); } - // add result, even if it's an error - return $this->_add_result($query); + $this->last_result = $query; + + return $query; } /** * Get number of affected rows for the last query * - * @param number $res_id Optional query handle identifier + * @param mixed $result Optional query handle * - * @return int Number of rows or false on failure + * @return int Number of (matching) rows */ - public function affected_rows($res_id = null) + public function affected_rows($result = null) { - if ($result = $this->_get_result($res_id)) { + if ($result || ($result === null && ($result = $this->last_result))) { return $result->rowCount(); } @@ -438,6 +439,32 @@ class rcube_db } /** + * Get number of rows for a SQL query + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * @return mixed Number of rows or false on failure + * @deprecated This method shows very poor performance and should be avoided. + */ + public function num_rows($result = null) + { + if ($result || ($result === null && ($result = $this->last_result))) { + // repeat query with SELECT COUNT(*) ... + if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) { + $query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM); + return $query ? intval($query->fetchColumn(0)) : false; + } + else { + $num = count($result->fetchAll()); + $result->execute(); // re-execute query because there's no seek(0) + return $num; + } + } + + return false; + } + + /** * Get last inserted record ID * * @param string $table Table name (to find the incremented sequence) @@ -464,13 +491,12 @@ class rcube_db * Get an associative array for one row * If no query handle is specified, the last query will be taken as reference * - * @param int $res_id Optional query handle identifier + * @param mixed $result Optional query handle * * @return mixed Array with col values or false on failure */ - public function fetch_assoc($res_id = null) + public function fetch_assoc($result = null) { - $result = $this->_get_result($res_id); return $this->_fetch_row($result, PDO::FETCH_ASSOC); } @@ -478,31 +504,30 @@ class rcube_db * Get an index array for one row * If no query handle is specified, the last query will be taken as reference * - * @param int $res_id Optional query handle identifier + * @param mixed $result Optional query handle * * @return mixed Array with col values or false on failure */ - public function fetch_array($res_id = null) + public function fetch_array($result = null) { - $result = $this->_get_result($res_id); return $this->_fetch_row($result, PDO::FETCH_NUM); } /** * Get col values for a result row * - * @param PDOStatement $result Result handle - * @param int $mode Fetch mode identifier + * @param mixed $result Optional query handle + * @param int $mode Fetch mode identifier * * @return mixed Array with col values or false on failure */ protected function _fetch_row($result, $mode) { - if (!is_object($result) || !$this->is_connected()) { - return false; + if ($result || ($result === null && ($result = $this->last_result))) { + return $result->fetch($mode); } - return $result->fetch($mode); + return false; } /** @@ -538,8 +563,8 @@ class rcube_db if ($this->tables === null) { $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME'); - if ($res = $this->_get_result($q)) { - $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0); + if ($q) { + $this->tables = $q->fetchAll(PDO::FETCH_COLUMN, 0); } else { $this->tables = array(); @@ -561,8 +586,8 @@ class rcube_db $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?', array($table)); - if ($res = $this->_get_result($q)) { - return $res->fetchAll(PDO::FETCH_COLUMN, 0); + if ($q) { + return $q->fetchAll(PDO::FETCH_COLUMN, 0); } return array(); @@ -572,7 +597,7 @@ class rcube_db * Formats input so it can be safely used in a query * * @param mixed $input Value to quote - * @param string $type Type of data + * @param string $type Type of data (integer, bool, ident) * * @return string Quoted/converted string for use in query */ @@ -587,6 +612,10 @@ class rcube_db return 'NULL'; } + if ($type == 'ident') { + return $this->quote_identifier($input); + } + // create DB handle if not available if (!$this->dbh) { $this->db_connect('r'); @@ -636,7 +665,7 @@ class rcube_db $name[] = $start . $elem . $end; } - return implode($name, '.'); + return implode($name, '.'); } /** @@ -653,7 +682,7 @@ class rcube_db * Return list of elements for use with SQL's IN clause * * @param array $arr Input array - * @param string $type Type of data + * @param string $type Type of data (integer, bool, ident) * * @return string Comma-separated list of quoted values for use in query */ @@ -777,42 +806,6 @@ class rcube_db } /** - * Adds a query result and returns a handle ID - * - * @param object $res Query handle - * - * @return int Handle ID - */ - protected function _add_result($res) - { - $this->last_res_id = sizeof($this->a_query_results); - $this->a_query_results[$this->last_res_id] = $res; - - return $this->last_res_id; - } - - /** - * Resolves a given handle ID and returns the according query handle - * If no ID is specified, the last resource handle will be returned - * - * @param int $res_id Handle ID - * - * @return mixed Resource handle or false on failure - */ - protected function _get_result($res_id = null) - { - if ($res_id == null) { - $res_id = $this->last_res_id; - } - - if (!empty($this->a_query_results[$res_id])) { - return $this->a_query_results[$res_id]; - } - - return false; - } - - /** * Return correct name for a specific database table * * @param string $table Table name diff --git a/program/lib/Roundcube/rcube_db_mssql.php b/program/lib/Roundcube/rcube_db_mssql.php index c95663c74..37a42678a 100644 --- a/program/lib/Roundcube/rcube_db_mssql.php +++ b/program/lib/Roundcube/rcube_db_mssql.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db_mssql.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -14,13 +12,11 @@ | PURPOSE: | | Database wrapper class that implements PHP PDO functions | | for MS SQL Server database | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface * This is a wrapper for the PHP PDO @@ -104,26 +100,30 @@ class rcube_db_mssql extends rcube_db { $limit = intval($limit); $offset = intval($offset); + $end = $offset + $limit; - $orderby = stristr($query, 'ORDER BY'); - if ($orderby !== false) { - $sort = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc'; - $order = str_ireplace('ORDER BY', '', $orderby); - $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order)); + // query without OFFSET + if (!$offset) { + $query = preg_replace('/^SELECT\s/i', "SELECT TOP $limit ", $query); + return $query; } - $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($limit + $offset) . ' ', $query); + $orderby = stristr($query, 'ORDER BY'); + $offset += 1; - $query = 'SELECT * FROM (SELECT TOP ' . $limit . ' * FROM (' . $query . ') AS inner_tbl'; if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' '; - $query .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC'; + $query = trim(substr($query, 0, -1 * strlen($orderby))); } - $query .= ') AS outer_tbl'; - if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' ' . $sort; + else { + // it shouldn't happen, paging without sorting has not much sense + // @FIXME: I don't know how to build paging query without ORDER BY + $orderby = "ORDER BY 1"; } + $query = preg_replace('/^SELECT\s/i', '', $query); + $query = "WITH paging AS (SELECT ROW_NUMBER() OVER ($orderby) AS [RowNumber], $query)" + . " SELECT * FROM paging WHERE [RowNumber] BETWEEN $offset AND $end ORDER BY [RowNumber]"; + return $query; } diff --git a/program/lib/Roundcube/rcube_db_mysql.php b/program/lib/Roundcube/rcube_db_mysql.php index 1c5ba1de7..8ab6403c8 100644 --- a/program/lib/Roundcube/rcube_db_mysql.php +++ b/program/lib/Roundcube/rcube_db_mysql.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db_mysql.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -14,13 +12,11 @@ | PURPOSE: | | Database wrapper class that implements PHP PDO functions | | for MySQL database | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface * @@ -130,6 +126,9 @@ class rcube_db_mysql extends rcube_db // Always return matching (not affected only) rows count $result[PDO::MYSQL_ATTR_FOUND_ROWS] = true; + // Enable AUTOCOMMIT mode (#1488902) + $dsn_options[PDO::ATTR_AUTOCOMMIT] = true; + return $result; } diff --git a/program/lib/Roundcube/rcube_db_pgsql.php b/program/lib/Roundcube/rcube_db_pgsql.php index 797860a84..cf23c5e48 100644 --- a/program/lib/Roundcube/rcube_db_pgsql.php +++ b/program/lib/Roundcube/rcube_db_pgsql.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db_pgsql.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -14,13 +12,11 @@ | PURPOSE: | | Database wrapper class that implements PHP PDO functions | | for PostgreSQL database | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface * This is a wrapper for the PHP PDO diff --git a/program/lib/Roundcube/rcube_db_sqlite.php b/program/lib/Roundcube/rcube_db_sqlite.php index 65dcb6d6e..145b8a371 100644 --- a/program/lib/Roundcube/rcube_db_sqlite.php +++ b/program/lib/Roundcube/rcube_db_sqlite.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db_sqlite.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -14,13 +12,11 @@ | PURPOSE: | | Database wrapper class that implements PHP PDO functions | | for SQLite database | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface * This is a wrapper for the PHP PDO @@ -124,12 +120,7 @@ class rcube_db_sqlite extends rcube_db $q = $this->query('SELECT name FROM sqlite_master' .' WHERE type = \'table\' ORDER BY name'); - if ($res = $this->_get_result($q)) { - $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0); - } - else { - $this->tables = array(); - } + $this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : array(); } return $this->tables; diff --git a/program/lib/Roundcube/rcube_db_sqlsrv.php b/program/lib/Roundcube/rcube_db_sqlsrv.php index 8b6ffe807..e5dfb1154 100644 --- a/program/lib/Roundcube/rcube_db_sqlsrv.php +++ b/program/lib/Roundcube/rcube_db_sqlsrv.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db_sqlsrv.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -14,13 +12,11 @@ | PURPOSE: | | Database wrapper class that implements PHP PDO functions | | for MS SQL Server database | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface * This is a wrapper for the PHP PDO @@ -104,26 +100,30 @@ class rcube_db_sqlsrv extends rcube_db { $limit = intval($limit); $offset = intval($offset); + $end = $offset + $limit; - $orderby = stristr($query, 'ORDER BY'); - if ($orderby !== false) { - $sort = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc'; - $order = str_ireplace('ORDER BY', '', $orderby); - $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order)); + // query without OFFSET + if (!$offset) { + $query = preg_replace('/^SELECT\s/i', "SELECT TOP $limit ", $query); + return $query; } - $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($limit + $offset) . ' ', $query); + $orderby = stristr($query, 'ORDER BY'); + $offset += 1; - $query = 'SELECT * FROM (SELECT TOP ' . $limit . ' * FROM (' . $query . ') AS inner_tbl'; if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' '; - $query .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC'; + $query = trim(substr($query, 0, -1 * strlen($orderby))); } - $query .= ') AS outer_tbl'; - if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' ' . $sort; + else { + // it shouldn't happen, paging without sorting has not much sense + // @FIXME: I don't know how to build paging query without ORDER BY + $orderby = "ORDER BY 1"; } + $query = preg_replace('/^SELECT\s/i', '', $query); + $query = "WITH paging AS (SELECT ROW_NUMBER() OVER ($orderby) AS [RowNumber], $query)" + . " SELECT * FROM paging WHERE [RowNumber] BETWEEN $offset AND $end ORDER BY [RowNumber]"; + return $query; } diff --git a/program/lib/Roundcube/rcube_enriched.php b/program/lib/Roundcube/rcube_enriched.php new file mode 100644 index 000000000..8c628c912 --- /dev/null +++ b/program/lib/Roundcube/rcube_enriched.php @@ -0,0 +1,143 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2005-2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Helper class to convert Enriched to HTML format (RFC 1523, 1896) | + +-----------------------------------------------------------------------+ + | Author: Aleksander Machniak <alec@alec.pl> | + | Author: Ryo Chijiiwa (IlohaMail) | + +-----------------------------------------------------------------------+ +*/ + +/** + * Class for Enriched to HTML conversion + * + * @package Framework + * @subpackage Utils + */ +class rcube_enriched +{ + protected static function convert_newlines($body) + { + // remove single newlines, convert N newlines to N-1 + $body = str_replace("\r\n", "\n", $body); + $len = strlen($body); + $nl = 0; + $out = ''; + + for ($i=0; $i<$len; $i++) { + $c = $body[$i]; + if (ord($c) == 10) + $nl++; + if ($nl && ord($c) != 10) + $nl = 0; + if ($nl != 1) + $out .= $c; + else + $out .= ' '; + } + + return $out; + } + + protected static function convert_formatting($body) + { + $replace = array( + '<bold>' => '<b>', '</bold>' => '</b>', + '<italic>' => '<i>', '</italic>' => '</i>', + '<fixed>' => '<tt>', '</fixed>' => '</tt>', + '<smaller>' => '<font size=-1>', '</smaller>'=> '</font>', + '<bigger>' => '<font size=+1>', '</bigger>' => '</font>', + '<underline>' => '<span style="text-decoration: underline">', '</underline>' => '</span>', + '<flushleft>' => '<span style="text-align: left">', '</flushleft>' => '</span>', + '<flushright>' => '<span style="text-align: right">', '</flushright>' => '</span>', + '<flushboth>' => '<span style="text-align: justified">', '</flushboth>' => '</span>', + '<indent>' => '<span style="padding-left: 20px">', '</indent>' => '</span>', + '<indentright>' => '<span style="padding-right: 20px">', '</indentright>' => '</span>', + ); + + return str_ireplace(array_keys($replace), array_values($replace), $body); + } + + protected static function convert_font($body) + { + $pattern = '/(.*)\<fontfamily\>\<param\>(.*)\<\/param\>(.*)\<\/fontfamily\>(.*)/ims'; + + while (preg_match($pattern, $body, $a)) { + if (count($a) != 5) + continue; + + $body = $a[1].'<span style="font-family: '.$a[2].'">'.$a[3].'</span>'.$a[4]; + } + + return $body; + } + + protected static function convert_color($body) + { + $pattern = '/(.*)\<color\>\<param\>(.*)\<\/param\>(.*)\<\/color\>(.*)/ims'; + + while (preg_match($pattern, $body, $a)) { + if (count($a) != 5) + continue; + + // extract color (either by name, or ####,####,####) + if (strpos($a[2],',')) { + $rgb = explode(',',$a[2]); + $color = '#'; + for ($i=0; $i<3; $i++) + $color .= substr($rgb[$i], 0, 2); // just take first 2 bytes + } + else { + $color = $a[2]; + } + + // put it all together + $body = $a[1].'<span style="color: '.$color.'">'.$a[3].'</span>'.$a[4]; + } + + return $body; + } + + protected static function convert_excerpt($body) + { + $pattern = '/(.*)\<excerpt\>(.*)\<\/excerpt\>(.*)/i'; + + while (preg_match($pattern, $body, $a)) { + if (count($a) != 4) + continue; + + $quoted = ''; + $lines = explode('<br>', $a[2]); + + foreach ($lines as $n => $line) + $quoted .= '>'.$line.'<br>'; + + $body = $a[1].'<span class="quotes">'.$quoted.'</span>'.$a[3]; + } + + return $body; + } + + public static function to_html($body) + { + $body = str_replace('<<','<',$body); + $body = self::convert_newlines($body); + $body = str_replace("\n", '<br>', $body); + $body = self::convert_formatting($body); + $body = self::convert_color($body); + $body = self::convert_font($body); + $body = self::convert_excerpt($body); + //$body = nl2br($body); + + return $body; + } +} diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php new file mode 100644 index 000000000..9b248a3a8 --- /dev/null +++ b/program/lib/Roundcube/rcube_html2text.php @@ -0,0 +1,701 @@ +<?php + +/** + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2008-2012, The Roundcube Dev Team | + | Copyright (c) 2005-2007, Jon Abernathy <jon@chuggnutt.com> | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Converts HTML to formatted plain text (based on html2text class) | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + | Author: Jon Abernathy <jon@chuggnutt.com> | + +-----------------------------------------------------------------------+ + */ + +/** + * Takes HTML and converts it to formatted, plain text. + * + * Thanks to Alexander Krug (http://www.krugar.de/) to pointing out and + * correcting an error in the regexp search array. Fixed 7/30/03. + * + * Updated set_html() function's file reading mechanism, 9/25/03. + * + * Thanks to Joss Sanglier (http://www.dancingbear.co.uk/) for adding + * several more HTML entity codes to the $search and $replace arrays. + * Updated 11/7/03. + * + * Thanks to Darius Kasperavicius (http://www.dar.dar.lt/) for + * suggesting the addition of $allowed_tags and its supporting function + * (which I slightly modified). Updated 3/12/04. + * + * Thanks to Justin Dearing for pointing out that a replacement for the + * <TH> tag was missing, and suggesting an appropriate fix. + * Updated 8/25/04. + * + * Thanks to Mathieu Collas (http://www.myefarm.com/) for finding a + * display/formatting bug in the _build_link_list() function: email + * readers would show the left bracket and number ("[1") as part of the + * rendered email address. + * Updated 12/16/04. + * + * Thanks to Wojciech Bajon (http://histeria.pl/) for submitting code + * to handle relative links, which I hadn't considered. I modified his + * code a bit to handle normal HTTP links and MAILTO links. Also for + * suggesting three additional HTML entity codes to search for. + * Updated 03/02/05. + * + * Thanks to Jacob Chandler for pointing out another link condition + * for the _build_link_list() function: "https". + * Updated 04/06/05. + * + * Thanks to Marc Bertrand (http://www.dresdensky.com/) for + * suggesting a revision to the word wrapping functionality; if you + * specify a $width of 0 or less, word wrapping will be ignored. + * Updated 11/02/06. + * + * *** Big housecleaning updates below: + * + * Thanks to Colin Brown (http://www.sparkdriver.co.uk/) for + * suggesting the fix to handle </li> and blank lines (whitespace). + * Christian Basedau (http://www.movetheweb.de/) also suggested the + * blank lines fix. + * + * Special thanks to Marcus Bointon (http://www.synchromedia.co.uk/), + * Christian Basedau, Norbert Laposa (http://ln5.co.uk/), + * Bas van de Weijer, and Marijn van Butselaar + * for pointing out my glaring error in the <th> handling. Marcus also + * supplied a host of fixes. + * + * Thanks to Jeffrey Silverman (http://www.newtnotes.com/) for pointing + * out that extra spaces should be compressed--a problem addressed with + * Marcus Bointon's fixes but that I had not yet incorporated. + * + * Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for + * suggesting a valuable fix with <a> tag handling. + * + * Thanks to Wojciech Bajon (again!) for suggesting fixes and additions, + * including the <a> tag handling that Daniel Schledermann pointed + * out but that I had not yet incorporated. I haven't (yet) + * incorporated all of Wojciech's changes, though I may at some + * future time. + * + * *** End of the housecleaning updates. Updated 08/08/07. + */ + +/** + * Converts HTML to formatted plain text + * + * @package Framework + * @subpackage Utils + */ +class rcube_html2text +{ + /** + * Contains the HTML content to convert. + * + * @var string $html + */ + protected $html; + + /** + * Contains the converted, formatted text. + * + * @var string $text + */ + protected $text; + + /** + * Maximum width of the formatted text, in columns. + * + * Set this value to 0 (or less) to ignore word wrapping + * and not constrain text to a fixed-width column. + * + * @var integer $width + */ + protected $width = 70; + + /** + * Target character encoding for output text + * + * @var string $charset + */ + protected $charset = 'UTF-8'; + + /** + * List of preg* regular expression patterns to search for, + * used in conjunction with $replace. + * + * @var array $search + * @see $replace + */ + protected $search = array( + "/\r/", // Non-legal carriage return + "/[\n\t]+/", // Newlines and tabs + '/<head[^>]*>.*?<\/head>/i', // <head> + '/<script[^>]*>.*?<\/script>/i', // <script>s -- which strip_tags supposedly has problems with + '/<style[^>]*>.*?<\/style>/i', // <style>s -- which strip_tags supposedly has problems with + '/<p[^>]*>/i', // <P> + '/<br[^>]*>/i', // <br> + '/<i[^>]*>(.*?)<\/i>/i', // <i> + '/<em[^>]*>(.*?)<\/em>/i', // <em> + '/(<ul[^>]*>|<\/ul>)/i', // <ul> and </ul> + '/(<ol[^>]*>|<\/ol>)/i', // <ol> and </ol> + '/<li[^>]*>(.*?)<\/li>/i', // <li> and </li> + '/<li[^>]*>/i', // <li> + '/<hr[^>]*>/i', // <hr> + '/<div[^>]*>/i', // <div> + '/(<table[^>]*>|<\/table>)/i', // <table> and </table> + '/(<tr[^>]*>|<\/tr>)/i', // <tr> and </tr> + '/<td[^>]*>(.*?)<\/td>/i', // <td> and </td> + ); + + /** + * List of pattern replacements corresponding to patterns searched. + * + * @var array $replace + * @see $search + */ + protected $replace = array( + '', // Non-legal carriage return + ' ', // Newlines and tabs + '', // <head> + '', // <script>s -- which strip_tags supposedly has problems with + '', // <style>s -- which strip_tags supposedly has problems with + "\n\n", // <P> + "\n", // <br> + '_\\1_', // <i> + '_\\1_', // <em> + "\n\n", // <ul> and </ul> + "\n\n", // <ol> and </ol> + "\t* \\1\n", // <li> and </li> + "\n\t* ", // <li> + "\n-------------------------\n", // <hr> + "<div>\n", // <div> + "\n\n", // <table> and </table> + "\n", // <tr> and </tr> + "\t\t\\1\n", // <td> and </td> + ); + + /** + * List of preg* regular expression patterns to search for, + * used in conjunction with $ent_replace. + * + * @var array $ent_search + * @see $ent_replace + */ + protected $ent_search = array( + '/&(nbsp|#160);/i', // Non-breaking space + '/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i', + // Double quotes + '/&(apos|rsquo|lsquo|#8216|#8217);/i', // Single quotes + '/>/i', // Greater-than + '/</i', // Less-than + '/&(copy|#169);/i', // Copyright + '/&(trade|#8482|#153);/i', // Trademark + '/&(reg|#174);/i', // Registered + '/&(mdash|#151|#8212);/i', // mdash + '/&(ndash|minus|#8211|#8722);/i', // ndash + '/&(bull|#149|#8226);/i', // Bullet + '/&(pound|#163);/i', // Pound sign + '/&(euro|#8364);/i', // Euro sign + '/&(amp|#38);/i', // Ampersand: see _converter() + '/[ ]{2,}/', // Runs of spaces, post-handling + ); + + /** + * List of pattern replacements corresponding to patterns searched. + * + * @var array $ent_replace + * @see $ent_search + */ + protected $ent_replace = array( + ' ', // Non-breaking space + '"', // Double quotes + "'", // Single quotes + '>', + '<', + '(c)', + '(tm)', + '(R)', + '--', + '-', + '*', + '£', + 'EUR', // Euro sign. € ? + '|+|amp|+|', // Ampersand: see _converter() + ' ', // Runs of spaces, post-handling + ); + + /** + * List of preg* regular expression patterns to search for + * and replace using callback function. + * + * @var array $callback_search + */ + protected $callback_search = array( + '/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i', // <a href=""> + '/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i', // h1 - h6 + '/<(b)( [^>]*)?>(.*?)<\/b>/i', // <b> + '/<(strong)( [^>]*)?>(.*?)<\/strong>/i', // <strong> + '/<(th)( [^>]*)?>(.*?)<\/th>/i', // <th> and </th> + ); + + /** + * List of preg* regular expression patterns to search for in PRE body, + * used in conjunction with $pre_replace. + * + * @var array $pre_search + * @see $pre_replace + */ + protected $pre_search = array( + "/\n/", + "/\t/", + '/ /', + '/<pre[^>]*>/', + '/<\/pre>/' + ); + + /** + * List of pattern replacements corresponding to patterns searched for PRE body. + * + * @var array $pre_replace + * @see $pre_search + */ + protected $pre_replace = array( + '<br>', + ' ', + ' ', + '', + '' + ); + + /** + * Contains a list of HTML tags to allow in the resulting text. + * + * @var string $allowed_tags + * @see set_allowed_tags() + */ + protected $allowed_tags = ''; + + /** + * Contains the base URL that relative links should resolve to. + * + * @var string $url + */ + protected $url; + + /** + * Indicates whether content in the $html variable has been converted yet. + * + * @var boolean $_converted + * @see $html, $text + */ + protected $_converted = false; + + /** + * Contains URL addresses from links to be rendered in plain text. + * + * @var array $_link_list + * @see _build_link_list() + */ + protected $_link_list = array(); + + /** + * Boolean flag, true if a table of link URLs should be listed after the text. + * + * @var boolean $_do_links + * @see __construct() + */ + protected $_do_links = true; + + /** + * Constructor. + * + * If the HTML source string (or file) is supplied, the class + * will instantiate with that source propagated, all that has + * to be done it to call get_text(). + * + * @param string $source HTML content + * @param boolean $from_file Indicates $source is a file to pull content from + * @param boolean $do_links Indicate whether a table of link URLs is desired + * @param integer $width Maximum width of the formatted text, 0 for no limit + */ + function __construct($source = '', $from_file = false, $do_links = true, $width = 75, $charset = 'UTF-8') + { + if (!empty($source)) { + $this->set_html($source, $from_file); + } + + $this->set_base_url(); + + $this->_do_links = $do_links; + $this->width = $width; + $this->charset = $charset; + } + + /** + * Loads source HTML into memory, either from $source string or a file. + * + * @param string $source HTML content + * @param boolean $from_file Indicates $source is a file to pull content from + */ + function set_html($source, $from_file = false) + { + if ($from_file && file_exists($source)) { + $this->html = file_get_contents($source); + } + else { + $this->html = $source; + } + + $this->_converted = false; + } + + /** + * Returns the text, converted from HTML. + * + * @return string Plain text + */ + function get_text() + { + if (!$this->_converted) { + $this->_convert(); + } + + return $this->text; + } + + /** + * Prints the text, converted from HTML. + */ + function print_text() + { + print $this->get_text(); + } + + /** + * Sets the allowed HTML tags to pass through to the resulting text. + * + * Tags should be in the form "<p>", with no corresponding closing tag. + */ + function set_allowed_tags($allowed_tags = '') + { + if (!empty($allowed_tags)) { + $this->allowed_tags = $allowed_tags; + } + } + + /** + * Sets a base URL to handle relative links. + */ + function set_base_url($url = '') + { + if (empty($url)) { + if (!empty($_SERVER['HTTP_HOST'])) { + $this->url = 'http://' . $_SERVER['HTTP_HOST']; + } + else { + $this->url = ''; + } + } + else { + // Strip any trailing slashes for consistency (relative + // URLs may already start with a slash like "/file.html") + if (substr($url, -1) == '/') { + $url = substr($url, 0, -1); + } + $this->url = $url; + } + } + + /** + * Workhorse function that does actual conversion (calls _converter() method). + */ + protected function _convert() + { + // Variables used for building the link list + $this->_link_list = array(); + + $text = trim(stripslashes($this->html)); + + // Convert HTML to TXT + $this->_converter($text); + + // Add link list + if (!empty($this->_link_list)) { + $text .= "\n\nLinks:\n------\n"; + foreach ($this->_link_list as $idx => $url) { + $text .= '[' . ($idx+1) . '] ' . $url . "\n"; + } + } + + $this->text = $text; + $this->_converted = true; + } + + /** + * Workhorse function that does actual conversion. + * + * First performs custom tag replacement specified by $search and + * $replace arrays. Then strips any remaining HTML tags, reduces whitespace + * and newlines to a readable format, and word wraps the text to + * $width characters. + * + * @param string Reference to HTML content string + */ + protected function _converter(&$text) + { + // Convert <BLOCKQUOTE> (before PRE!) + $this->_convert_blockquotes($text); + + // Convert <PRE> + $this->_convert_pre($text); + + // Run our defined tags search-and-replace + $text = preg_replace($this->search, $this->replace, $text); + + // Run our defined tags search-and-replace with callback + $text = preg_replace_callback($this->callback_search, array($this, 'tags_preg_callback'), $text); + + // Strip any other HTML tags + $text = strip_tags($text, $this->allowed_tags); + + // Run our defined entities/characters search-and-replace + $text = preg_replace($this->ent_search, $this->ent_replace, $text); + + // Replace known html entities + $text = html_entity_decode($text, ENT_QUOTES, $this->charset); + + // Remove unknown/unhandled entities (this cannot be done in search-and-replace block) + $text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text); + + // Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities + // This properly handles situation of "&quot;" in input string + $text = str_replace('|+|amp|+|', '&', $text); + + // Bring down number of empty lines to 2 max + $text = preg_replace("/\n\s+\n/", "\n\n", $text); + $text = preg_replace("/[\n]{3,}/", "\n\n", $text); + + // remove leading empty lines (can be produced by eg. P tag on the beginning) + $text = ltrim($text, "\n"); + + // Wrap the text to a readable format + // for PHP versions >= 4.0.2. Default width is 75 + // If width is 0 or less, don't wrap the text. + if ( $this->width > 0 ) { + $text = wordwrap($text, $this->width); + } + } + + /** + * Helper function called by preg_replace() on link replacement. + * + * Maintains an internal list of links to be displayed at the end of the + * text, with numeric indices to the original point in the text they + * appeared. Also makes an effort at identifying and handling absolute + * and relative links. + * + * @param string $link URL of the link + * @param string $display Part of the text to associate number with + */ + protected function _build_link_list( $link, $display ) + { + if (!$this->_do_links || empty($link)) { + return $display; + } + + // Ignored link types + if (preg_match('!^(javascript:|mailto:|#)!i', $link)) { + return $display; + } + + if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) { + $url = $link; + } + else { + $url = $this->url; + if (substr($link, 0, 1) != '/') { + $url .= '/'; + } + $url .= "$link"; + } + + if (($index = array_search($url, $this->_link_list)) === false) { + $index = count($this->_link_list); + $this->_link_list[] = $url; + } + + return $display . ' [' . ($index+1) . ']'; + } + + /** + * Helper function for PRE body conversion. + * + * @param string HTML content + */ + protected function _convert_pre(&$text) + { + // get the content of PRE element + while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) { + $this->pre_content = $matches[1]; + + // Run our defined tags search-and-replace with callback + $this->pre_content = preg_replace_callback($this->callback_search, + array($this, 'tags_preg_callback'), $this->pre_content); + + // convert the content + $this->pre_content = sprintf('<div><br>%s<br></div>', + preg_replace($this->pre_search, $this->pre_replace, $this->pre_content)); + + // replace the content (use callback because content can contain $0 variable) + $text = preg_replace_callback('/<pre[^>]*>.*<\/pre>/ismU', + array($this, 'pre_preg_callback'), $text, 1); + + // free memory + $this->pre_content = ''; + } + } + + /** + * Helper function for BLOCKQUOTE body conversion. + * + * @param string HTML content + */ + protected function _convert_blockquotes(&$text) + { + $level = 0; + $offset = 0; + while (($start = strpos($text, '<blockquote', $offset)) !== false) { + $offset = $start + 12; + do { + $end = strpos($text, '</blockquote>', $offset); + $next = strpos($text, '<blockquote', $offset); + + // nested <blockquote>, skip + if ($next !== false && $next < $end) { + $offset = $next + 12; + $level++; + } + // nested </blockquote> tag + if ($end !== false && $level > 0) { + $offset = $end + 12; + $level--; + } + // found matching end tag + else if ($end !== false && $level == 0) { + $taglen = strpos($text, '>', $start) - $start; + $startpos = $start + $taglen + 1; + + // get blockquote content + $body = trim(substr($text, $startpos, $end - $startpos)); + + // adjust text wrapping width + $p_width = $this->width; + if ($this->width > 0) $this->width -= 2; + + // replace content with inner blockquotes + $this->_converter($body); + + // resore text width + $this->width = $p_width; + + // Add citation markers and create <pre> block + $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body)); + $body = '<pre>' . htmlspecialchars($body) . '</pre>'; + + $text = substr($text, 0, $start) . $body . "\n" . substr($text, $end + 13); + $offset = 0; + break; + } + } while ($end || $next); + } + } + + /** + * Callback function to correctly add citation markers for blockquote contents + */ + public function blockquote_citation_ballback($m) + { + $line = ltrim($m[2]); + $space = $line[0] == '>' ? '' : ' '; + return $m[1] . '>' . $space . $line; + } + + /** + * Callback function for preg_replace_callback use. + * + * @param array PREG matches + * @return string + */ + public function tags_preg_callback($matches) + { + switch (strtolower($matches[1])) { + case 'b': + case 'strong': + return $this->_toupper($matches[3]); + case 'th': + return $this->_toupper("\t\t". $matches[3] ."\n"); + case 'h': + return $this->_toupper("\n\n". $matches[3] ."\n\n"); + case 'a': + // Remove spaces in URL (#1487805) + $url = str_replace(' ', '', $matches[3]); + return $this->_build_link_list($url, $matches[4]); + } + } + + /** + * Callback function for preg_replace_callback use in PRE content handler. + * + * @param array PREG matches + * @return string + */ + public function pre_preg_callback($matches) + { + return $this->pre_content; + } + + /** + * Strtoupper function with HTML tags and entities handling. + * + * @param string $str Text to convert + * @return string Converted text + */ + private function _toupper($str) + { + // string can containg HTML tags + $chunks = preg_split('/(<[^>]*>)/', $str, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + + // convert toupper only the text between HTML tags + foreach ($chunks as $idx => $chunk) { + if ($chunk[0] != '<') { + $chunks[$idx] = $this->_strtoupper($chunk); + } + } + + return implode($chunks); + } + + /** + * Strtoupper multibyte wrapper function with HTML entities handling. + * + * @param string $str Text to convert + * @return string Converted text + */ + private function _strtoupper($str) + { + $str = html_entity_decode($str, ENT_COMPAT, $this->charset); + $str = mb_strtoupper($str); + $str = htmlspecialchars($str, ENT_COMPAT, $this->charset); + + return $str; + } +} diff --git a/program/lib/Roundcube/rcube_image.php b/program/lib/Roundcube/rcube_image.php index b72a24c51..a55ba1600 100644 --- a/program/lib/Roundcube/rcube_image.php +++ b/program/lib/Roundcube/rcube_image.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_image.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,7 +12,6 @@ | | | PURPOSE: | | Image resizer and converter | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | @@ -80,7 +77,8 @@ class rcube_image } /** - * Resize image to a given size + * Resize image to a given size. Use only to shrink an image. + * If an image is smaller than specified size it will be not resized. * * @param int $size Max width/height size * @param string $filename Output filename @@ -131,19 +129,33 @@ class rcube_image } // use GD extension - $gd_types = array(IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG); - if ($props['gd_type'] && in_array($props['gd_type'], $gd_types)) { - if ($props['gd_type'] == IMAGETYPE_JPEG) { + if ($props['gd_type']) { + if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) { $image = imagecreatefromjpeg($this->image_file); + $type = 'jpg'; } - elseif($props['gd_type'] == IMAGETYPE_GIF) { + else if($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) { $image = imagecreatefromgif($this->image_file); + $type = 'gid'; } - elseif($props['gd_type'] == IMAGETYPE_PNG) { + else if($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) { $image = imagecreatefrompng($this->image_file); + $type = 'png'; + } + else { + // @TODO: print error to the log? + return false; + } + + $scale = $size / max($props['width'], $props['height']); + + // Imagemagick resize is implemented in shrinking mode (see -resize argument above) + // we do the same here, if an image is smaller than specified size + // we do nothing but copy original file to destination file + if ($scale > 1) { + return $this->image_file == $filename || copy($this->image_file, $filename) ? $type : false; } - $scale = $size / max($props['width'], $props['height']); $width = $props['width'] * $scale; $height = $props['height'] * $scale; @@ -162,15 +174,12 @@ class rcube_image if ($props['gd_type'] == IMAGETYPE_JPEG) { $result = imagejpeg($image, $filename, 75); - $type = 'jpg'; } elseif($props['gd_type'] == IMAGETYPE_GIF) { $result = imagegif($image, $filename); - $type = 'gid'; } elseif($props['gd_type'] == IMAGETYPE_PNG) { $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS); - $type = 'png'; } if ($result) { @@ -219,19 +228,22 @@ class rcube_image } // use GD extension (TIFF isn't supported) - $props = $this->props(); - $gd_types = array(IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG); + $props = $this->props(); - if ($props['gd_type'] && in_array($props['gd_type'], $gd_types)) { - if ($props['gd_type'] == IMAGETYPE_JPEG) { + if ($props['gd_type']) { + if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) { $image = imagecreatefromjpeg($this->image_file); } - else if ($props['gd_type'] == IMAGETYPE_GIF) { + else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) { $image = imagecreatefromgif($this->image_file); } - else if ($props['gd_type'] == IMAGETYPE_PNG) { + else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) { $image = imagecreatefrompng($this->image_file); } + else { + // @TODO: print error to the log? + return false; + } if ($type == self::TYPE_JPG) { $result = imagejpeg($image, $filename, 75); @@ -242,6 +254,10 @@ class rcube_image else if ($type == self::TYPE_PNG) { $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS); } + + if ($result) { + return true; + } } // @TODO: print error to the log? diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 8ca24dec7..c67985186 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_imap.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,14 +12,12 @@ | | | PURPOSE: | | IMAP Storage Engine | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Interface class for accessing an IMAP server * @@ -151,7 +147,7 @@ class rcube_imap extends rcube_storage $attempt = 0; do { - $data = rcube::get_instance()->plugins->exec_hook('imap_connect', + $data = rcube::get_instance()->plugins->exec_hook('storage_connect', array_merge($this->options, array('host' => $host, 'user' => $user, 'attempt' => ++$attempt))); @@ -571,7 +567,7 @@ class rcube_imap extends rcube_storage * Get message count for a specific folder * * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] + * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] * @param boolean $force Force reading from server and update cache * @param boolean $status Enables storing folder status info (max UID/count), * required for folder_status() @@ -592,7 +588,7 @@ class rcube_imap extends rcube_storage * protected method for getting nr of messages * * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] + * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] * @param boolean $force Force reading from server and update cache * @param boolean $status Enables storing folder status info (max UID/count), * required for folder_status() @@ -614,6 +610,10 @@ class rcube_imap extends rcube_storage } } + // EXISTS is a special alias for ALL, it allows to get the number + // of all messages in a folder also when search is active and with + // any skip_deleted setting + $a_folder_cache = $this->get_cache('messagecount'); // return cached value @@ -644,7 +644,7 @@ class rcube_imap extends rcube_storage $count = $this->conn->countRecent($folder); } // use SEARCH for message counting - else if (!empty($this->options['skip_deleted'])) { + else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) { $search_str = "ALL UNDELETED"; $keys = array('COUNT'); @@ -683,8 +683,8 @@ class rcube_imap extends rcube_storage } else { $count = $this->conn->countMessages($folder); - if ($status) { - $this->set_folder_stats($folder,'cnt', $count); + if ($status && $mode == 'ALL') { + $this->set_folder_stats($folder, 'cnt', $count); $this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0); } } @@ -1096,16 +1096,17 @@ class rcube_imap extends rcube_storage /** - * Returns current status of folder + * Returns current status of a folder (compared to the last time use) * * We compare the maximum UID to determine the number of * new messages because the RECENT flag is not reliable. * * @param string $folder Folder name + * @param array $diff Difference data * - * @return int Folder status + * @return int Folder status */ - public function folder_status($folder = null) + public function folder_status($folder = null, &$diff = array()) { if (!strlen($folder)) { $folder = $this->folder; @@ -1126,6 +1127,9 @@ class rcube_imap extends rcube_storage // got new messages if ($new['maxuid'] > $old['maxuid']) { $result += 1; + // get new message UIDs range, that can be used for example + // to get the data of these messages + $diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid']; } // some messages has been deleted if ($new['cnt'] < $old['cnt']) { @@ -1634,9 +1638,15 @@ class rcube_imap extends rcube_storage // Example of structure for malformed MIME message: // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL) if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain' - && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') { + && strtolower($structure[0].'/'.$structure[1]) == 'text/plain' + ) { + // A special known case "Content-type: text" (#1488968) + if ($headers->ctype == 'text') { + $structure[1] = 'plain'; + $headers->ctype = 'text/plain'; + } // we can handle single-part messages, by simple fix in structure (#1486898) - if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) { + else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) { $structure[0] = $m[1]; $structure[1] = $m[2]; } @@ -1660,11 +1670,21 @@ class rcube_imap extends rcube_storage $struct = $this->structure_part($structure, 0, '', $headers); } - // don't trust given content-type - if (empty($struct->parts) && !empty($headers->ctype)) { - $struct->mime_id = '1'; - $struct->mimetype = strtolower($headers->ctype); - list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype); + // some workarounds on simple messages... + if (empty($struct->parts)) { + // ...don't trust given content-type + if (!empty($headers->ctype)) { + $struct->mime_id = '1'; + $struct->mimetype = strtolower($headers->ctype); + list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype); + } + + // ...and charset (there's a case described in #1488968 where invalid content-type + // results in invalid charset in BODYSTRUCTURE) + if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) { + $struct->charset = $headers->charset; + $struct->ctype_parameters['charset'] = $headers->charset; + } } $headers->structure = $struct; @@ -2226,10 +2246,11 @@ class rcube_imap extends rcube_storage * @param boolean $is_file True if $message is a filename * @param array $flags Message flags * @param mixed $date Message internal date + * @param bool $binary Enables BINARY append * * @return int|bool Appended message UID or True on success, False on error */ - public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null) + public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false) { if (!strlen($folder)) { $folder = $this->folder; @@ -2247,10 +2268,10 @@ class rcube_imap extends rcube_storage $date = $this->date_format($date); if ($is_file) { - $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date); + $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary); } else { - $saved = $this->conn->append($folder, $message, $flags, $date); + $saved = $this->conn->append($folder, $message, $flags, $date, $binary); } if ($saved) { @@ -2316,10 +2337,7 @@ class rcube_imap extends rcube_storage // move messages $moved = $this->conn->move($uids, $from_mbox, $to_mbox); - // send expunge command in order to have the moved message - // really deleted from the source folder if ($moved) { - $this->expunge_message($uids, $from_mbox, false); $this->clear_messagecount($from_mbox); $this->clear_messagecount($to_mbox); } @@ -3354,7 +3372,6 @@ class rcube_imap extends rcube_storage { if (!empty($this->options['fetch_headers'])) { $headers = explode(' ', $this->options['fetch_headers']); - $headers = array_map('strtoupper', $headers); } else { $headers = array(); @@ -3364,7 +3381,7 @@ class rcube_imap extends rcube_storage $headers = array_merge($headers, $this->all_headers); } - return implode(' ', array_unique($headers)); + return $headers; } diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php index 31214cfbf..748474af2 100644 --- a/program/lib/Roundcube/rcube_imap_cache.php +++ b/program/lib/Roundcube/rcube_imap_cache.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_imap_cache.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -13,14 +11,12 @@ | | | PURPOSE: | | Caching of IMAP folder contents (messages and index) | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Interface class for accessing Roundcube messages cache * @@ -489,7 +485,7 @@ class rcube_imap_cache .", flags = flags ".($enabled ? "+ $idx" : "- $idx") ." WHERE user_id = ?" ." AND mailbox = ?" - .($uids !== null ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "") + .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "") ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"), $this->userid, $mailbox); } diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index 70fd6eb2c..04dc594ae 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_imap_generic.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -19,14 +17,12 @@ | functionality built-in. | | | | Based on Iloha IMAP Library. See http://ilohamail.org/ for details | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | | Author: Ryo Chijiiwa <Ryo@IlohaMail.org> | +-----------------------------------------------------------------------+ */ - /** * PHP based wrapper class to connect to an IMAP server * @@ -757,12 +753,16 @@ class rcube_imap_generic $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); if (!$this->fp) { + if (!$errstr) { + $errstr = "Unknown reason (fsockopen() function disabled?)"; + } $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr)); return false; } - if ($this->prefs['timeout'] > 0) + if ($this->prefs['timeout'] > 0) { stream_set_timeout($this->fp, $this->prefs['timeout']); + } $line = trim(fgets($this->fp, 8192)); @@ -906,7 +906,7 @@ class rcube_imap_generic */ function closeConnection() { - if ($this->putLine($this->nextTag() . ' LOGOUT')) { + if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) { $this->readReply(); } @@ -1065,8 +1065,8 @@ class rcube_imap_generic /** * Executes EXPUNGE command * - * @param string $mailbox Mailbox name - * @param string $messages Message UIDs to expunge + * @param string $mailbox Mailbox name + * @param string|array $messages Message UIDs to expunge * * @return boolean True on success, False on error */ @@ -1084,10 +1084,13 @@ class rcube_imap_generic // Clear internal status cache unset($this->data['STATUS:'.$mailbox]); - if ($messages) - $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE); - else + if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) { + $messages = self::compressMessageSet($messages); + $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE); + } + else { $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE); + } if ($result == self::ERROR_OK) { $this->selected = null; // state has changed, need to reselect @@ -1311,6 +1314,11 @@ class rcube_imap_generic if ($cmd == 'LIST' || $cmd == 'LSUB') { list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3); + // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879) + if ($delim) { + $mailbox = rtrim($mailbox, $delim); + } + // Add to result array if (!$lstatus) { $folders[] = $mailbox; @@ -1975,7 +1983,6 @@ class rcube_imap_generic /** * Moves message(s) from one folder to another. - * Original message(s) will be marked as deleted. * * @param string|array $messages Message UID(s) * @param string $from Mailbox name @@ -1994,15 +2001,41 @@ class rcube_imap_generic return false; } - $r = $this->copy($messages, $from, $to); + // use MOVE command (RFC 6851) + if ($this->hasCapability('MOVE')) { + // Clear last COPYUID data + unset($this->data['COPYUID']); - if ($r) { // Clear internal status cache + unset($this->data['STATUS:'.$to]); unset($this->data['STATUS:'.$from]); - return $this->flag($from, $messages, 'DELETED'); + $result = $this->execute('UID MOVE', array( + $this->compressMessageSet($messages), $this->escape($to)), + self::COMMAND_NORESPONSE); + + return ($result == self::ERROR_OK); } - return $r; + + // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE + $result = $this->copy($messages, $from, $to); + + if ($result) { + // Clear internal status cache + unset($this->data['STATUS:'.$from]); + + $result = $this->flag($from, $messages, 'DELETED'); + + if ($messages == '*') { + // CLOSE+SELECT should be faster than EXPUNGE + $this->close(); + } + else { + $this->expunge($from, $messages); + } + } + + return $result; } /** @@ -2206,10 +2239,13 @@ class rcube_imap_generic } break; default: - if (strlen($field) > 2) { - $result[$id]->others[$field] = $string; + if (strlen($field) < 3) { + break; } - break; + if ($result[$id]->others[$field]) { + $string = array_merge((array)$result[$id]->others[$field], (array)$string); + } + $result[$id]->others[$field] = $string; } } } @@ -2217,7 +2253,6 @@ class rcube_imap_generic // VANISHED response (QRESYNC RFC5162) // Sample: * VANISHED (EARLIER) 300:310,405,411 - else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { $line = substr($line, strlen($match[0])); $v_data = $this->tokenizeResponse($line, 1); @@ -2230,24 +2265,53 @@ class rcube_imap_generic return $result; } - function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '') + /** + * Returns message(s) data (flags, headers, etc.) + * + * @param string $mailbox Mailbox name + * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) + * @param bool $is_uid True if $message_set contains UIDs + * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result + * @param array $add_headers List of additional headers + * + * @return bool|array List of rcube_message_header elements, False on error + */ + function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array()) { $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'); - if ($bodystr) + $headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', + 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY'); + + if (!empty($add_headers)) { + $add_headers = array_map('strtoupper', $add_headers); + $headers = array_unique(array_merge($headers, $add_headers)); + } + + if ($bodystr) { $query_items[] = 'BODYSTRUCTURE'; - $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' - . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY' - . ($add ? ' ' . trim($add) : '') - . ')]'; + } + + $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]'; $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items); return $result; } - function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='') + /** + * Returns message data (flags, headers, etc.) + * + * @param string $mailbox Mailbox name + * @param int $id Message sequence identifier or UID + * @param bool $is_uid True if $id is an UID + * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result + * @param array $add_headers List of additional headers + * + * @return bool|rcube_message_header Message data, False on error + */ + function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array()) { - $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add); + $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); if (is_array($a)) { return array_shift($a); } @@ -2408,8 +2472,9 @@ class rcube_imap_generic $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : ''; // format request - $key = $this->nextTag(); - $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; + $key = $this->nextTag(); + $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; + $result = false; // send request if (!$this->putLine($request)) { @@ -2422,118 +2487,117 @@ class rcube_imap_generic $mode = -1; } - // receive reply line do { - $line = rtrim($this->readLine(1024)); - $a = explode(' ', $line); - } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH'); - - $len = strlen($line); - $result = false; + $line = trim($this->readLine(1024)); - if ($a[2] != 'FETCH') { - } - // handle empty "* X FETCH ()" response - else if ($line[$len-1] == ')' && $line[$len-2] != '(') { - // one line response, get everything between first and last quotes - if (substr($line, -4, 3) == 'NIL') { - // NIL response - $result = ''; - } else { - $from = strpos($line, '"') + 1; - $to = strrpos($line, '"'); - $len = $to - $from; - $result = substr($line, $from, $len); + if (!$line) { + break; } - if ($mode == 1) { - $result = base64_decode($result); - } - else if ($mode == 2) { - $result = quoted_printable_decode($result); - } - else if ($mode == 3) { - $result = convert_uudecode($result); + if (!preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { + continue; } - } else if ($line[$len-1] == '}') { - // multi-line request, find sizes of content and receive that many bytes - $from = strpos($line, '{') + 1; - $to = strrpos($line, '}'); - $len = $to - $from; - $sizeStr = substr($line, $from, $len); - $bytes = (int)$sizeStr; - $prev = ''; + $line = $m[2]; + $last = substr($line, -1); - while ($bytes > 0) { - $line = $this->readLine(8192); + // handle one line response + if ($line[0] == '(' && $last == ')') { + // tokenize content inside brackets + $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\$)/', '', $line)); + $result = count($tokens) == 1 ? $tokens[0] : false; - if ($line === NULL) { - break; + if ($result !== false) { + if ($mode == 1) { + $result = base64_decode($result); + } + else if ($mode == 2) { + $result = quoted_printable_decode($result); + } + else if ($mode == 3) { + $result = convert_uudecode($result); + } } + } + // response with string literal + else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { + $bytes = (int) $m[1]; + $prev = ''; + + while ($bytes > 0) { + $line = $this->readLine(8192); - $len = strlen($line); + if ($line === NULL) { + break; + } - if ($len > $bytes) { - $line = substr($line, 0, $bytes); $len = strlen($line); - } - $bytes -= $len; - - // BASE64 - if ($mode == 1) { - $line = rtrim($line, "\t\r\n\0\x0B"); - // create chunks with proper length for base64 decoding - $line = $prev.$line; - $length = strlen($line); - if ($length % 4) { - $length = floor($length / 4) * 4; - $prev = substr($line, $length); - $line = substr($line, 0, $length); + + if ($len > $bytes) { + $line = substr($line, 0, $bytes); + $len = strlen($line); + } + $bytes -= $len; + + // BASE64 + if ($mode == 1) { + $line = rtrim($line, "\t\r\n\0\x0B"); + // create chunks with proper length for base64 decoding + $line = $prev.$line; + $length = strlen($line); + if ($length % 4) { + $length = floor($length / 4) * 4; + $prev = substr($line, $length); + $line = substr($line, 0, $length); + } + else { + $prev = ''; + } + $line = base64_decode($line); + } + // QUOTED-PRINTABLE + else if ($mode == 2) { + $line = rtrim($line, "\t\r\0\x0B"); + $line = quoted_printable_decode($line); + } + // UUENCODE + else if ($mode == 3) { + $line = rtrim($line, "\t\r\n\0\x0B"); + if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) { + continue; + } + $line = convert_uudecode($line); + } + // default + else if ($formatted) { + $line = rtrim($line, "\t\r\n\0\x0B") . "\n"; } - else - $prev = ''; - $line = base64_decode($line); - // QUOTED-PRINTABLE - } else if ($mode == 2) { - $line = rtrim($line, "\t\r\0\x0B"); - $line = quoted_printable_decode($line); - // UUENCODE - } else if ($mode == 3) { - $line = rtrim($line, "\t\r\n\0\x0B"); - if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) - continue; - $line = convert_uudecode($line); - // default - } else if ($formatted) { - $line = rtrim($line, "\t\r\n\0\x0B") . "\n"; - } - if ($file) { - if (fwrite($file, $line) === false) - break; + if ($file) { + if (fwrite($file, $line) === false) { + break; + } + } + else if ($print) { + echo $line; + } + else { + $result .= $line; + } } - else if ($print) - echo $line; - else - $result .= $line; } - } - - // read in anything up until last line - if (!$end) - do { - $line = $this->readLine(1024); - } while (!$this->startsWith($line, $key, true)); + } while (!$this->startsWith($line, $key, true)); if ($result !== false) { if ($file) { return fwrite($file, $result); - } else if ($print) { + } + else if ($print) { echo $result; - } else - return $result; - return true; + return true; + } + + return $result; } return false; @@ -2546,10 +2610,11 @@ class rcube_imap_generic * @param string $message Message content * @param array $flags Message flags * @param string $date Message internal date + * @param bool $binary Enable BINARY append (RFC3516) * * @return string|bool On success APPENDUID response (if available) or True, False on failure */ - function append($mailbox, &$message, $flags = array(), $date = null) + function append($mailbox, &$message, $flags = array(), $date = null, $binary = false) { unset($this->data['APPENDUID']); @@ -2557,8 +2622,13 @@ class rcube_imap_generic return false; } - $message = str_replace("\r", '', $message); - $message = str_replace("\n", "\r\n", $message); + $binary = $binary && $this->getCapability('BINARY'); + $literal_plus = !$binary && $this->prefs['literal+']; + + if (!$binary) { + $message = str_replace("\r", '', $message); + $message = str_replace("\n", "\r\n", $message); + } $len = strlen($message); if (!$len) { @@ -2571,12 +2641,12 @@ class rcube_imap_generic if (!empty($date)) { $request .= ' ' . $this->escape($date); } - $request .= ' {' . $len . ($this->prefs['literal+'] ? '+' : '') . '}'; + $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}'; // send APPEND command if ($this->putLine($request)) { // Do not wait when LITERAL+ is supported - if (!$this->prefs['literal+']) { + if (!$literal_plus) { $line = $this->readReply(); if ($line[0] != '+') { @@ -2618,10 +2688,11 @@ class rcube_imap_generic * @param string $headers Message headers * @param array $flags Message flags * @param string $date Message internal date + * @param bool $binary Enable BINARY append (RFC3516) * * @return string|bool On success APPENDUID response (if available) or True, False on failure */ - function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null) + function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false) { unset($this->data['APPENDUID']); @@ -2652,18 +2723,21 @@ class rcube_imap_generic $len += strlen($headers) + strlen($body_separator); } + $binary = $binary && $this->getCapability('BINARY'); + $literal_plus = !$binary && $this->prefs['literal+']; + // build APPEND command $key = $this->nextTag(); $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; if (!empty($date)) { $request .= ' ' . $this->escape($date); } - $request .= ' {' . $len . ($this->prefs['literal+'] ? '+' : '') . '}'; + $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}'; // send APPEND command if ($this->putLine($request)) { // Don't wait when LITERAL+ is supported - if (!$this->prefs['literal+']) { + if (!$literal_plus) { $line = $this->readReply(); if ($line[0] != '+') { @@ -3485,7 +3559,7 @@ class rcube_imap_generic // if less than 255 bytes long, let's not bother if (!$force && strlen($messages)<255) { return $messages; - } + } // see if it's already been compressed if (strpos($messages, ':') !== false) { diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index c9a14d863..a2dd163e9 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_ldap.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2006-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,7 +12,6 @@ | | | PURPOSE: | | Interface to an LDAP address directory | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Andreas Dick <andudi (at) gmx (dot) ch> | @@ -22,7 +19,6 @@ +-----------------------------------------------------------------------+ */ - /** * Model class to access an LDAP address directory * @@ -218,15 +214,16 @@ class rcube_ldap extends rcube_addressbook if (empty($this->prop['ldap_version'])) $this->prop['ldap_version'] = 3; - foreach ($this->prop['hosts'] as $host) - { + // try to connect + bind for every host configured + // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable + // see http://www.php.net/manual/en/function.ldap-connect.php + foreach ($this->prop['hosts'] as $host) { $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : ''); $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]"); - if ($lc = @ldap_connect($host, $this->prop['port'])) - { + if ($lc = @ldap_connect($host, $this->prop['port'])) { if ($this->prop['use_tls'] === true) if (!ldap_start_tls($lc)) continue; @@ -237,113 +234,124 @@ class rcube_ldap extends rcube_addressbook $this->prop['host'] = $host; $this->conn = $lc; + if (!empty($this->prop['network_timeout'])) + ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']); + if (isset($this->prop['referrals'])) ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']); - break; } - $this->_debug("S: NOT OK"); - } - - // See if the directory is writeable. - if ($this->prop['writable']) { - $this->readonly = false; - } - - if (!is_resource($this->conn)) { - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not connect to any LDAP server, last tried $hostname"), true); + else { + $this->_debug("S: NOT OK"); + continue; + } - return false; - } + // See if the directory is writeable. + if ($this->prop['writable']) { + $this->readonly = false; + } - $bind_pass = $this->prop['bind_pass']; - $bind_user = $this->prop['bind_user']; - $bind_dn = $this->prop['bind_dn']; + $bind_pass = $this->prop['bind_pass']; + $bind_user = $this->prop['bind_user']; + $bind_dn = $this->prop['bind_dn']; - $this->base_dn = $this->prop['base_dn']; - $this->groups_base_dn = ($this->prop['groups']['base_dn']) ? - $this->prop['groups']['base_dn'] : $this->base_dn; + $this->base_dn = $this->prop['base_dn']; + $this->groups_base_dn = ($this->prop['groups']['base_dn']) ? + $this->prop['groups']['base_dn'] : $this->base_dn; - // User specific access, generate the proper values to use. - if ($this->prop['user_specific']) { - // No password set, use the session password - if (empty($bind_pass)) { - $bind_pass = $rcube->get_user_password(); - } + // User specific access, generate the proper values to use. + if ($this->prop['user_specific']) { + // No password set, use the session password + if (empty($bind_pass)) { + $bind_pass = $rcube->get_user_password(); + } - // Get the pieces needed for variable replacement. - if ($fu = $rcube->get_user_email()) - list($u, $d) = explode('@', $fu); - else - $d = $this->mail_domain; + // Get the pieces needed for variable replacement. + if ($fu = $rcube->get_user_email()) + list($u, $d) = explode('@', $fu); + else + $d = $this->mail_domain; - $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string + $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string - $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); + $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); - if ($this->prop['search_base_dn'] && $this->prop['search_filter']) { - if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) { - $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']); - } + if ($this->prop['search_base_dn'] && $this->prop['search_filter']) { + if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) { + $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']); + } - // Search for the dn to use to authenticate - $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces); - $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces); + // Search for the dn to use to authenticate + $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces); + $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces); - $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}"); + $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}"); - $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid')); - if ($res) { - if (($entry = ldap_first_entry($this->conn, $res)) - && ($bind_dn = ldap_get_dn($this->conn, $entry)) - ) { - $this->_debug("S: search returned dn: $bind_dn"); - $dn = ldap_explode_dn($bind_dn, 1); - $replaces['%dn'] = $dn[0]; + $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid')); + if ($res) { + if (($entry = ldap_first_entry($this->conn, $res)) + && ($bind_dn = ldap_get_dn($this->conn, $entry)) + ) { + $this->_debug("S: search returned dn: $bind_dn"); + $dn = ldap_explode_dn($bind_dn, 1); + $replaces['%dn'] = $dn[0]; + } } - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - - // DN not found - if (empty($replaces['%dn'])) { - if (!empty($this->prop['search_dn_default'])) - $replaces['%dn'] = $this->prop['search_dn_default']; else { - rcube::raise_error(array( - 'code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "DN not found using LDAP search."), true); - return false; + $this->_debug("S: ".ldap_error($this->conn)); + } + + // DN not found + if (empty($replaces['%dn'])) { + if (!empty($this->prop['search_dn_default'])) + $replaces['%dn'] = $this->prop['search_dn_default']; + else { + rcube::raise_error(array( + 'code' => 100, 'type' => 'ldap', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "DN not found using LDAP search."), true); + return false; + } } } - } - // Replace the bind_dn and base_dn variables. - $bind_dn = strtr($bind_dn, $replaces); - $this->base_dn = strtr($this->base_dn, $replaces); - $this->groups_base_dn = strtr($this->groups_base_dn, $replaces); + // Replace the bind_dn and base_dn variables. + $bind_dn = strtr($bind_dn, $replaces); + $this->base_dn = strtr($this->base_dn, $replaces); + $this->groups_base_dn = strtr($this->groups_base_dn, $replaces); - if (empty($bind_user)) { - $bind_user = $u; + if (empty($bind_user)) { + $bind_user = $u; + } } - } - if (empty($bind_pass)) { - $this->ready = true; - } - else { - if (!empty($bind_dn)) { - $this->ready = $this->bind($bind_dn, $bind_pass); - } - else if (!empty($this->prop['auth_cid'])) { - $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user); + if (empty($bind_pass)) { + $this->ready = true; } else { - $this->ready = $this->sasl_bind($bind_user, $bind_pass); + if (!empty($bind_dn)) { + $this->ready = $this->bind($bind_dn, $bind_pass); + } + else if (!empty($this->prop['auth_cid'])) { + $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user); + } + else { + $this->ready = $this->sasl_bind($bind_user, $bind_pass); + } + } + + // connection established, we're done here + if ($this->ready) { + break; } + + } // end foreach hosts + + if (!is_resource($this->conn)) { + rcube::raise_error(array('code' => 100, 'type' => 'ldap', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not connect to any LDAP server, last tried $hostname"), true); + + return false; } return $this->ready; @@ -798,27 +806,14 @@ class rcube_ldap extends rcube_addressbook $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)"); // get all entries of this page and post-filter those that really match the query - $search = mb_strtolower($value); + $search = mb_strtolower($value); $entries = ldap_get_entries($this->conn, $this->ldap_result); for ($i = 0; $i < $entries['count']; $i++) { $rec = $this->_ldap2result($entries[$i]); foreach ($fields as $f) { foreach ((array)$rec[$f] as $val) { - $val = mb_strtolower($val); - switch ($mode) { - case 1: - $got = ($val == $search); - break; - case 2: - $got = ($search == substr($val, 0, strlen($search))); - break; - default: - $got = (strpos($val, $search) !== false); - break; - } - - if ($got) { + if ($this->compare_search_value($f, $val, $search, $mode)) { $this->result->add($rec); $this->result->count++; break 2; @@ -1455,6 +1450,7 @@ class rcube_ldap extends rcube_addressbook if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) { if (ldap_parse_result($this->conn, $this->ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) + && $serverctrls // can be null e.g. in case of adm. limit error ) { ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $this->vlv_count, $vresult); diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php index 4ef534a0a..41a114f7f 100644 --- a/program/lib/Roundcube/rcube_message.php +++ b/program/lib/Roundcube/rcube_message.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_message.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2010, The Roundcube Dev Team | | | @@ -19,7 +17,6 @@ +-----------------------------------------------------------------------+ */ - /** * Logical representation of a mail message with all its data * and related functions @@ -96,7 +93,7 @@ class rcube_message $this->subject = $this->mime->decode_mime_string($this->headers->subject); list(, $this->sender) = each($this->mime->decode_address_list($this->headers->from, 1)); - $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid])); + $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$this->folder.':'.$uid])); $this->opt = array( 'safe' => $this->is_safe, 'prefer_html' => $this->app->config->get('prefer_html'), @@ -147,8 +144,7 @@ class rcube_message */ public function set_safe($safe = true) { - $this->is_safe = $safe; - $_SESSION['safe_messages'][$this->uid] = $this->is_safe; + $_SESSION['safe_messages'][$this->folder.':'.$this->uid] = $this->is_safe = $safe; } @@ -197,39 +193,82 @@ class rcube_message /** - * Determine if the message contains a HTML part + * Determine if the message contains a HTML part. This must to be + * a real part not an attachment (or its part) + * This must to be + * a real part not an attachment (or its part) * - * @param bool $recursive Enables checking in all levels of the structure - * @param bool $enriched Enables checking for text/enriched parts too + * @param bool $enriched Enables checking for text/enriched parts too * * @return bool True if a HTML is available, False if not */ - function has_html_part($recursive = true, $enriched = false) + function has_html_part($enriched = false) { // check all message parts - foreach ($this->parts as $part) { + foreach ($this->mime_parts as $part) { if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) { - // Level check, we'll skip e.g. HTML attachments - if (!$recursive) { - $level = explode('.', $part->mime_id); + // Skip if part is an attachment, don't use is_attachment() here + if ($part->filename) { + continue; + } - // Skip if level too deep or part has a file name - if (count($level) > 2 || $part->filename) { - continue; + $level = explode('.', $part->mime_id); + + // Check if the part belongs to higher-level's alternative/related + while (array_pop($level) !== null) { + if (!count($level)) { + return true; } - // HTML part can be on the lower level, if not... - if (count($level) > 1) { - array_pop($level); - $parent = $this->mime_parts[join('.', $level)]; - // ... parent isn't multipart/alternative or related - if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') { - continue; - } + $parent = $this->mime_parts[join('.', $level)]; + if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') { + continue 2; } } - return true; + if ($part->size) { + return true; + } + } + } + + return false; + } + + + /** + * Determine if the message contains a text/plain part. This must to be + * a real part not an attachment (or its part) + * + * @return bool True if a plain text part is available, False if not + */ + function has_text_part() + { + // check all message parts + foreach ($this->mime_parts as $part) { + if ($part->mimetype == 'text/plain') { + // Skip if part is an attachment, don't use is_attachment() here + if ($part->filename) { + continue; + } + + $level = explode('.', $part->mime_id); + + // Check if the part belongs to higher-level's alternative/related + while (array_pop($level) !== null) { + if (!count($level)) { + return true; + } + + $parent = $this->mime_parts[join('.', $level)]; + if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') { + continue 2; + } + } + + if ($part->size) { + return true; + } } } @@ -274,7 +313,7 @@ class rcube_message $out = $this->get_part_content($mime_id); // create instance of html2text class - $txt = new html2text($out); + $txt = new rcube_html2text($out); return $txt->get_text(); } } @@ -320,16 +359,23 @@ class rcube_message private function parse_structure($structure, $recursive = false) { // real content-type of message/rfc822 part - if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) + if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) { $mimetype = $structure->real_mimetype; + + // parse headers from message/rfc822 part + if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) { + list($headers, $dump) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 8192)); + $structure->headers = rcube_mime::parse_headers($headers); + } + } else $mimetype = $structure->mimetype; // show message headers - if ($recursive && is_array($structure->headers) && isset($structure->headers['subject'])) { + if ($recursive && is_array($structure->headers) && (isset($structure->headers['subject']) || isset($structure->headers['from']))) { $c = new stdClass; $c->type = 'headers'; - $c->headers = &$structure->headers; + $c->headers = $structure->headers; $this->parts[] = $c; } @@ -346,45 +392,59 @@ class rcube_message // print body if message doesn't have multiple parts if ($message_ctype_primary == 'text' && !$recursive) { + // parts with unsupported type add to attachments list + if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) { + $this->attachments[] = $structure; + return; + } + $structure->type = 'content'; - $this->parts[] = &$structure; + $this->parts[] = $structure; // Parse simple (plain text) message body - if ($message_ctype_secondary == 'plain') + if ($message_ctype_secondary == 'plain') { foreach ((array)$this->uu_decode($structure) as $uupart) { $this->mime_parts[$uupart->mime_id] = $uupart; $this->attachments[] = $uupart; } + } } // the same for pgp signed messages else if ($mimetype == 'application/pgp' && !$recursive) { $structure->type = 'content'; - $this->parts[] = &$structure; + $this->parts[] = $structure; } // message contains (more than one!) alternative parts else if ($mimetype == 'multipart/alternative' && is_array($structure->parts) && count($structure->parts) > 1 ) { - // get html/plaintext parts - $plain_part = $html_part = $print_part = $related_part = null; + $plain_part = null; + $html_part = null; + $print_part = null; + $related_part = null; + $attach_part = null; + // get html/plaintext parts, other add to attachments list foreach ($structure->parts as $p => $sub_part) { $sub_mimetype = $sub_part->mimetype; + $is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype); // skip empty text parts - if (!$sub_part->size && preg_match('#^text/(plain|html|enriched)$#', $sub_mimetype)) { + if (!$sub_part->size && !$is_multipart) { continue; } // check if sub part is - if ($sub_mimetype == 'text/plain') + if ($is_multipart) + $related_part = $p; + else if ($sub_mimetype == 'text/plain') $plain_part = $p; else if ($sub_mimetype == 'text/html') $html_part = $p; else if ($sub_mimetype == 'text/enriched') $enriched_part = $p; - else if (in_array($sub_mimetype, array('multipart/related', 'multipart/mixed', 'multipart/alternative'))) - $related_part = $p; + else + $attach_part = $p; } // parse related part (alternative part could be in here) @@ -400,13 +460,13 @@ class rcube_message // choose html/plain part to print if ($html_part !== null && $this->opt['prefer_html']) { - $print_part = &$structure->parts[$html_part]; + $print_part = $structure->parts[$html_part]; } else if ($enriched_part !== null) { - $print_part = &$structure->parts[$enriched_part]; + $print_part = $structure->parts[$enriched_part]; } else if ($plain_part !== null) { - $print_part = &$structure->parts[$plain_part]; + $print_part = $structure->parts[$plain_part]; } // add the right message body @@ -426,12 +486,9 @@ class rcube_message $this->parts[] = $c; } - // add html part as attachment - if ($html_part !== null && $structure->parts[$html_part] !== $print_part) { - $html_part = &$structure->parts[$html_part]; - $html_part->mimetype = 'text/html'; - - $this->attachments[] = $html_part; + // add unsupported/unrecognized parts to attachments list + if ($attach_part) { + $this->attachments[] = $structure->parts[$attach_part]; } } // this is an ecrypted message -> create a plaintext body with the according message @@ -445,6 +502,17 @@ class rcube_message $this->parts[] = $p; } + // this is an S/MIME ecrypted message -> create a plaintext body with the according message + else if ($mimetype == 'application/pkcs7-mime') { + $p = new stdClass; + $p->type = 'content'; + $p->ctype_primary = 'text'; + $p->ctype_secondary = 'plain'; + $p->mimetype = 'text/plain'; + $p->realtype = 'application/pkcs7-mime'; + + $this->parts[] = $p; + } // message contains multiple parts else if (is_array($structure->parts) && !empty($structure->parts)) { // iterate over parts @@ -502,10 +570,6 @@ class rcube_message if (!empty($mail_part->filename)) { $this->attachments[] = $mail_part; } - // list html part as attachment (here the part is most likely inside a multipart/related part) - else if ($this->parse_alternative && ($secondary_type == 'html' && !$this->opt['prefer_html'])) { - $this->attachments[] = $mail_part; - } } // part message/* else if ($primary_type == 'message') { @@ -537,7 +601,7 @@ class rcube_message continue; // part belongs to a related message and is linked - if ($mimetype == 'multipart/related' + if (preg_match('/^multipart\/(related|relative)/', $mimetype) && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) { if ($mail_part->headers['content-id']) $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']); @@ -557,9 +621,6 @@ class rcube_message // regular attachment with valid content type // (content-type name regexp according to RFC4288.4.2) else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) { - if (!$mail_part->filename) - $mail_part->filename = 'Part '.$mail_part->mime_id; - $this->attachments[] = $mail_part; } // attachment with invalid content type @@ -579,13 +640,13 @@ class rcube_message } // if this was a related part try to resolve references - if ($mimetype == 'multipart/related' && sizeof($this->inline_parts)) { + if (preg_match('/^multipart\/(related|relative)/', $mimetype) && sizeof($this->inline_parts)) { $a_replaces = array(); $img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/'; foreach ($this->inline_parts as $inline_object) { $part_url = $this->get_part_url($inline_object->mime_id, true); - if ($inline_object->content_id) + if (isset($inline_object->content_id)) $a_replaces['cid:'.$inline_object->content_id] = $part_url; if ($inline_object->content_location) { $a_replaces[$inline_object->content_location] = $part_url; @@ -624,7 +685,6 @@ class rcube_message } // message is a single part non-text (without filename) else if (preg_match('/application\//i', $mimetype)) { - $structure->filename = 'Part '.$structure->mime_id; $this->attachments[] = $structure; } } diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php index 445d0bd39..274ae7f9f 100644 --- a/program/lib/Roundcube/rcube_message_header.php +++ b/program/lib/Roundcube/rcube_message_header.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_message_header.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,7 +12,6 @@ | | | PURPOSE: | | E-mail message headers representation | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ @@ -235,13 +232,30 @@ class rcube_message_header $this->others[$name] = $value; } } + + + /** + * Factory method to instantiate headers from a data array + * + * @param array Hash array with header values + * @return object rcube_message_header instance filled with headers values + */ + public static function from_array($arr) + { + $obj = new rcube_message_header; + foreach ($arr as $k => $v) + $obj->set($k, $v); + + return $obj; + } } /** * Class for sorting an array of rcube_message_header objects in a predetermined order. * - * @package Mail + * @package Framework + * @subpackage Storage * @author Aleksander Machniak <alec@alec.pl> */ class rcube_message_header_sorter diff --git a/program/lib/Roundcube/rcube_message_part.php b/program/lib/Roundcube/rcube_message_part.php index c9c9257eb..4222ba390 100644 --- a/program/lib/Roundcube/rcube_message_part.php +++ b/program/lib/Roundcube/rcube_message_part.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_message_part.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,14 +12,12 @@ | | | PURPOSE: | | Class representing a message part | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Class representing a message part * diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index 17cb3f015..7cd520752 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_mime.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -14,14 +12,12 @@ | | | PURPOSE: | | MIME message parsing utilities | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Class for parsing MIME messages * @@ -480,13 +476,19 @@ class rcube_mime $q_level = 0; foreach ($text as $idx => $line) { - if ($line[0] == '>' && preg_match('/^(>+\s*)/', $line, $regs)) { - $q = strlen(str_replace(' ', '', $regs[0])); - $line = substr($line, strlen($regs[0])); - - if ($q == $q_level && $line - && isset($text[$last]) - && $text[$last][strlen($text[$last])-1] == ' ' + if ($line[0] == '>') { + // remove quote chars, store level in $q + $line = preg_replace('/^>+/', '', $line, -1, $q); + // remove (optional) space-staffing + $line = preg_replace('/^ /', '', $line); + + // The same paragraph (We join current line with the previous one) when: + // - the same level of quoting + // - previous line was flowed + // - previous line contains more than only one single space (and quote char(s)) + if ($q == $q_level + && isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' ' + && !preg_match('/^>+ {0,1}$/', $text[$last]) ) { $text[$last] .= $line; unset($text[$idx]); @@ -539,10 +541,12 @@ class rcube_mime foreach ($text as $idx => $line) { if ($line != '-- ') { - if ($line[0] == '>' && preg_match('/^(>+ {0,1})+/', $line, $regs)) { - $level = substr_count($regs[0], '>'); + if ($line[0] == '>') { + // remove quote chars, store level in $level + $line = preg_replace('/^>+/', '', $line, -1, $level); + // remove (optional) space-staffing and spaces before the line end + $line = preg_replace('/(^ | +$)/', '', $line); $prefix = str_repeat('>', $level) . ' '; - $line = rtrim(substr($line, strlen($regs[0]))); $line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset); } else if ($line) { @@ -582,7 +586,7 @@ class rcube_mime while (count($para)) { $line = array_shift($para); if ($line[0] == '>') { - $string .= $line.$break; + $string .= $line . (count($para) ? $break : ''); continue; } @@ -591,11 +595,12 @@ class rcube_mime while (count($list)) { $line = array_shift($list); $l = mb_strlen($line); - $newlen = $len + $l + ($len ? 1 : 0); + $space = $len ? 1 : 0; + $newlen = $len + $l + $space; if ($newlen <= $width) { - $string .= ($len ? ' ' : '').$line; - $len += (1 + $l); + $string .= ($space ? ' ' : '').$line; + $len += ($space + $l); } else { if ($l > $width) { @@ -667,7 +672,16 @@ class rcube_mime // try fileinfo extension if available if (!$mime_type && function_exists('finfo_open')) { - if ($finfo = finfo_open(FILEINFO_MIME, $mime_magic)) { + // null as a 2nd argument should be the same as no argument + // this however is not true on all systems/versions + if ($mime_magic) { + $finfo = finfo_open(FILEINFO_MIME, $mime_magic); + } + else { + $finfo = finfo_open(FILEINFO_MIME); + } + + if ($finfo) { if ($is_stream) $mime_type = finfo_buffer($finfo, $path); else @@ -713,20 +727,27 @@ class rcube_mime // load mapping file $file_paths = array(); - if ($mime_types = rcube::get_instance()->config->get('mime_types')) + if ($mime_types = rcube::get_instance()->config->get('mime_types')) { $file_paths[] = $mime_types; + } // try common locations - $file_paths[] = '/etc/httpd/mime.types'; - $file_paths[] = '/etc/httpd2/mime.types'; - $file_paths[] = '/etc/apache/mime.types'; - $file_paths[] = '/etc/apache2/mime.types'; - $file_paths[] = '/usr/local/etc/httpd/conf/mime.types'; - $file_paths[] = '/usr/local/etc/apache/conf/mime.types'; + if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { + $file_paths[] = 'C:/xampp/apache/conf/mime.types.'; + } + else { + $file_paths[] = '/etc/mime.types'; + $file_paths[] = '/etc/httpd/mime.types'; + $file_paths[] = '/etc/httpd2/mime.types'; + $file_paths[] = '/etc/apache/mime.types'; + $file_paths[] = '/etc/apache2/mime.types'; + $file_paths[] = '/usr/local/etc/httpd/conf/mime.types'; + $file_paths[] = '/usr/local/etc/apache/conf/mime.types'; + } foreach ($file_paths as $fp) { if (is_readable($fp)) { - $lines = file($fp, FILE_IGNORE_NEW_LINES); + $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); break; } } @@ -748,11 +769,35 @@ class rcube_mime // fallback to some well-known types most important for daily emails if (empty($mime_types)) { - $mime_extensions = @include(RCUBE_CONFIG_DIR . '/mimetypes.php'); - $mime_extensions += array('gif' => 'image/gif', 'png' => 'image/png', 'jpg' => 'image/jpg', 'jpeg' => 'image/jpeg', 'tif' => 'image/tiff'); + $mime_extensions = (array) @include(RCUBE_CONFIG_DIR . '/mimetypes.php'); - foreach ($mime_extensions as $ext => $mime) + foreach ($mime_extensions as $ext => $mime) { $mime_types[$mime][] = $ext; + } + } + + // Add some known aliases that aren't included by some mime.types (#1488891) + // the order is important here so standard extensions have higher prio + $aliases = array( + 'image/gif' => array('gif'), + 'image/png' => array('png'), + 'image/x-png' => array('png'), + 'image/jpeg' => array('jpg', 'jpeg', 'jpe'), + 'image/jpg' => array('jpg', 'jpeg', 'jpe'), + 'image/pjpeg' => array('jpg', 'jpeg', 'jpe'), + 'image/tiff' => array('tif'), + 'message/rfc822' => array('eml'), + 'text/x-mail' => array('eml'), + ); + + foreach ($aliases as $mime => $exts) { + $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts)); + + foreach ($exts as $ext) { + if (!isset($mime_extensions[$ext])) { + $mime_extensions[$ext] = $mime; + } + } } return $mimetype ? $mime_types[$mimetype] : $mime_extensions; diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php index 4ef42f598..b8ae86cf6 100644 --- a/program/lib/Roundcube/rcube_output.php +++ b/program/lib/Roundcube/rcube_output.php @@ -2,17 +2,15 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_output.php | - | | | This file is part of the Roundcube PHP suite | | Copyright (C) 2005-2012 The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | | See the README file for a full license statement. | + | | | CONTENTS: | | Abstract class for output generation | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php index dbb15e8be..167a9eb4f 100644 --- a/program/lib/Roundcube/rcube_plugin.php +++ b/program/lib/Roundcube/rcube_plugin.php @@ -2,10 +2,8 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_plugin.php | - | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2009, The Roundcube Dev Team | + | Copyright (C) 2008-2012, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -27,334 +25,361 @@ */ abstract class rcube_plugin { - /** - * Class name of the plugin instance - * - * @var string - */ - public $ID; - - /** - * Instance of Plugin API - * - * @var rcube_plugin_api - */ - public $api; - - /** - * Regular expression defining task(s) to bind with - * - * @var string - */ - public $task; - - /** - * Disables plugin in AJAX requests - * - * @var boolean - */ - public $noajax = false; - - /** - * Disables plugin in framed mode - * - * @var boolean - */ - public $noframe = false; - - protected $home; - protected $urlbase; - private $mytask; - - - /** - * Default constructor. - * - * @param rcube_plugin_api $api Plugin API - */ - public function __construct($api) - { - $this->ID = get_class($this); - $this->api = $api; - $this->home = $api->dir . $this->ID; - $this->urlbase = $api->url . $this->ID . '/'; - } - - /** - * Initialization method, needs to be implemented by the plugin itself - */ - abstract function init(); - - - /** - * Attempt to load the given plugin which is required for the current plugin - * - * @param string Plugin name - * @return boolean True on success, false on failure - */ - public function require_plugin($plugin_name) - { - return $this->api->load_plugin($plugin_name); - } - - - /** - * Load local config file from plugins directory. - * The loaded values are patched over the global configuration. - * - * @param string $fname Config file name relative to the plugin's folder - * @return boolean True on success, false on failure - */ - public function load_config($fname = 'config.inc.php') - { - $fpath = $this->home.'/'.$fname; - $rcube = rcube::get_instance(); - if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) { - rcube::raise_error(array( - 'code' => 527, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to load config from $fpath"), true, false); - return false; + /** + * Class name of the plugin instance + * + * @var string + */ + public $ID; + + /** + * Instance of Plugin API + * + * @var rcube_plugin_api + */ + public $api; + + /** + * Regular expression defining task(s) to bind with + * + * @var string + */ + public $task; + + /** + * Disables plugin in AJAX requests + * + * @var boolean + */ + public $noajax = false; + + /** + * Disables plugin in framed mode + * + * @var boolean + */ + public $noframe = false; + + /** + * A list of config option names that can be modified + * by the user via user interface (with save-prefs command) + * + * @var array + */ + public $allowed_prefs; + + protected $home; + protected $urlbase; + private $mytask; + + + /** + * Default constructor. + * + * @param rcube_plugin_api $api Plugin API + */ + public function __construct($api) + { + $this->ID = get_class($this); + $this->api = $api; + $this->home = $api->dir . $this->ID; + $this->urlbase = $api->url . $this->ID . '/'; + } + + /** + * Initialization method, needs to be implemented by the plugin itself + */ + abstract function init(); + + /** + * Attempt to load the given plugin which is required for the current plugin + * + * @param string Plugin name + * @return boolean True on success, false on failure + */ + public function require_plugin($plugin_name) + { + return $this->api->load_plugin($plugin_name); + } + + /** + * Load local config file from plugins directory. + * The loaded values are patched over the global configuration. + * + * @param string $fname Config file name relative to the plugin's folder + * + * @return boolean True on success, false on failure + */ + public function load_config($fname = 'config.inc.php') + { + $fpath = $this->home.'/'.$fname; + $rcube = rcube::get_instance(); + + if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) { + rcube::raise_error(array( + 'code' => 527, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Failed to load config from $fpath"), true, false); + return false; + } + + return true; } - return true; - } - - /** - * Register a callback function for a specific (server-side) hook - * - * @param string $hook Hook name - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function add_hook($hook, $callback) - { - $this->api->register_hook($hook, $callback); - } - - /** - * Unregister a callback function for a specific (server-side) hook. - * - * @param string $hook Hook name - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function remove_hook($hook, $callback) - { - $this->api->unregister_hook($hook, $callback); - } - - /** - * Load localized texts from the plugins dir - * - * @param string $dir Directory to search in - * @param mixed $add2client Make texts also available on the client (array with list or true for all) - */ - public function add_texts($dir, $add2client = false) - { - $domain = $this->ID; - $lang = $_SESSION['language']; - $langs = array_unique(array('en_US', $lang)); - $locdir = slashify(realpath(slashify($this->home) . $dir)); - $texts = array(); - - // Language aliases used to find localization in similar lang, see below - $aliases = array( - 'de_CH' => 'de_DE', - 'es_AR' => 'es_ES', - 'fa_AF' => 'fa_IR', - 'nl_BE' => 'nl_NL', - 'pt_BR' => 'pt_PT', - 'zh_CN' => 'zh_TW', - ); - - // use buffering to handle empty lines/spaces after closing PHP tag - ob_start(); - - foreach ($langs as $lng) { - $fpath = $locdir . $lng . '.inc'; - if (is_file($fpath) && is_readable($fpath)) { - include $fpath; - $texts = (array)$labels + (array)$messages + (array)$texts; - } - else if ($lng != 'en_US') { - // Find localization in similar language (#1488401) - $alias = null; - if (!empty($aliases[$lng])) { - $alias = $aliases[$lng]; + /** + * Register a callback function for a specific (server-side) hook + * + * @param string $hook Hook name + * @param mixed $callback Callback function as string or array + * with object reference and method name + */ + public function add_hook($hook, $callback) + { + $this->api->register_hook($hook, $callback); + } + + /** + * Unregister a callback function for a specific (server-side) hook. + * + * @param string $hook Hook name + * @param mixed $callback Callback function as string or array + * with object reference and method name + */ + public function remove_hook($hook, $callback) + { + $this->api->unregister_hook($hook, $callback); + } + + /** + * Load localized texts from the plugins dir + * + * @param string $dir Directory to search in + * @param mixed $add2client Make texts also available on the client + * (array with list or true for all) + */ + public function add_texts($dir, $add2client = false) + { + $domain = $this->ID; + $lang = $_SESSION['language']; + $langs = array_unique(array('en_US', $lang)); + $locdir = slashify(realpath(slashify($this->home) . $dir)); + $texts = array(); + + // Language aliases used to find localization in similar lang, see below + $aliases = array( + 'de_CH' => 'de_DE', + 'es_AR' => 'es_ES', + 'fa_AF' => 'fa_IR', + 'nl_BE' => 'nl_NL', + 'pt_BR' => 'pt_PT', + 'zh_CN' => 'zh_TW', + ); + + // use buffering to handle empty lines/spaces after closing PHP tag + ob_start(); + + foreach ($langs as $lng) { + $fpath = $locdir . $lng . '.inc'; + if (is_file($fpath) && is_readable($fpath)) { + include $fpath; + $texts = (array)$labels + (array)$messages + (array)$texts; + } + else if ($lng != 'en_US') { + // Find localization in similar language (#1488401) + $alias = null; + if (!empty($aliases[$lng])) { + $alias = $aliases[$lng]; + } + else if ($key = array_search($lng, $aliases)) { + $alias = $key; + } + + if (!empty($alias)) { + $fpath = $locdir . $alias . '.inc'; + if (is_file($fpath) && is_readable($fpath)) { + include $fpath; + $texts = (array)$labels + (array)$messages + (array)$texts; + } + } + } } - else if ($key = array_search($lng, $aliases)) { - $alias = $key; + + ob_end_clean(); + + // prepend domain to text keys and add to the application texts repository + if (!empty($texts)) { + $add = array(); + foreach ($texts as $key => $value) { + $add[$domain.'.'.$key] = $value; + } + + $rcube = rcube::get_instance(); + $rcube->load_language($lang, $add); + + // add labels to client + if ($add2client) { + if (is_array($add2client)) { + $js_labels = array_map(array($this, 'label_map_callback'), $add2client); + } + else { + $js_labels = array_keys($add); + } + $rcube->output->add_label($js_labels); + } } + } + + /** + * Wrapper for rcube::gettext() adding the plugin ID as domain + * + * @param string $p Message identifier + * + * @return string Localized text + * @see rcube::gettext() + */ + public function gettext($p) + { + return rcube::get_instance()->gettext($p, $this->ID); + } - if (!empty($alias)) { - $fpath = $locdir . $alias . '.inc'; - if (is_file($fpath) && is_readable($fpath)) { - include $fpath; - $texts = (array)$labels + (array)$messages + (array)$texts; - } + /** + * Register this plugin to be responsible for a specific task + * + * @param string $task Task name (only characters [a-z0-9_-] are allowed) + */ + public function register_task($task) + { + if ($this->api->register_task($task, $this->ID)) { + $this->mytask = $task; } - } } - ob_end_clean(); + /** + * Register a handler for a specific client-request action + * + * The callback will be executed upon a request like /?_task=mail&_action=plugin.myaction + * + * @param string $action Action name (should be unique) + * @param mixed $callback Callback function as string + * or array with object reference and method name + */ + public function register_action($action, $callback) + { + $this->api->register_action($action, $this->ID, $callback, $this->mytask); + } - // prepend domain to text keys and add to the application texts repository - if (!empty($texts)) { - $add = array(); - foreach ($texts as $key => $value) - $add[$domain.'.'.$key] = $value; + /** + * Register a handler function for a template object + * + * When parsing a template for display, tags like <roundcube:object name="plugin.myobject" /> + * will be replaced by the return value if the registered callback function. + * + * @param string $name Object name (should be unique and start with 'plugin.') + * @param mixed $callback Callback function as string or array with object reference + * and method name + */ + public function register_handler($name, $callback) + { + $this->api->register_handler($name, $this->ID, $callback); + } - $rcmail = rcube::get_instance(); - $rcmail->load_language($lang, $add); + /** + * Make this javascipt file available on the client + * + * @param string $fn File path; absolute or relative to the plugin directory + */ + public function include_script($fn) + { + $this->api->include_script($this->resource_url($fn)); + } - // add labels to client - if ($add2client) { - $js_labels = is_array($add2client) ? array_map(array($this, 'label_map_callback'), $add2client) : array_keys($add); - $rcmail->output->add_label($js_labels); - } + /** + * Make this stylesheet available on the client + * + * @param string $fn File path; absolute or relative to the plugin directory + */ + public function include_stylesheet($fn) + { + $this->api->include_stylesheet($this->resource_url($fn)); + } + + /** + * Append a button to a certain container + * + * @param array $p Hash array with named parameters (as used in skin templates) + * @param string $container Container name where the buttons should be added to + * + * @see rcube_remplate::button() + */ + public function add_button($p, $container) + { + if ($this->api->output->type == 'html') { + // fix relative paths + foreach (array('imagepas', 'imageact', 'imagesel') as $key) { + if ($p[$key]) { + $p[$key] = $this->api->url . $this->resource_url($p[$key]); + } + } + + $this->api->add_content($this->api->output->button($p), $container); + } } - } - - /** - * Wrapper for rcmail::gettext() adding the plugin ID as domain - * - * @param string $p Message identifier - * @return string Localized text - * @see rcmail::gettext() - */ - public function gettext($p) - { - return rcube::get_instance()->gettext($p, $this->ID); - } - - /** - * Register this plugin to be responsible for a specific task - * - * @param string $task Task name (only characters [a-z0-9_.-] are allowed) - */ - public function register_task($task) - { - if ($this->api->register_task($task, $this->ID)) - $this->mytask = $task; - } - - /** - * Register a handler for a specific client-request action - * - * The callback will be executed upon a request like /?_task=mail&_action=plugin.myaction - * - * @param string $action Action name (should be unique) - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function register_action($action, $callback) - { - $this->api->register_action($action, $this->ID, $callback, $this->mytask); - } - - /** - * Register a handler function for a template object - * - * When parsing a template for display, tags like <roundcube:object name="plugin.myobject" /> - * will be replaced by the return value if the registered callback function. - * - * @param string $name Object name (should be unique and start with 'plugin.') - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function register_handler($name, $callback) - { - $this->api->register_handler($name, $this->ID, $callback); - } - - /** - * Make this javascipt file available on the client - * - * @param string $fn File path; absolute or relative to the plugin directory - */ - public function include_script($fn) - { - $this->api->include_script($this->resource_url($fn)); - } - - /** - * Make this stylesheet available on the client - * - * @param string $fn File path; absolute or relative to the plugin directory - */ - public function include_stylesheet($fn) - { - $this->api->include_stylesheet($this->resource_url($fn)); - } - - /** - * Append a button to a certain container - * - * @param array $p Hash array with named parameters (as used in skin templates) - * @param string $container Container name where the buttons should be added to - * @see rcube_remplate::button() - */ - public function add_button($p, $container) - { - if ($this->api->output->type == 'html') { - // fix relative paths - foreach (array('imagepas', 'imageact', 'imagesel') as $key) - if ($p[$key]) - $p[$key] = $this->api->url . $this->resource_url($p[$key]); - - $this->api->add_content($this->api->output->button($p), $container); + + /** + * Generate an absolute URL to the given resource within the current + * plugin directory + * + * @param string $fn The file name + * + * @return string Absolute URL to the given resource + */ + public function url($fn) + { + return $this->api->url . $this->resource_url($fn); } - } - - /** - * Generate an absolute URL to the given resource within the current - * plugin directory - * - * @param string $fn The file name - * @return string Absolute URL to the given resource - */ - public function url($fn) - { - return $this->api->url . $this->resource_url($fn); - } - - /** - * Make the given file name link into the plugin directory - * - * @param string $fn Filename - */ - private function resource_url($fn) - { - if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) - return $this->ID . '/' . $fn; - else - return $fn; - } - - /** - * Provide path to the currently selected skin folder within the plugin directory - * with a fallback to the default skin folder. - * - * @return string Skin path relative to plugins directory - */ - public function local_skin_path() - { - $rcmail = rcube::get_instance(); - foreach (array($rcmail->config->get('skin'), 'larry') as $skin) { - $skin_path = 'skins/' . $skin; - if (is_dir(realpath(slashify($this->home) . $skin_path))) - break; + + /** + * Make the given file name link into the plugin directory + * + * @param string $fn Filename + */ + private function resource_url($fn) + { + if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) { + return $this->ID . '/' . $fn; + } + else { + return $fn; + } } - return $skin_path; - } + /** + * Provide path to the currently selected skin folder within the plugin directory + * with a fallback to the default skin folder. + * + * @return string Skin path relative to plugins directory + */ + public function local_skin_path() + { + $rcube = rcube::get_instance(); + foreach (array($rcube->config->get('skin'), 'larry') as $skin) { + $skin_path = 'skins/' . $skin; + if (is_dir(realpath(slashify($this->home) . $skin_path))) { + break; + } + } - /** - * Callback function for array_map - * - * @param string $key Array key. - * @return string - */ - private function label_map_callback($key) - { - return $this->ID.'.'.$key; - } + return $skin_path; + } + /** + * Callback function for array_map + * + * @param string $key Array key. + * @return string + */ + private function label_map_callback($key) + { + return $this->ID.'.'.$key; + } } diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php index 51cf5d246..a89f14712 100644 --- a/program/lib/Roundcube/rcube_plugin_api.php +++ b/program/lib/Roundcube/rcube_plugin_api.php @@ -2,10 +2,8 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_plugin_api.php | - | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2011, The Roundcube Dev Team | + | Copyright (C) 2008-2012, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -13,16 +11,15 @@ | | | PURPOSE: | | Plugins repository | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ */ // location where plugins are loade from -if (!defined('RCUBE_PLUGINS_DIR')) - define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/'); - +if (!defined('RCUBE_PLUGINS_DIR')) { + define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/'); +} /** * The plugin loader and global API @@ -32,468 +29,482 @@ if (!defined('RCUBE_PLUGINS_DIR')) */ class rcube_plugin_api { - static protected $instance; - - public $dir; - public $url = 'plugins/'; - public $task = ''; - public $output; - - public $handlers = array(); - protected $plugins = array(); - protected $tasks = array(); - protected $actions = array(); - protected $actionmap = array(); - protected $objectsmap = array(); - protected $template_contents = array(); - protected $active_hook = false; - - // Deprecated names of hooks, will be removed after 0.5-stable release - protected $deprecated_hooks = array( - 'create_user' => 'user_create', - 'kill_session' => 'session_destroy', - 'upload_attachment' => 'attachment_upload', - 'save_attachment' => 'attachment_save', - 'get_attachment' => 'attachment_get', - 'cleanup_attachments' => 'attachments_cleanup', - 'display_attachment' => 'attachment_display', - 'remove_attachment' => 'attachment_delete', - 'outgoing_message_headers' => 'message_outgoing_headers', - 'outgoing_message_body' => 'message_outgoing_body', - 'address_sources' => 'addressbooks_list', - 'get_address_book' => 'addressbook_get', - 'create_contact' => 'contact_create', - 'save_contact' => 'contact_update', - 'contact_save' => 'contact_update', - 'delete_contact' => 'contact_delete', - 'manage_folders' => 'folders_list', - 'list_mailboxes' => 'mailboxes_list', - 'save_preferences' => 'preferences_save', - 'user_preferences' => 'preferences_list', - 'list_prefs_sections' => 'preferences_sections_list', - 'list_identities' => 'identities_list', - 'create_identity' => 'identity_create', - 'delete_identity' => 'identity_delete', - 'save_identity' => 'identity_update', - 'identity_save' => 'identity_update', - // to be removed after 0.8 - 'imap_init' => 'storage_init', - 'mailboxes_list' => 'storage_folders', - ); - - /** - * This implements the 'singleton' design pattern - * - * @return rcube_plugin_api The one and only instance if this class - */ - static function get_instance() - { - if (!self::$instance) { - self::$instance = new rcube_plugin_api(); - } + static protected $instance; + + public $dir; + public $url = 'plugins/'; + public $task = ''; + public $output; + public $handlers = array(); + public $allowed_prefs = array(); + + protected $plugins = array(); + protected $tasks = array(); + protected $actions = array(); + protected $actionmap = array(); + protected $objectsmap = array(); + protected $template_contents = array(); + protected $active_hook = false; + + // Deprecated names of hooks, will be removed after 0.5-stable release + protected $deprecated_hooks = array( + 'create_user' => 'user_create', + 'kill_session' => 'session_destroy', + 'upload_attachment' => 'attachment_upload', + 'save_attachment' => 'attachment_save', + 'get_attachment' => 'attachment_get', + 'cleanup_attachments' => 'attachments_cleanup', + 'display_attachment' => 'attachment_display', + 'remove_attachment' => 'attachment_delete', + 'outgoing_message_headers' => 'message_outgoing_headers', + 'outgoing_message_body' => 'message_outgoing_body', + 'address_sources' => 'addressbooks_list', + 'get_address_book' => 'addressbook_get', + 'create_contact' => 'contact_create', + 'save_contact' => 'contact_update', + 'contact_save' => 'contact_update', + 'delete_contact' => 'contact_delete', + 'manage_folders' => 'folders_list', + 'list_mailboxes' => 'mailboxes_list', + 'save_preferences' => 'preferences_save', + 'user_preferences' => 'preferences_list', + 'list_prefs_sections' => 'preferences_sections_list', + 'list_identities' => 'identities_list', + 'create_identity' => 'identity_create', + 'delete_identity' => 'identity_delete', + 'save_identity' => 'identity_update', + 'identity_save' => 'identity_update', + // to be removed after 0.8 + 'imap_init' => 'storage_init', + 'mailboxes_list' => 'storage_folders', + 'imap_connect' => 'storage_connect', + ); + + /** + * This implements the 'singleton' design pattern + * + * @return rcube_plugin_api The one and only instance if this class + */ + static function get_instance() + { + if (!self::$instance) { + self::$instance = new rcube_plugin_api(); + } - return self::$instance; - } - - - /** - * Private constructor - */ - protected function __construct() - { - $this->dir = slashify(RCUBE_PLUGINS_DIR); - } - - - /** - * Initialize plugin engine - * - * This has to be done after rcmail::load_gui() or rcmail::json_init() - * was called because plugins need to have access to rcmail->output - * - * @param object rcube Instance of the rcube base class - * @param string Current application task (used for conditional plugin loading) - */ - public function init($app, $task = '') - { - $this->task = $task; - $this->output = $app->output; - - // register an internal hook - $this->register_hook('template_container', array($this, 'template_container_hook')); - - // maybe also register a shudown function which triggers shutdown functions of all plugin objects - } - - - /** - * Load and init all enabled plugins - * - * This has to be done after rcmail::load_gui() or rcmail::json_init() - * was called because plugins need to have access to rcmail->output - * - * @param array List of configured plugins to load - * @param array List of plugins required by the application - */ - public function load_plugins($plugins_enabled, $required_plugins = array()) - { - foreach ($plugins_enabled as $plugin_name) { - $this->load_plugin($plugin_name); + return self::$instance; } - // check existance of all required core plugins - foreach ($required_plugins as $plugin_name) { - $loaded = false; - foreach ($this->plugins as $plugin) { - if ($plugin instanceof $plugin_name) { - $loaded = true; - break; - } - } - - // load required core plugin if no derivate was found - if (!$loaded) - $loaded = $this->load_plugin($plugin_name); - - // trigger fatal error if still not loaded - if (!$loaded) { - rcube::raise_error(array('code' => 520, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Requried plugin $plugin_name was not loaded"), true, true); - } + /** + * Private constructor + */ + protected function __construct() + { + $this->dir = slashify(RCUBE_PLUGINS_DIR); } - } - - /** - * Load the specified plugin - * - * @param string Plugin name - * @return boolean True on success, false if not loaded or failure - */ - public function load_plugin($plugin_name) - { - static $plugins_dir; - - if (!$plugins_dir) { - $dir = dir($this->dir); - $plugins_dir = unslashify($dir->path); + + /** + * Initialize plugin engine + * + * This has to be done after rcmail::load_gui() or rcmail::json_init() + * was called because plugins need to have access to rcmail->output + * + * @param object rcube Instance of the rcube base class + * @param string Current application task (used for conditional plugin loading) + */ + public function init($app, $task = '') + { + $this->task = $task; + $this->output = $app->output; + + // register an internal hook + $this->register_hook('template_container', array($this, 'template_container_hook')); + + // maybe also register a shudown function which triggers + // shutdown functions of all plugin objects } - // plugin already loaded - if ($this->plugins[$plugin_name] || class_exists($plugin_name, false)) - return true; - - $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php'; - - if (file_exists($fn)) { - include($fn); - - // instantiate class if exists - if (class_exists($plugin_name, false)) { - $plugin = new $plugin_name($this); - // check inheritance... - if (is_subclass_of($plugin, 'rcube_plugin')) { - // ... task, request type and framed mode - if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task)) - && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html')) - && (!$plugin->noframe || empty($_REQUEST['_framed'])) - ) { - $plugin->init(); - $this->plugins[$plugin_name] = $plugin; - } - return true; + /** + * Load and init all enabled plugins + * + * This has to be done after rcmail::load_gui() or rcmail::json_init() + * was called because plugins need to have access to rcmail->output + * + * @param array List of configured plugins to load + * @param array List of plugins required by the application + */ + public function load_plugins($plugins_enabled, $required_plugins = array()) + { + foreach ($plugins_enabled as $plugin_name) { + $this->load_plugin($plugin_name); + } + + // check existance of all required core plugins + foreach ($required_plugins as $plugin_name) { + $loaded = false; + foreach ($this->plugins as $plugin) { + if ($plugin instanceof $plugin_name) { + $loaded = true; + break; + } + } + + // load required core plugin if no derivate was found + if (!$loaded) { + $loaded = $this->load_plugin($plugin_name); + } + + // trigger fatal error if still not loaded + if (!$loaded) { + rcube::raise_error(array( + 'code' => 520, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Requried plugin $plugin_name was not loaded"), true, true); + } } - } - else { - rcube::raise_error(array('code' => 520, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "No plugin class $plugin_name found in $fn"), true, false); - } } - else { - rcube::raise_error(array('code' => 520, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to load plugin file $fn"), true, false); + + /** + * Load the specified plugin + * + * @param string Plugin name + * + * @return boolean True on success, false if not loaded or failure + */ + public function load_plugin($plugin_name) + { + static $plugins_dir; + + if (!$plugins_dir) { + $dir = dir($this->dir); + $plugins_dir = unslashify($dir->path); + } + + // plugin already loaded + if ($this->plugins[$plugin_name] || class_exists($plugin_name, false)) { + return true; + } + + $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name + . DIRECTORY_SEPARATOR . $plugin_name . '.php'; + + if (file_exists($fn)) { + include $fn; + + // instantiate class if exists + if (class_exists($plugin_name, false)) { + $plugin = new $plugin_name($this); + // check inheritance... + if (is_subclass_of($plugin, 'rcube_plugin')) { + // ... task, request type and framed mode + if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task)) + && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html')) + && (!$plugin->noframe || empty($_REQUEST['_framed'])) + ) { + $plugin->init(); + $this->plugins[$plugin_name] = $plugin; + } + + if (!empty($plugin->allowed_prefs)) { + $this->allowed_prefs = array_merge($this->allowed_prefs, $plugin->allowed_prefs); + } + + return true; + } + } + else { + rcube::raise_error(array('code' => 520, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "No plugin class $plugin_name found in $fn"), + true, false); + } + } + else { + rcube::raise_error(array('code' => 520, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Failed to load plugin file $fn"), true, false); + } + + return false; } - return false; - } - - - /** - * Allows a plugin object to register a callback for a certain hook - * - * @param string $hook Hook name - * @param mixed $callback String with global function name or array($obj, 'methodname') - */ - public function register_hook($hook, $callback) - { - if (is_callable($callback)) { - if (isset($this->deprecated_hooks[$hook])) { - rcube::raise_error(array('code' => 522, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Deprecated hook name. ".$hook.' -> '.$this->deprecated_hooks[$hook]), true, false); - $hook = $this->deprecated_hooks[$hook]; - } - $this->handlers[$hook][] = $callback; + /** + * Allows a plugin object to register a callback for a certain hook + * + * @param string $hook Hook name + * @param mixed $callback String with global function name or array($obj, 'methodname') + */ + public function register_hook($hook, $callback) + { + if (is_callable($callback)) { + if (isset($this->deprecated_hooks[$hook])) { + rcube::raise_error(array('code' => 522, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Deprecated hook name. " + . $hook . ' -> ' . $this->deprecated_hooks[$hook]), true, false); + $hook = $this->deprecated_hooks[$hook]; + } + $this->handlers[$hook][] = $callback; + } + else { + rcube::raise_error(array('code' => 521, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Invalid callback function for $hook"), true, false); + } } - else - rcube::raise_error(array('code' => 521, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Invalid callback function for $hook"), true, false); - } - - /** - * Allow a plugin object to unregister a callback. - * - * @param string $hook Hook name - * @param mixed $callback String with global function name or array($obj, 'methodname') - */ - public function unregister_hook($hook, $callback) - { - $callback_id = array_search($callback, $this->handlers[$hook]); - if ($callback_id !== false) { - unset($this->handlers[$hook][$callback_id]); + + /** + * Allow a plugin object to unregister a callback. + * + * @param string $hook Hook name + * @param mixed $callback String with global function name or array($obj, 'methodname') + */ + public function unregister_hook($hook, $callback) + { + $callback_id = array_search($callback, $this->handlers[$hook]); + if ($callback_id !== false) { + unset($this->handlers[$hook][$callback_id]); + } } - } - - - /** - * Triggers a plugin hook. - * This is called from the application and executes all registered handlers - * - * @param string $hook Hook name - * @param array $args Named arguments (key->value pairs) - * @return array The (probably) altered hook arguments - */ - public function exec_hook($hook, $args = array()) - { - if (!is_array($args)) - $args = array('arg' => $args); - - $args += array('abort' => false); - $this->active_hook = $hook; - - foreach ((array)$this->handlers[$hook] as $callback) { - $ret = call_user_func($callback, $args); - if ($ret && is_array($ret)) - $args = $ret + $args; - - if ($args['abort']) - break; + + /** + * Triggers a plugin hook. + * This is called from the application and executes all registered handlers + * + * @param string $hook Hook name + * @param array $args Named arguments (key->value pairs) + * + * @return array The (probably) altered hook arguments + */ + public function exec_hook($hook, $args = array()) + { + if (!is_array($args)) { + $args = array('arg' => $args); + } + + $args += array('abort' => false); + $this->active_hook = $hook; + + foreach ((array)$this->handlers[$hook] as $callback) { + $ret = call_user_func($callback, $args); + if ($ret && is_array($ret)) { + $args = $ret + $args; + } + + if ($args['abort']) { + break; + } + } + + $this->active_hook = false; + return $args; } - $this->active_hook = false; - return $args; - } - - - /** - * Let a plugin register a handler for a specific request - * - * @param string $action Action name (_task=mail&_action=plugin.foo) - * @param string $owner Plugin name that registers this action - * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') - * @param string $task Task name registered by this plugin - */ - public function register_action($action, $owner, $callback, $task = null) - { - // check action name - if ($task) - $action = $task.'.'.$action; - else if (strpos($action, 'plugin.') !== 0) - $action = 'plugin.'.$action; - - // can register action only if it's not taken or registered by myself - if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) { - $this->actions[$action] = $callback; - $this->actionmap[$action] = $owner; + /** + * Let a plugin register a handler for a specific request + * + * @param string $action Action name (_task=mail&_action=plugin.foo) + * @param string $owner Plugin name that registers this action + * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') + * @param string $task Task name registered by this plugin + */ + public function register_action($action, $owner, $callback, $task = null) + { + // check action name + if ($task) + $action = $task.'.'.$action; + else if (strpos($action, 'plugin.') !== 0) + $action = 'plugin.'.$action; + + // can register action only if it's not taken or registered by myself + if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) { + $this->actions[$action] = $callback; + $this->actionmap[$action] = $owner; + } + else { + rcube::raise_error(array('code' => 523, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Cannot register action $action;" + ." already taken by another plugin"), true, false); + } } - else { - rcube::raise_error(array('code' => 523, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Cannot register action $action; already taken by another plugin"), true, false); + + /** + * This method handles requests like _task=mail&_action=plugin.foo + * It executes the callback function that was registered with the given action. + * + * @param string $action Action name + */ + public function exec_action($action) + { + if (isset($this->actions[$action])) { + call_user_func($this->actions[$action]); + } + else if (rcube::get_instance()->action != 'refresh') { + rcube::raise_error(array('code' => 524, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "No handler found for action $action"), true, true); + } } - } - - - /** - * This method handles requests like _task=mail&_action=plugin.foo - * It executes the callback function that was registered with the given action. - * - * @param string $action Action name - */ - public function exec_action($action) - { - if (isset($this->actions[$action])) { - call_user_func($this->actions[$action]); + + /** + * Register a handler function for template objects + * + * @param string $name Object name + * @param string $owner Plugin name that registers this action + * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') + */ + public function register_handler($name, $owner, $callback) + { + // check name + if (strpos($name, 'plugin.') !== 0) { + $name = 'plugin.' . $name; + } + + // can register handler only if it's not taken or registered by myself + if (is_object($this->output) + && (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner) + ) { + $this->output->add_handler($name, $callback); + $this->objectsmap[$name] = $owner; + } + else { + rcube::raise_error(array('code' => 525, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Cannot register template handler $name;" + ." already taken by another plugin or no output object available"), true, false); + } } - else if (rcube::get_instance()->action != 'refresh') { - rcube::raise_error(array('code' => 524, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "No handler found for action $action"), true, true); + + /** + * Register this plugin to be responsible for a specific task + * + * @param string $task Task name (only characters [a-z0-9_-] are allowed) + * @param string $owner Plugin name that registers this action + */ + public function register_task($task, $owner) + { + // tasks are irrelevant in framework mode + if (!class_exists('rcmail', false)) { + return true; + } + + if ($task != asciiwords($task, true)) { + rcube::raise_error(array('code' => 526, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Invalid task name: $task." + ." Only characters [a-z0-9_.-] are allowed"), true, false); + } + else if (in_array($task, rcmail::$main_tasks)) { + rcube::raise_error(array('code' => 526, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Cannot register taks $task;" + ." already taken by another plugin or the application itself"), true, false); + } + else { + $this->tasks[$task] = $owner; + rcmail::$main_tasks[] = $task; + return true; + } + + return false; } - } - - - /** - * Register a handler function for template objects - * - * @param string $name Object name - * @param string $owner Plugin name that registers this action - * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') - */ - public function register_handler($name, $owner, $callback) - { - // check name - if (strpos($name, 'plugin.') !== 0) - $name = 'plugin.'.$name; - - // can register handler only if it's not taken or registered by myself - if (is_object($this->output) && (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)) { - $this->output->add_handler($name, $callback); - $this->objectsmap[$name] = $owner; + + /** + * Checks whether the given task is registered by a plugin + * + * @param string $task Task name + * + * @return boolean True if registered, otherwise false + */ + public function is_plugin_task($task) + { + return $this->tasks[$task] ? true : false; } - else { - rcube::raise_error(array('code' => 525, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Cannot register template handler $name; already taken by another plugin or no output object available"), true, false); + + /** + * Check if a plugin hook is currently processing. + * Mainly used to prevent loops and recursion. + * + * @param string $hook Hook to check (optional) + * + * @return boolean True if any/the given hook is currently processed, otherwise false + */ + public function is_processing($hook = null) + { + return $this->active_hook && (!$hook || $this->active_hook == $hook); } - } - - - /** - * Register this plugin to be responsible for a specific task - * - * @param string $task Task name (only characters [a-z0-9_.-] are allowed) - * @param string $owner Plugin name that registers this action - */ - public function register_task($task, $owner) - { - // tasks are irrelevant in framework mode - if (!class_exists('rcmail', false)) - return true; - - if ($task != asciiwords($task)) { - rcube::raise_error(array('code' => 526, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Invalid task name: $task. Only characters [a-z0-9_.-] are allowed"), true, false); + + /** + * Include a plugin script file in the current HTML page + * + * @param string $fn Path to script + */ + public function include_script($fn) + { + if (is_object($this->output) && $this->output->type == 'html') { + $src = $this->resource_url($fn); + $this->output->add_header(html::tag('script', + array('type' => "text/javascript", 'src' => $src))); + } } - else if (in_array($task, rcmail::$main_tasks)) { - rcube::raise_error(array('code' => 526, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Cannot register taks $task; already taken by another plugin or the application itself"), true, false); + + /** + * Include a plugin stylesheet in the current HTML page + * + * @param string $fn Path to stylesheet + */ + public function include_stylesheet($fn) + { + if (is_object($this->output) && $this->output->type == 'html') { + $src = $this->resource_url($fn); + $this->output->include_css($src); + } } - else { - $this->tasks[$task] = $owner; - rcmail::$main_tasks[] = $task; - return true; + + /** + * Save the given HTML content to be added to a template container + * + * @param string $html HTML content + * @param string $container Template container identifier + */ + public function add_content($html, $container) + { + $this->template_contents[$container] .= $html . "\n"; } - return false; - } - - - /** - * Checks whether the given task is registered by a plugin - * - * @param string $task Task name - * @return boolean True if registered, otherwise false - */ - public function is_plugin_task($task) - { - return $this->tasks[$task] ? true : false; - } - - - /** - * Check if a plugin hook is currently processing. - * Mainly used to prevent loops and recursion. - * - * @param string $hook Hook to check (optional) - * @return boolean True if any/the given hook is currently processed, otherwise false - */ - public function is_processing($hook = null) - { - return $this->active_hook && (!$hook || $this->active_hook == $hook); - } - - /** - * Include a plugin script file in the current HTML page - * - * @param string $fn Path to script - */ - public function include_script($fn) - { - if (is_object($this->output) && $this->output->type == 'html') { - $src = $this->resource_url($fn); - $this->output->add_header(html::tag('script', array('type' => "text/javascript", 'src' => $src))); + /** + * Returns list of loaded plugins names + * + * @return array List of plugin names + */ + public function loaded_plugins() + { + return array_keys($this->plugins); } - } - - - /** - * Include a plugin stylesheet in the current HTML page - * - * @param string $fn Path to stylesheet - */ - public function include_stylesheet($fn) - { - if (is_object($this->output) && $this->output->type == 'html') { - $src = $this->resource_url($fn); - $this->output->include_css($src); + + /** + * Callback for template_container hooks + * + * @param array $attrib + * @return array + */ + protected function template_container_hook($attrib) + { + $container = $attrib['name']; + return array('content' => $attrib['content'] . $this->template_contents[$container]); } - } - - - /** - * Save the given HTML content to be added to a template container - * - * @param string $html HTML content - * @param string $container Template container identifier - */ - public function add_content($html, $container) - { - $this->template_contents[$container] .= $html . "\n"; - } - - - /** - * Returns list of loaded plugins names - * - * @return array List of plugin names - */ - public function loaded_plugins() - { - return array_keys($this->plugins); - } - - - /** - * Callback for template_container hooks - * - * @param array $attrib - * @return array - */ - protected function template_container_hook($attrib) - { - $container = $attrib['name']; - return array('content' => $attrib['content'] . $this->template_contents[$container]); - } - - - /** - * Make the given file name link into the plugins directory - * - * @param string $fn Filename - * @return string - */ - protected function resource_url($fn) - { - if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) - return $this->url . $fn; - else - return $fn; - } + /** + * Make the given file name link into the plugins directory + * + * @param string $fn Filename + * @return string + */ + protected function resource_url($fn) + { + if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) + return $this->url . $fn; + else + return $fn; + } } diff --git a/program/lib/Roundcube/rcube_result_index.php b/program/lib/Roundcube/rcube_result_index.php index 4d1ae13b6..5f592c54f 100644 --- a/program/lib/Roundcube/rcube_result_index.php +++ b/program/lib/Roundcube/rcube_result_index.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_result_index.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2011, The Roundcube Dev Team | | Copyright (C) 2011, Kolab Systems AG | @@ -14,14 +12,12 @@ | | | PURPOSE: | | SORT/SEARCH/ESEARCH response handler | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Class for accessing IMAP's SORT/SEARCH/ESEARCH result * diff --git a/program/lib/Roundcube/rcube_result_set.php b/program/lib/Roundcube/rcube_result_set.php index 456d1c9d6..a4b070e28 100644 --- a/program/lib/Roundcube/rcube_result_set.php +++ b/program/lib/Roundcube/rcube_result_set.php @@ -2,10 +2,8 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_result_set.php | - | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2011, The Roundcube Dev Team | + | Copyright (C) 2006-2013, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -13,28 +11,28 @@ | | | PURPOSE: | | Class representing an address directory result set | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ */ - /** - * Roundcube result set class. + * Roundcube result set class + * * Representing an address directory result set. + * Implenets Iterator and thus be used in foreach() loops. * * @package Framework * @subpackage Addressbook */ -class rcube_result_set +class rcube_result_set implements Iterator { - var $count = 0; - var $first = 0; - var $current = 0; - var $searchonly = false; - var $records = array(); + public $count = 0; + public $first = 0; + public $searchonly = false; + public $records = array(); + private $current = 0; function __construct($c=0, $f=0) { @@ -55,18 +53,39 @@ class rcube_result_set function first() { $this->current = 0; - return $this->records[$this->current++]; + return $this->records[$this->current]; + } + + function seek($i) + { + $this->current = $i; + } + + /*** PHP 5 Iterator interface ***/ + + function rewind() + { + $this->current = 0; + } + + function current() + { + return $this->records[$this->current]; + } + + function key() + { + return $this->current; } - // alias for iterate() function next() { return $this->iterate(); } - function seek($i) + function valid() { - $this->current = $i; + return isset($this->records[$this->current]); } } diff --git a/program/lib/Roundcube/rcube_result_thread.php b/program/lib/Roundcube/rcube_result_thread.php index c609bdc39..7657550be 100644 --- a/program/lib/Roundcube/rcube_result_thread.php +++ b/program/lib/Roundcube/rcube_result_thread.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_result_thread.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2011, The Roundcube Dev Team | | Copyright (C) 2011, Kolab Systems AG | @@ -14,14 +12,12 @@ | | | PURPOSE: | | THREAD response handler | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Class for accessing IMAP's THREAD result * diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php index fdbf668ca..dedde2284 100644 --- a/program/lib/Roundcube/rcube_session.php +++ b/program/lib/Roundcube/rcube_session.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_session.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2011, Kolab Systems AG | @@ -14,7 +12,6 @@ | | | PURPOSE: | | Provide database supported session management | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | @@ -31,602 +28,689 @@ */ class rcube_session { - private $db; - private $ip; - private $start; - private $changed; - private $unsets = array(); - private $gc_handlers = array(); - private $cookiename = 'roundcube_sessauth'; - private $vars; - private $key; - private $now; - private $secret = ''; - private $ip_check = false; - private $logging = false; - private $memcache; - - /** - * Default constructor - */ - public function __construct($db, $config) - { - $this->db = $db; - $this->start = microtime(true); - $this->ip = $_SERVER['REMOTE_ADDR']; - $this->logging = $config->get('log_session', false); - - $lifetime = $config->get('session_lifetime', 1) * 60; - $this->set_lifetime($lifetime); - - // use memcache backend - if ($config->get('session_storage', 'db') == 'memcache') { - $this->memcache = rcube::get_instance()->get_memcache(); - - // set custom functions for PHP session management if memcache is available - if ($this->memcache) { - session_set_save_handler( - array($this, 'open'), - array($this, 'close'), - array($this, 'mc_read'), - array($this, 'mc_write'), - array($this, 'mc_destroy'), - array($this, 'gc')); - } - else { - rcube::raise_error(array('code' => 604, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => "Failed to connect to memcached. Please check configuration"), - true, true); - } + private $db; + private $ip; + private $start; + private $changed; + private $reloaded = false; + private $unsets = array(); + private $gc_handlers = array(); + private $cookiename = 'roundcube_sessauth'; + private $vars; + private $key; + private $now; + private $secret = ''; + private $ip_check = false; + private $logging = false; + private $memcache; + + + /** + * Default constructor + */ + public function __construct($db, $config) + { + $this->db = $db; + $this->start = microtime(true); + $this->ip = $_SERVER['REMOTE_ADDR']; + $this->logging = $config->get('log_session', false); + + $lifetime = $config->get('session_lifetime', 1) * 60; + $this->set_lifetime($lifetime); + + // use memcache backend + if ($config->get('session_storage', 'db') == 'memcache') { + $this->memcache = rcube::get_instance()->get_memcache(); + + // set custom functions for PHP session management if memcache is available + if ($this->memcache) { + session_set_save_handler( + array($this, 'open'), + array($this, 'close'), + array($this, 'mc_read'), + array($this, 'mc_write'), + array($this, 'mc_destroy'), + array($this, 'gc')); + } + else { + rcube::raise_error(array('code' => 604, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Failed to connect to memcached. Please check configuration"), + true, true); + } + } + else { + // set custom functions for PHP session management + session_set_save_handler( + array($this, 'open'), + array($this, 'close'), + array($this, 'db_read'), + array($this, 'db_write'), + array($this, 'db_destroy'), + array($this, 'db_gc')); + } } - else { - // set custom functions for PHP session management - session_set_save_handler( - array($this, 'open'), - array($this, 'close'), - array($this, 'db_read'), - array($this, 'db_write'), - array($this, 'db_destroy'), - array($this, 'db_gc')); - } - } - - - public function open($save_path, $session_name) - { - return true; - } - - - public function close() - { - return true; - } - - - /** - * Delete session data for the given key - * - * @param string Session ID - */ - public function destroy($key) - { - return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key); - } - - - /** - * Read session data from database - * - * @param string Session ID - * @return string Session vars - */ - public function db_read($key) - { - $sql_result = $this->db->query( - "SELECT vars, ip, changed FROM ".$this->db->table_name('session') - ." WHERE sess_id = ?", $key); - - if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $this->changed = strtotime($sql_arr['changed']); - $this->ip = $sql_arr['ip']; - $this->vars = base64_decode($sql_arr['vars']); - $this->key = $key; - - return !empty($this->vars) ? (string) $this->vars : ''; + + + public function open($save_path, $session_name) + { + return true; } - return null; - } - - - /** - * Save session data. - * handler for session_read() - * - * @param string Session ID - * @param string Serialized session vars - * @return boolean True on success - */ - public function db_write($key, $vars) - { - $ts = microtime(true); - $now = $this->db->fromunixtime((int)$ts); - - // no session row in DB (db_read() returns false) - if (!$this->key) { - $oldvars = null; + + public function close() + { + return true; } - // use internal data from read() for fast requests (up to 0.5 sec.) - else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) { - $oldvars = $this->vars; + + + /** + * Delete session data for the given key + * + * @param string Session ID + */ + public function destroy($key) + { + return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key); } - else { // else read data again from DB - $oldvars = $this->db_read($key); + + + /** + * Read session data from database + * + * @param string Session ID + * + * @return string Session vars + */ + public function db_read($key) + { + $sql_result = $this->db->query( + "SELECT vars, ip, changed FROM ".$this->db->table_name('session') + ." WHERE sess_id = ?", $key); + + if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { + $this->changed = strtotime($sql_arr['changed']); + $this->ip = $sql_arr['ip']; + $this->vars = base64_decode($sql_arr['vars']); + $this->key = $key; + + return !empty($this->vars) ? (string) $this->vars : ''; + } + + return null; } - if ($oldvars !== null) { - $newvars = $this->_fixvars($vars, $oldvars); - if ($newvars !== $oldvars) { + /** + * Save session data. + * handler for session_read() + * + * @param string Session ID + * @param string Serialized session vars + * + * @return boolean True on success + */ + public function db_write($key, $vars) + { + $ts = microtime(true); + $now = $this->db->fromunixtime((int)$ts); + + // no session row in DB (db_read() returns false) + if (!$this->key) { + $oldvars = null; + } + // use internal data from read() for fast requests (up to 0.5 sec.) + else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) { + $oldvars = $this->vars; + } + else { // else read data again from DB + $oldvars = $this->db_read($key); + } + + if ($oldvars !== null) { + $newvars = $this->_fixvars($vars, $oldvars); + + if ($newvars !== $oldvars) { + $this->db->query( + sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?", + $this->db->table_name('session'), $now), + base64_encode($newvars), $key); + } + else if ($ts - $this->changed > $this->lifetime / 2) { + $this->db->query("UPDATE ".$this->db->table_name('session') + ." SET changed=$now WHERE sess_id=?", $key); + } + } + else { + $this->db->query( + sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ". + "VALUES (?, ?, ?, %s, %s)", + $this->db->table_name('session'), $now, $now), + $key, base64_encode($vars), (string)$this->ip); + } + + return true; + } + + + /** + * Merge vars with old vars and apply unsets + */ + private function _fixvars($vars, $oldvars) + { + if ($oldvars !== null) { + $a_oldvars = $this->unserialize($oldvars); + if (is_array($a_oldvars)) { + // remove unset keys on oldvars + foreach ((array)$this->unsets as $var) { + if (isset($a_oldvars[$var])) { + unset($a_oldvars[$var]); + } + else { + $path = explode('.', $var); + $k = array_pop($path); + $node = &$this->get_node($path, $a_oldvars); + unset($node[$k]); + } + } + + $newvars = $this->serialize(array_merge( + (array)$a_oldvars, (array)$this->unserialize($vars))); + } + else { + $newvars = $vars; + } + } + + $this->unsets = array(); + return $newvars; + } + + + /** + * Handler for session_destroy() + * + * @param string Session ID + * + * @return boolean True on success + */ + public function db_destroy($key) + { + if ($key) { + $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?", + $this->db->table_name('session')), $key); + } + + return true; + } + + + /** + * Garbage collecting function + * + * @param string Session lifetime in seconds + * @return boolean True on success + */ + public function db_gc($maxlifetime) + { + // just delete all expired sessions $this->db->query( - sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?", - $this->db->table_name('session'), $now), - base64_encode($newvars), $key); - } - else if ($ts - $this->changed > $this->lifetime / 2) { - $this->db->query("UPDATE ".$this->db->table_name('session')." SET changed=$now WHERE sess_id=?", $key); - } + sprintf("DELETE FROM %s WHERE changed < %s", + $this->db->table_name('session'), $this->db->fromunixtime(time() - $maxlifetime))); + + $this->gc(); + + return true; + } + + + /** + * Read session data from memcache + * + * @param string Session ID + * @return string Session vars + */ + public function mc_read($key) + { + if ($value = $this->memcache->get($key)) { + $arr = unserialize($value); + $this->changed = $arr['changed']; + $this->ip = $arr['ip']; + $this->vars = $arr['vars']; + $this->key = $key; + + return !empty($this->vars) ? (string) $this->vars : ''; + } + + return null; + } + + + /** + * Save session data. + * handler for session_read() + * + * @param string Session ID + * @param string Serialized session vars + * + * @return boolean True on success + */ + public function mc_write($key, $vars) + { + $ts = microtime(true); + + // no session data in cache (mc_read() returns false) + if (!$this->key) + $oldvars = null; + // use internal data for fast requests (up to 0.5 sec.) + else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) + $oldvars = $this->vars; + else // else read data again + $oldvars = $this->mc_read($key); + + $newvars = $oldvars !== null ? $this->_fixvars($vars, $oldvars) : $vars; + + if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2) { + return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)), + MEMCACHE_COMPRESSED, $this->lifetime); + } + + return true; + } + + + /** + * Handler for session_destroy() with memcache backend + * + * @param string Session ID + * + * @return boolean True on success + */ + public function mc_destroy($key) + { + if ($key) { + // #1488592: use 2nd argument + $this->memcache->delete($key, 0); + } + + return true; + } + + + /** + * Execute registered garbage collector routines + */ + public function gc() + { + foreach ($this->gc_handlers as $fct) { + call_user_func($fct); + } + } + + + /** + * Register additional garbage collector functions + * + * @param mixed Callback function + */ + public function register_gc_handler($func) + { + foreach ($this->gc_handlers as $handler) { + if ($handler == $func) { + return; + } + } + + $this->gc_handlers[] = $func; } - else { - $this->db->query( - sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ". - "VALUES (?, ?, ?, %s, %s)", - $this->db->table_name('session'), $now, $now), - $key, base64_encode($vars), (string)$this->ip); + + + /** + * Generate and set new session id + * + * @param boolean $destroy If enabled the current session will be destroyed + */ + public function regenerate_id($destroy=true) + { + session_regenerate_id($destroy); + + $this->vars = null; + $this->key = session_id(); + + return true; } - return true; - } - - - /** - * Merge vars with old vars and apply unsets - */ - private function _fixvars($vars, $oldvars) - { - if ($oldvars !== null) { - $a_oldvars = $this->unserialize($oldvars); - if (is_array($a_oldvars)) { - foreach ((array)$this->unsets as $k) - unset($a_oldvars[$k]); - - $newvars = $this->serialize(array_merge( - (array)$a_oldvars, (array)$this->unserialize($vars))); - } - else - $newvars = $vars; + + /** + * Append the given value to the certain node in the session data array + * + * @param string Path denoting the session variable where to append the value + * @param string Key name under which to append the new value (use null for appending to an indexed list) + * @param mixed Value to append to the session data array + */ + public function append($path, $key, $value) + { + // re-read session data from DB because it might be outdated + if (!$this->reloaded && microtime(true) - $this->start > 0.5) { + $this->reload(); + $this->reloaded = true; + $this->start = microtime(true); + } + + $node = &$this->get_node(explode('.', $path), $_SESSION); + + if ($key !== null) $node[$key] = $value; + else $node[] = $value; } - $this->unsets = array(); - return $newvars; - } - - - /** - * Handler for session_destroy() - * - * @param string Session ID - * - * @return boolean True on success - */ - public function db_destroy($key) - { - if ($key) { - $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?", $this->db->table_name('session')), $key); + + /** + * Unset a session variable + * + * @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5) + * @return boolean True on success + */ + public function remove($var=null) + { + if (empty($var)) { + return $this->destroy(session_id()); + } + + $this->unsets[] = $var; + + if (isset($_SESSION[$var])) { + unset($_SESSION[$var]); + } + else { + $path = explode('.', $var); + $key = array_pop($path); + $node = &$this->get_node($path, $_SESSION); + unset($node[$key]); + } + + return true; } - return true; - } - - - /** - * Garbage collecting function - * - * @param string Session lifetime in seconds - * @return boolean True on success - */ - public function db_gc($maxlifetime) - { - // just delete all expired sessions - $this->db->query( - sprintf("DELETE FROM %s WHERE changed < %s", - $this->db->table_name('session'), $this->db->fromunixtime(time() - $maxlifetime))); - - $this->gc(); - - return true; - } - - - /** - * Read session data from memcache - * - * @param string Session ID - * @return string Session vars - */ - public function mc_read($key) - { - if ($value = $this->memcache->get($key)) { - $arr = unserialize($value); - $this->changed = $arr['changed']; - $this->ip = $arr['ip']; - $this->vars = $arr['vars']; - $this->key = $key; - - return !empty($this->vars) ? (string) $this->vars : ''; + + /** + * Kill this session + */ + public function kill() + { + $this->vars = null; + $this->ip = $_SERVER['REMOTE_ADDR']; // update IP (might have changed) + $this->destroy(session_id()); + rcube_utils::setcookie($this->cookiename, '-del-', time() - 60); } - return null; - } - - - /** - * Save session data. - * handler for session_read() - * - * @param string Session ID - * @param string Serialized session vars - * @return boolean True on success - */ - public function mc_write($key, $vars) - { - $ts = microtime(true); - - // no session data in cache (mc_read() returns false) - if (!$this->key) - $oldvars = null; - // use internal data for fast requests (up to 0.5 sec.) - else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) - $oldvars = $this->vars; - else // else read data again - $oldvars = $this->mc_read($key); - - $newvars = $oldvars !== null ? $this->_fixvars($vars, $oldvars) : $vars; - - if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2) - return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)), MEMCACHE_COMPRESSED, $this->lifetime); - - return true; - } - - - /** - * Handler for session_destroy() with memcache backend - * - * @param string Session ID - * - * @return boolean True on success - */ - public function mc_destroy($key) - { - if ($key) { - // #1488592: use 2nd argument - $this->memcache->delete($key, 0); + + /** + * Re-read session data from storage backend + */ + public function reload() + { + if ($this->key && $this->memcache) + $data = $this->mc_read($this->key); + else if ($this->key) + $data = $this->db_read($this->key); + + if ($data) + session_decode($data); } - return true; - } + /** + * Returns a reference to the node in data array referenced by the given path. + * e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments'] + */ + private function &get_node($path, &$data_arr) + { + $node = &$data_arr; + if (!empty($path)) { + foreach ((array)$path as $key) { + if (!isset($node[$key])) + $node[$key] = array(); + $node = &$node[$key]; + } + } + + return $node; + } + /** + * Serialize session data + */ + private function serialize($vars) + { + $data = ''; + if (is_array($vars)) { + foreach ($vars as $var=>$value) + $data .= $var.'|'.serialize($value); + } + else { + $data = 'b:0;'; + } - /** - * Execute registered garbage collector routines - */ - public function gc() - { - foreach ($this->gc_handlers as $fct) { - call_user_func($fct); + return $data; } - } - - - /** - * Register additional garbage collector functions - * - * @param mixed Callback function - */ - public function register_gc_handler($func) - { - foreach ($this->gc_handlers as $handler) { - if ($handler == $func) { - return; - } + + + /** + * Unserialize session data + * http://www.php.net/manual/en/function.session-decode.php#56106 + */ + private function unserialize($str) + { + $str = (string)$str; + $endptr = strlen($str); + $p = 0; + + $serialized = ''; + $items = 0; + $level = 0; + + while ($p < $endptr) { + $q = $p; + while ($str[$q] != '|') + if (++$q >= $endptr) + break 2; + + if ($str[$p] == '!') { + $p++; + $has_value = false; + } + else { + $has_value = true; + } + + $name = substr($str, $p, $q - $p); + $q++; + + $serialized .= 's:' . strlen($name) . ':"' . $name . '";'; + + if ($has_value) { + for (;;) { + $p = $q; + switch (strtolower($str[$q])) { + case 'n': // null + case 'b': // boolean + case 'i': // integer + case 'd': // decimal + do $q++; + while ( ($q < $endptr) && ($str[$q] != ';') ); + $q++; + $serialized .= substr($str, $p, $q - $p); + if ($level == 0) + break 2; + break; + case 'r': // reference + $q+= 2; + for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) + $id .= $str[$q]; + $q++; + // increment pointer because of outer array + $serialized .= 'R:' . ($id + 1) . ';'; + if ($level == 0) + break 2; + break; + case 's': // string + $q+=2; + for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) + $length .= $str[$q]; + $q+=2; + $q+= (int)$length + 2; + $serialized .= substr($str, $p, $q - $p); + if ($level == 0) + break 2; + break; + case 'a': // array + case 'o': // object + do $q++; + while ($q < $endptr && $str[$q] != '{'); + $q++; + $level++; + $serialized .= substr($str, $p, $q - $p); + break; + case '}': // end of array|object + $q++; + $serialized .= substr($str, $p, $q - $p); + if (--$level == 0) + break 2; + break; + default: + return false; + } + } + } + else { + $serialized .= 'N;'; + $q += 2; + } + $items++; + $p = $q; + } + + return unserialize( 'a:' . $items . ':{' . $serialized . '}' ); } - $this->gc_handlers[] = $func; - } - - - /** - * Generate and set new session id - * - * @param boolean $destroy If enabled the current session will be destroyed - */ - public function regenerate_id($destroy=true) - { - session_regenerate_id($destroy); - - $this->vars = null; - $this->key = session_id(); - - return true; - } - - - /** - * Unset a session variable - * - * @param string Varibale name - * @return boolean True on success - */ - public function remove($var=null) - { - if (empty($var)) - return $this->destroy(session_id()); - - $this->unsets[] = $var; - unset($_SESSION[$var]); - - return true; - } - - - /** - * Kill this session - */ - public function kill() - { - $this->vars = null; - $this->ip = $_SERVER['REMOTE_ADDR']; // update IP (might have changed) - $this->destroy(session_id()); - rcube_utils::setcookie($this->cookiename, '-del-', time() - 60); - } - - - /** - * Re-read session data from storage backend - */ - public function reload() - { - if ($this->key && $this->memcache) - $data = $this->mc_read($this->key); - else if ($this->key) - $data = $this->db_read($this->key); - - if ($data) - session_decode($data); - } - - - /** - * Serialize session data - */ - private function serialize($vars) - { - $data = ''; - if (is_array($vars)) - foreach ($vars as $var=>$value) - $data .= $var.'|'.serialize($value); - else - $data = 'b:0;'; - return $data; - } - - - /** - * Unserialize session data - * http://www.php.net/manual/en/function.session-decode.php#56106 - */ - private function unserialize($str) - { - $str = (string)$str; - $endptr = strlen($str); - $p = 0; - - $serialized = ''; - $items = 0; - $level = 0; - - while ($p < $endptr) { - $q = $p; - while ($str[$q] != '|') - if (++$q >= $endptr) break 2; - - if ($str[$p] == '!') { - $p++; - $has_value = false; - } else { - $has_value = true; - } - - $name = substr($str, $p, $q - $p); - $q++; - - $serialized .= 's:' . strlen($name) . ':"' . $name . '";'; - - if ($has_value) { - for (;;) { - $p = $q; - switch (strtolower($str[$q])) { - case 'n': /* null */ - case 'b': /* boolean */ - case 'i': /* integer */ - case 'd': /* decimal */ - do $q++; - while ( ($q < $endptr) && ($str[$q] != ';') ); - $q++; - $serialized .= substr($str, $p, $q - $p); - if ($level == 0) break 2; - break; - case 'r': /* reference */ - $q+= 2; - for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q]; - $q++; - $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */ - if ($level == 0) break 2; - break; - case 's': /* string */ - $q+=2; - for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q]; - $q+=2; - $q+= (int)$length + 2; - $serialized .= substr($str, $p, $q - $p); - if ($level == 0) break 2; - break; - case 'a': /* array */ - case 'o': /* object */ - do $q++; - while ( ($q < $endptr) && ($str[$q] != '{') ); - $q++; - $level++; - $serialized .= substr($str, $p, $q - $p); - break; - case '}': /* end of array|object */ - $q++; - $serialized .= substr($str, $p, $q - $p); - if (--$level == 0) break 2; - break; - default: - return false; - } + + /** + * Setter for session lifetime + */ + public function set_lifetime($lifetime) + { + $this->lifetime = max(120, $lifetime); + + // valid time range is now - 1/2 lifetime to now + 1/2 lifetime + $now = time(); + $this->now = $now - ($now % ($this->lifetime / 2)); + } + + + /** + * Getter for remote IP saved with this session + */ + public function get_ip() + { + return $this->ip; + } + + + /** + * Setter for cookie encryption secret + */ + function set_secret($secret) + { + $this->secret = $secret; + } + + + /** + * Enable/disable IP check + */ + function set_ip_check($check) + { + $this->ip_check = $check; + } + + + /** + * Setter for the cookie name used for session cookie + */ + function set_cookiename($cookiename) + { + if ($cookiename) { + $this->cookiename = $cookiename; } - } else { - $serialized .= 'N;'; - $q += 2; - } - $items++; - $p = $q; } - return unserialize( 'a:' . $items . ':{' . $serialized . '}' ); - } - - - /** - * Setter for session lifetime - */ - public function set_lifetime($lifetime) - { - $this->lifetime = max(120, $lifetime); - - // valid time range is now - 1/2 lifetime to now + 1/2 lifetime - $now = time(); - $this->now = $now - ($now % ($this->lifetime / 2)); - } - - - /** - * Getter for remote IP saved with this session - */ - public function get_ip() - { - return $this->ip; - } - - - /** - * Setter for cookie encryption secret - */ - function set_secret($secret) - { - $this->secret = $secret; - } - - - /** - * Enable/disable IP check - */ - function set_ip_check($check) - { - $this->ip_check = $check; - } - - - /** - * Setter for the cookie name used for session cookie - */ - function set_cookiename($cookiename) - { - if ($cookiename) - $this->cookiename = $cookiename; - } - - - /** - * Check session authentication cookie - * - * @return boolean True if valid, False if not - */ - function check_auth() - { - $this->cookie = $_COOKIE[$this->cookiename]; - $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true; - - if (!$result) - $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']); - - if ($result && $this->_mkcookie($this->now) != $this->cookie) { - $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now)); - $result = false; - - // Check if using id from a previous time slot - for ($i = 1; $i <= 2; $i++) { - $prev = $this->now - ($this->lifetime / 2) * $i; - if ($this->_mkcookie($prev) == $this->cookie) { - $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie); - $this->set_auth_cookie(); - $result = true; + + /** + * Check session authentication cookie + * + * @return boolean True if valid, False if not + */ + function check_auth() + { + $this->cookie = $_COOKIE[$this->cookiename]; + $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true; + + if (!$result) { + $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']); + } + + if ($result && $this->_mkcookie($this->now) != $this->cookie) { + $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now)); + $result = false; + + // Check if using id from a previous time slot + for ($i = 1; $i <= 2; $i++) { + $prev = $this->now - ($this->lifetime / 2) * $i; + if ($this->_mkcookie($prev) == $this->cookie) { + $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie); + $this->set_auth_cookie(); + $result = true; + } + } } - } + + if (!$result) { + $this->log("Session authentication failed for " . $this->key + . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev)); + } + + return $result; } - if (!$result) - $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev)); - - return $result; - } - - - /** - * Set session authentication cookie - */ - function set_auth_cookie() - { - $this->cookie = $this->_mkcookie($this->now); - rcube_utils::setcookie($this->cookiename, $this->cookie, 0); - $_COOKIE[$this->cookiename] = $this->cookie; - } - - - /** - * Create session cookie from session data - * - * @param int Time slot to use - */ - function _mkcookie($timeslot) - { - $auth_string = "$this->key,$this->secret,$timeslot"; - return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string)); - } - - /** - * Writes debug information to the log - */ - function log($line) - { - if ($this->logging) - rcube::write_log('session', $line); - } + /** + * Set session authentication cookie + */ + function set_auth_cookie() + { + $this->cookie = $this->_mkcookie($this->now); + rcube_utils::setcookie($this->cookiename, $this->cookie, 0); + $_COOKIE[$this->cookiename] = $this->cookie; + } + + + /** + * Create session cookie from session data + * + * @param int Time slot to use + */ + function _mkcookie($timeslot) + { + $auth_string = "$this->key,$this->secret,$timeslot"; + return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string)); + } + + /** + * Writes debug information to the log + */ + function log($line) + { + if ($this->logging) { + rcube::write_log('session', $line); + } + } } diff --git a/program/lib/Roundcube/rcube_smtp.php b/program/lib/Roundcube/rcube_smtp.php index 96534c0b8..5c7d2203c 100644 --- a/program/lib/Roundcube/rcube_smtp.php +++ b/program/lib/Roundcube/rcube_smtp.php @@ -2,10 +2,8 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_smtp.php | - | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2010, The Roundcube Dev Team | + | Copyright (C) 2005-2012, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -13,15 +11,11 @@ | | | PURPOSE: | | Provide SMTP functionality using socket connections | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ */ -// define headers delimiter -define('SMTP_MIME_CRLF', "\r\n"); - /** * Class to provide SMTP functionality using PEAR Net_SMTP * @@ -32,439 +26,425 @@ define('SMTP_MIME_CRLF', "\r\n"); */ class rcube_smtp { - - private $conn = null; - private $response; - private $error; - - - /** - * SMTP Connection and authentication - * - * @param string Server host - * @param string Server port - * @param string User name - * @param string Password - * - * @return bool Returns true on success, or false on error - */ - public function connect($host=null, $port=null, $user=null, $pass=null) - { - $rcube = rcube::get_instance(); - - // disconnect/destroy $this->conn - $this->disconnect(); - - // reset error/response var - $this->error = $this->response = null; - - // let plugins alter smtp connection config - $CONFIG = $rcube->plugins->exec_hook('smtp_connect', array( - 'smtp_server' => $host ? $host : $rcube->config->get('smtp_server'), - 'smtp_port' => $port ? $port : $rcube->config->get('smtp_port', 25), - 'smtp_user' => $user ? $user : $rcube->config->get('smtp_user'), - 'smtp_pass' => $pass ? $pass : $rcube->config->get('smtp_pass'), - 'smtp_auth_cid' => $rcube->config->get('smtp_auth_cid'), - 'smtp_auth_pw' => $rcube->config->get('smtp_auth_pw'), - 'smtp_auth_type' => $rcube->config->get('smtp_auth_type'), - 'smtp_helo_host' => $rcube->config->get('smtp_helo_host'), - 'smtp_timeout' => $rcube->config->get('smtp_timeout'), - 'smtp_auth_callbacks' => array(), - )); - - $smtp_host = rcube_utils::parse_host($CONFIG['smtp_server']); - // when called from Installer it's possible to have empty $smtp_host here - if (!$smtp_host) $smtp_host = 'localhost'; - $smtp_port = is_numeric($CONFIG['smtp_port']) ? $CONFIG['smtp_port'] : 25; - $smtp_host_url = parse_url($smtp_host); - - // overwrite port - if (isset($smtp_host_url['host']) && isset($smtp_host_url['port'])) + private $conn = null; + private $response; + private $error; + + // define headers delimiter + const SMTP_MIME_CRLF = "\r\n"; + + + /** + * SMTP Connection and authentication + * + * @param string Server host + * @param string Server port + * @param string User name + * @param string Password + * + * @return bool Returns true on success, or false on error + */ + public function connect($host=null, $port=null, $user=null, $pass=null) { - $smtp_host = $smtp_host_url['host']; - $smtp_port = $smtp_host_url['port']; - } + $rcube = rcube::get_instance(); - // re-write smtp host - if (isset($smtp_host_url['host']) && isset($smtp_host_url['scheme'])) - $smtp_host = sprintf('%s://%s', $smtp_host_url['scheme'], $smtp_host_url['host']); + // disconnect/destroy $this->conn + $this->disconnect(); - // remove TLS prefix and set flag for use in Net_SMTP::auth() - if (preg_match('#^tls://#i', $smtp_host)) { - $smtp_host = preg_replace('#^tls://#i', '', $smtp_host); - $use_tls = true; - } + // reset error/response var + $this->error = $this->response = null; + + // let plugins alter smtp connection config + $CONFIG = $rcube->plugins->exec_hook('smtp_connect', array( + 'smtp_server' => $host ? $host : $rcube->config->get('smtp_server'), + 'smtp_port' => $port ? $port : $rcube->config->get('smtp_port', 25), + 'smtp_user' => $user ? $user : $rcube->config->get('smtp_user'), + 'smtp_pass' => $pass ? $pass : $rcube->config->get('smtp_pass'), + 'smtp_auth_cid' => $rcube->config->get('smtp_auth_cid'), + 'smtp_auth_pw' => $rcube->config->get('smtp_auth_pw'), + 'smtp_auth_type' => $rcube->config->get('smtp_auth_type'), + 'smtp_helo_host' => $rcube->config->get('smtp_helo_host'), + 'smtp_timeout' => $rcube->config->get('smtp_timeout'), + 'smtp_auth_callbacks' => array(), + )); + + $smtp_host = rcube_utils::parse_host($CONFIG['smtp_server']); + // when called from Installer it's possible to have empty $smtp_host here + if (!$smtp_host) $smtp_host = 'localhost'; + $smtp_port = is_numeric($CONFIG['smtp_port']) ? $CONFIG['smtp_port'] : 25; + $smtp_host_url = parse_url($smtp_host); + + // overwrite port + if (isset($smtp_host_url['host']) && isset($smtp_host_url['port'])) { + $smtp_host = $smtp_host_url['host']; + $smtp_port = $smtp_host_url['port']; + } - if (!empty($CONFIG['smtp_helo_host'])) - $helo_host = $CONFIG['smtp_helo_host']; - else if (!empty($_SERVER['SERVER_NAME'])) - $helo_host = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']); - else - $helo_host = 'localhost'; + // re-write smtp host + if (isset($smtp_host_url['host']) && isset($smtp_host_url['scheme'])) { + $smtp_host = sprintf('%s://%s', $smtp_host_url['scheme'], $smtp_host_url['host']); + } - // IDNA Support - $smtp_host = rcube_utils::idn_to_ascii($smtp_host); + // remove TLS prefix and set flag for use in Net_SMTP::auth() + if (preg_match('#^tls://#i', $smtp_host)) { + $smtp_host = preg_replace('#^tls://#i', '', $smtp_host); + $use_tls = true; + } - $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host); + if (!empty($CONFIG['smtp_helo_host'])) { + $helo_host = $CONFIG['smtp_helo_host']; + } + else if (!empty($_SERVER['SERVER_NAME'])) { + $helo_host = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']); + } + else { + $helo_host = 'localhost'; + } - if ($rcube->config->get('smtp_debug')) - $this->conn->setDebug(true, array($this, 'debug_handler')); + // IDNA Support + $smtp_host = rcube_utils::idn_to_ascii($smtp_host); - // register authentication methods - if (!empty($CONFIG['smtp_auth_callbacks']) && method_exists($this->conn, 'setAuthMethod')) { - foreach ($CONFIG['smtp_auth_callbacks'] as $callback) { - $this->conn->setAuthMethod($callback['name'], $callback['function'], - isset($callback['prepend']) ? $callback['prepend'] : true); - } - } + $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host); - // try to connect to server and exit on failure - $result = $this->conn->connect($smtp_timeout); + if ($rcube->config->get('smtp_debug')) { + $this->conn->setDebug(true, array($this, 'debug_handler')); + } - if (PEAR::isError($result)) { - $this->response[] = "Connection failed: ".$result->getMessage(); - $this->error = array('label' => 'smtpconnerror', 'vars' => array('code' => $this->conn->_code)); - $this->conn = null; - return false; - } + // register authentication methods + if (!empty($CONFIG['smtp_auth_callbacks']) && method_exists($this->conn, 'setAuthMethod')) { + foreach ($CONFIG['smtp_auth_callbacks'] as $callback) { + $this->conn->setAuthMethod($callback['name'], $callback['function'], + isset($callback['prepend']) ? $callback['prepend'] : true); + } + } - // workaround for timeout bug in Net_SMTP 1.5.[0-1] (#1487843) - if (method_exists($this->conn, 'setTimeout') - && ($timeout = ini_get('default_socket_timeout')) - ) { - $this->conn->setTimeout($timeout); - } + // try to connect to server and exit on failure + $result = $this->conn->connect($smtp_timeout); - $smtp_user = str_replace('%u', $rcube->get_user_name(), $CONFIG['smtp_user']); - $smtp_pass = str_replace('%p', $rcube->get_user_password(), $CONFIG['smtp_pass']); - $smtp_auth_type = empty($CONFIG['smtp_auth_type']) ? NULL : $CONFIG['smtp_auth_type']; + if (PEAR::isError($result)) { + $this->response[] = "Connection failed: ".$result->getMessage(); + $this->error = array('label' => 'smtpconnerror', 'vars' => array('code' => $this->conn->_code)); + $this->conn = null; + return false; + } - if (!empty($CONFIG['smtp_auth_cid'])) { - $smtp_authz = $smtp_user; - $smtp_user = $CONFIG['smtp_auth_cid']; - $smtp_pass = $CONFIG['smtp_auth_pw']; - } + // workaround for timeout bug in Net_SMTP 1.5.[0-1] (#1487843) + if (method_exists($this->conn, 'setTimeout') + && ($timeout = ini_get('default_socket_timeout')) + ) { + $this->conn->setTimeout($timeout); + } - // attempt to authenticate to the SMTP server - if ($smtp_user && $smtp_pass) - { - // IDNA Support - if (strpos($smtp_user, '@')) { - $smtp_user = rcube_utils::idn_to_ascii($smtp_user); - } - - $result = $this->conn->auth($smtp_user, $smtp_pass, $smtp_auth_type, $use_tls, $smtp_authz); - - if (PEAR::isError($result)) - { - $this->error = array('label' => 'smtpautherror', 'vars' => array('code' => $this->conn->_code)); - $this->response[] .= 'Authentication failure: ' . $result->getMessage() . ' (Code: ' . $result->getCode() . ')'; - $this->reset(); - $this->disconnect(); - return false; - } - } + $smtp_user = str_replace('%u', $rcube->get_user_name(), $CONFIG['smtp_user']); + $smtp_pass = str_replace('%p', $rcube->get_user_password(), $CONFIG['smtp_pass']); + $smtp_auth_type = empty($CONFIG['smtp_auth_type']) ? NULL : $CONFIG['smtp_auth_type']; - return true; - } - - - /** - * Function for sending mail - * - * @param string Sender e-Mail address - * - * @param mixed Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. This may contain recipients not - * specified in the headers, for Bcc:, resending - * messages, etc. - * @param mixed The message headers to send with the mail - * Either as an associative array or a finally - * formatted string - * @param mixed The full text of the message body, including any Mime parts - * or file handle - * @param array Delivery options (e.g. DSN request) - * - * @return bool Returns true on success, or false on error - */ - public function send_mail($from, $recipients, &$headers, &$body, $opts=null) - { - if (!is_object($this->conn)) - return false; - - // prepare message headers as string - if (is_array($headers)) - { - if (!($headerElements = $this->_prepare_headers($headers))) { - $this->reset(); - return false; - } + if (!empty($CONFIG['smtp_auth_cid'])) { + $smtp_authz = $smtp_user; + $smtp_user = $CONFIG['smtp_auth_cid']; + $smtp_pass = $CONFIG['smtp_auth_pw']; + } - list($from, $text_headers) = $headerElements; - } - else if (is_string($headers)) - $text_headers = $headers; - else - { - $this->reset(); - $this->response[] = "Invalid message headers"; - return false; + // attempt to authenticate to the SMTP server + if ($smtp_user && $smtp_pass) { + // IDNA Support + if (strpos($smtp_user, '@')) { + $smtp_user = rcube_utils::idn_to_ascii($smtp_user); + } + + $result = $this->conn->auth($smtp_user, $smtp_pass, $smtp_auth_type, $use_tls, $smtp_authz); + + if (PEAR::isError($result)) { + $this->error = array('label' => 'smtpautherror', 'vars' => array('code' => $this->conn->_code)); + $this->response[] .= 'Authentication failure: ' . $result->getMessage() . ' (Code: ' . $result->getCode() . ')'; + $this->reset(); + $this->disconnect(); + return false; + } + } + + return true; } - // exit if no from address is given - if (!isset($from)) + /** + * Function for sending mail + * + * @param string Sender e-Mail address + * + * @param mixed Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. This may contain recipients not + * specified in the headers, for Bcc:, resending + * messages, etc. + * @param mixed The message headers to send with the mail + * Either as an associative array or a finally + * formatted string + * @param mixed The full text of the message body, including any Mime parts + * or file handle + * @param array Delivery options (e.g. DSN request) + * + * @return bool Returns true on success, or false on error + */ + public function send_mail($from, $recipients, &$headers, &$body, $opts=null) { - $this->reset(); - $this->response[] = "No From address has been provided"; - return false; - } + if (!is_object($this->conn)) { + return false; + } - // RFC3461: Delivery Status Notification - if ($opts['dsn']) { - $exts = $this->conn->getServiceExtensions(); + // prepare message headers as string + if (is_array($headers)) { + if (!($headerElements = $this->_prepare_headers($headers))) { + $this->reset(); + return false; + } - if (isset($exts['DSN'])) { - $from_params = 'RET=HDRS'; - $recipient_params = 'NOTIFY=SUCCESS,FAILURE'; - } - } + list($from, $text_headers) = $headerElements; + } + else if (is_string($headers)) { + $text_headers = $headers; + } + else { + $this->reset(); + $this->response[] = "Invalid message headers"; + return false; + } + + // exit if no from address is given + if (!isset($from)) { + $this->reset(); + $this->response[] = "No From address has been provided"; + return false; + } + + // RFC3461: Delivery Status Notification + if ($opts['dsn']) { + $exts = $this->conn->getServiceExtensions(); + + if (isset($exts['DSN'])) { + $from_params = 'RET=HDRS'; + $recipient_params = 'NOTIFY=SUCCESS,FAILURE'; + } + } + + // RFC2298.3: remove envelope sender address + if (empty($opts['mdn_use_from']) + && preg_match('/Content-Type: multipart\/report/', $text_headers) + && preg_match('/report-type=disposition-notification/', $text_headers) + ) { + $from = ''; + } + + // set From: address + if (PEAR::isError($this->conn->mailFrom($from, $from_params))) { + $err = $this->conn->getResponse(); + $this->error = array('label' => 'smtpfromerror', 'vars' => array( + 'from' => $from, 'code' => $this->conn->_code, 'msg' => $err[1])); + $this->response[] = "Failed to set sender '$from'"; + $this->reset(); + return false; + } + + // prepare list of recipients + $recipients = $this->_parse_rfc822($recipients); + if (PEAR::isError($recipients)) { + $this->error = array('label' => 'smtprecipientserror'); + $this->reset(); + return false; + } - // RFC2298.3: remove envelope sender address - if (preg_match('/Content-Type: multipart\/report/', $text_headers) - && preg_match('/report-type=disposition-notification/', $text_headers) - ) { - $from = ''; + // set mail recipients + foreach ($recipients as $recipient) { + if (PEAR::isError($this->conn->rcptTo($recipient, $recipient_params))) { + $err = $this->conn->getResponse(); + $this->error = array('label' => 'smtptoerror', 'vars' => array( + 'to' => $recipient, 'code' => $this->conn->_code, 'msg' => $err[1])); + $this->response[] = "Failed to add recipient '$recipient'"; + $this->reset(); + return false; + } + } + + if (is_resource($body)) { + // file handle + $data = $body; + $text_headers = preg_replace('/[\r\n]+$/', '', $text_headers); + } + else { + // Concatenate headers and body so it can be passed by reference to SMTP_CONN->data + // so preg_replace in SMTP_CONN->quotedata will store a reference instead of a copy. + // We are still forced to make another copy here for a couple ticks so we don't really + // get to save a copy in the method call. + $data = $text_headers . "\r\n" . $body; + + // unset old vars to save data and so we can pass into SMTP_CONN->data by reference. + unset($text_headers, $body); + } + + // Send the message's headers and the body as SMTP data. + if (PEAR::isError($result = $this->conn->data($data, $text_headers))) { + $err = $this->conn->getResponse(); + if (!in_array($err[0], array(354, 250, 221))) { + $msg = sprintf('[%d] %s', $err[0], $err[1]); + } + else { + $msg = $result->getMessage(); + } + + $this->error = array('label' => 'smtperror', 'vars' => array('msg' => $msg)); + $this->response[] = "Failed to send data"; + $this->reset(); + return false; + } + + $this->response[] = join(': ', $this->conn->getResponse()); + return true; } - // set From: address - if (PEAR::isError($this->conn->mailFrom($from, $from_params))) + /** + * Reset the global SMTP connection + */ + public function reset() { - $err = $this->conn->getResponse(); - $this->error = array('label' => 'smtpfromerror', 'vars' => array( - 'from' => $from, 'code' => $this->conn->_code, 'msg' => $err[1])); - $this->response[] = "Failed to set sender '$from'"; - $this->reset(); - return false; + if (is_object($this->conn)) { + $this->conn->rset(); + } } - // prepare list of recipients - $recipients = $this->_parse_rfc822($recipients); - if (PEAR::isError($recipients)) + /** + * Disconnect the global SMTP connection + */ + public function disconnect() { - $this->error = array('label' => 'smtprecipientserror'); - $this->reset(); - return false; + if (is_object($this->conn)) { + $this->conn->disconnect(); + $this->conn = null; + } } - // set mail recipients - foreach ($recipients as $recipient) + + /** + * This is our own debug handler for the SMTP connection + */ + public function debug_handler(&$smtp, $message) { - if (PEAR::isError($this->conn->rcptTo($recipient, $recipient_params))) { - $err = $this->conn->getResponse(); - $this->error = array('label' => 'smtptoerror', 'vars' => array( - 'to' => $recipient, 'code' => $this->conn->_code, 'msg' => $err[1])); - $this->response[] = "Failed to add recipient '$recipient'"; - $this->reset(); - return false; - } + rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message)); } - if (is_resource($body)) + /** + * Get error message + */ + public function get_error() { - // file handle - $data = $body; - $text_headers = preg_replace('/[\r\n]+$/', '', $text_headers); - } else { - // Concatenate headers and body so it can be passed by reference to SMTP_CONN->data - // so preg_replace in SMTP_CONN->quotedata will store a reference instead of a copy. - // We are still forced to make another copy here for a couple ticks so we don't really - // get to save a copy in the method call. - $data = $text_headers . "\r\n" . $body; - - // unset old vars to save data and so we can pass into SMTP_CONN->data by reference. - unset($text_headers, $body); + return $this->error; } - // Send the message's headers and the body as SMTP data. - if (PEAR::isError($result = $this->conn->data($data, $text_headers))) + /** + * Get server response messages array + */ + public function get_response() { - $err = $this->conn->getResponse(); - if (!in_array($err[0], array(354, 250, 221))) - $msg = sprintf('[%d] %s', $err[0], $err[1]); - else - $msg = $result->getMessage(); - - $this->error = array('label' => 'smtperror', 'vars' => array('msg' => $msg)); - $this->response[] = "Failed to send data"; - $this->reset(); - return false; + return $this->response; } - $this->response[] = join(': ', $this->conn->getResponse()); - return true; - } - - - /** - * Reset the global SMTP connection - * @access public - */ - public function reset() - { - if (is_object($this->conn)) - $this->conn->rset(); - } - - - /** - * Disconnect the global SMTP connection - * @access public - */ - public function disconnect() - { - if (is_object($this->conn)) { - $this->conn->disconnect(); - $this->conn = null; - } - } - - - /** - * This is our own debug handler for the SMTP connection - * @access public - */ - public function debug_handler(&$smtp, $message) - { - rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message)); - } - - - /** - * Get error message - * @access public - */ - public function get_error() - { - return $this->error; - } - - - /** - * Get server response messages array - * @access public - */ - public function get_response() - { - return $this->response; - } - - - /** - * Take an array of mail headers and return a string containing - * text usable in sending a message. - * - * @param array $headers The array of headers to prepare, in an associative - * array, where the array key is the header name (ie, - * 'Subject'), and the array value is the header - * value (ie, 'test'). The header produced from those - * values would be 'Subject: test'. - * - * @return mixed Returns false if it encounters a bad address, - * otherwise returns an array containing two - * elements: Any From: address found in the headers, - * and the plain text version of the headers. - * @access private - */ - private function _prepare_headers($headers) - { - $lines = array(); - $from = null; - - foreach ($headers as $key => $value) + /** + * Take an array of mail headers and return a string containing + * text usable in sending a message. + * + * @param array $headers The array of headers to prepare, in an associative + * array, where the array key is the header name (ie, + * 'Subject'), and the array value is the header + * value (ie, 'test'). The header produced from those + * values would be 'Subject: test'. + * + * @return mixed Returns false if it encounters a bad address, + * otherwise returns an array containing two + * elements: Any From: address found in the headers, + * and the plain text version of the headers. + */ + private function _prepare_headers($headers) { - if (strcasecmp($key, 'From') === 0) - { - $addresses = $this->_parse_rfc822($value); - - if (is_array($addresses)) - $from = $addresses[0]; - - // Reject envelope From: addresses with spaces. - if (strpos($from, ' ') !== false) - return false; - - $lines[] = $key . ': ' . $value; - } - else if (strcasecmp($key, 'Received') === 0) - { - $received = array(); - if (is_array($value)) - { - foreach ($value as $line) - $received[] = $key . ': ' . $line; - } - else - { - $received[] = $key . ': ' . $value; + $lines = array(); + $from = null; + + foreach ($headers as $key => $value) { + if (strcasecmp($key, 'From') === 0) { + $addresses = $this->_parse_rfc822($value); + + if (is_array($addresses)) { + $from = $addresses[0]; + } + + // Reject envelope From: addresses with spaces. + if (strpos($from, ' ') !== false) { + return false; + } + + $lines[] = $key . ': ' . $value; + } + else if (strcasecmp($key, 'Received') === 0) { + $received = array(); + if (is_array($value)) { + foreach ($value as $line) { + $received[] = $key . ': ' . $line; + } + } + else { + $received[] = $key . ': ' . $value; + } + + // Put Received: headers at the top. Spam detectors often + // flag messages with Received: headers after the Subject: + // as spam. + $lines = array_merge($received, $lines); + } + else { + // If $value is an array (i.e., a list of addresses), convert + // it to a comma-delimited string of its elements (addresses). + if (is_array($value)) { + $value = implode(', ', $value); + } + + $lines[] = $key . ': ' . $value; + } } - // Put Received: headers at the top. Spam detectors often - // flag messages with Received: headers after the Subject: - // as spam. - $lines = array_merge($received, $lines); - } - else - { - // If $value is an array (i.e., a list of addresses), convert - // it to a comma-delimited string of its elements (addresses). - if (is_array($value)) - $value = implode(', ', $value); - - $lines[] = $key . ': ' . $value; - } + return array($from, join(self::SMTP_MIME_CRLF, $lines) . self::SMTP_MIME_CRLF); } - return array($from, join(SMTP_MIME_CRLF, $lines) . SMTP_MIME_CRLF); - } - - /** - * Take a set of recipients and parse them, returning an array of - * bare addresses (forward paths) that can be passed to sendmail - * or an smtp server with the rcpt to: command. - * - * @param mixed Either a comma-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. - * - * @return array An array of forward paths (bare addresses). - * @access private - */ - private function _parse_rfc822($recipients) - { - // if we're passed an array, assume addresses are valid and implode them before parsing. - if (is_array($recipients)) - $recipients = implode(', ', $recipients); - - $addresses = array(); - $recipients = rcube_utils::explode_quoted_string(',', $recipients); - - reset($recipients); - while (list($k, $recipient) = each($recipients)) + /** + * Take a set of recipients and parse them, returning an array of + * bare addresses (forward paths) that can be passed to sendmail + * or an smtp server with the rcpt to: command. + * + * @param mixed Either a comma-seperated list of recipients + * (RFC822 compliant), or an array of recipients, + * each RFC822 valid. + * + * @return array An array of forward paths (bare addresses). + */ + private function _parse_rfc822($recipients) { - $a = rcube_utils::explode_quoted_string(' ', $recipient); - while (list($k2, $word) = each($a)) - { - if (strpos($word, "@") > 0 && $word[strlen($word)-1] != '"') - { - $word = preg_replace('/^<|>$/', '', trim($word)); - if (in_array($word, $addresses)===false) - array_push($addresses, $word); + // if we're passed an array, assume addresses are valid and implode them before parsing. + if (is_array($recipients)) { + $recipients = implode(', ', $recipients); } - } - } - return $addresses; - } + $addresses = array(); + $recipients = rcube_utils::explode_quoted_string(',', $recipients); + + reset($recipients); + while (list($k, $recipient) = each($recipients)) { + $a = rcube_utils::explode_quoted_string(' ', $recipient); + while (list($k2, $word) = each($a)) { + if (strpos($word, "@") > 0 && $word[strlen($word)-1] != '"') { + $word = preg_replace('/^<|>$/', '', trim($word)); + if (in_array($word, $addresses) === false) { + array_push($addresses, $word); + } + } + } + } + return $addresses; + } } diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php index fce2cac75..816bcad2f 100644 --- a/program/lib/Roundcube/rcube_spellchecker.php +++ b/program/lib/Roundcube/rcube_spellchecker.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_spellchecker.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2011, Kolab Systems AG | | Copyright (C) 2008-2011, The Roundcube Dev Team | @@ -14,14 +12,12 @@ | | | PURPOSE: | | Spellchecking using different backends | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <machniak@kolabsys.com> | | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ */ - /** * Helper class for spellchecking with Googielspell and PSpell support. * @@ -35,7 +31,7 @@ class rcube_spellchecker private $lang; private $rc; private $error; - private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.]([^\w]|$)/'; + private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/'; private $options = array(); private $dict; private $have_dict; @@ -447,7 +443,7 @@ class rcube_spellchecker private function html2text($text) { - $h2t = new html2text($text, false, true, 0); + $h2t = new rcube_html2text($text, false, true, 0); return $h2t->get_text(); } diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php index 1556aae41..700d12ffb 100644 --- a/program/lib/Roundcube/rcube_storage.php +++ b/program/lib/Roundcube/rcube_storage.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_storage.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | Copyright (C) 2012, Kolab Systems AG | @@ -14,14 +12,12 @@ | | | PURPOSE: | | Mail Storage Engine | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Abstract class for accessing mail messages storage server * @@ -57,6 +53,7 @@ abstract class rcube_storage protected $all_headers = array( 'IN-REPLY-TO', 'BCC', + 'SENDER', 'MESSAGE-ID', 'CONTENT-TRANSFER-ENCODING', 'REFERENCES', @@ -64,6 +61,8 @@ abstract class rcube_storage 'MAIL-FOLLOWUP-TO', 'MAIL-REPLY-TO', 'RETURN-PATH', + 'DELIVERED-TO', + 'ENVELOPE-TO', ); const UNKNOWN = 0; @@ -352,7 +351,7 @@ abstract class rcube_storage * Get messages count for a specific folder. * * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] + * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS] * @param boolean $force Force reading from server and update cache * @param boolean $status Enables storing folder status info (max UID/count), * required for folder_status() @@ -808,13 +807,14 @@ abstract class rcube_storage /** - * Returns current status of a folder + * Returns current status of a folder (compared to the last time use) * * @param string $folder Folder name + * @param array $diff Difference data * * @return int Folder status */ - abstract function folder_status($folder = null); + abstract function folder_status($folder = null, &$diff = array()); /** diff --git a/program/lib/Roundcube/rcube_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php index 584b9f68c..b8768bc98 100644 --- a/program/lib/Roundcube/rcube_string_replacer.php +++ b/program/lib/Roundcube/rcube_string_replacer.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_string_replacer.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2009-2012, The Roundcube Dev Team | | | @@ -13,13 +11,11 @@ | | | PURPOSE: | | Handle string replacements based on preg_replace_callback | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | +-----------------------------------------------------------------------+ */ - /** * Helper class for string replacements based on preg_replace_callback * @@ -28,164 +24,192 @@ */ class rcube_string_replacer { - public static $pattern = '/##str_replacement\[([0-9]+)\]##/'; - public $mailto_pattern; - public $link_pattern; - private $values = array(); - - - function __construct() - { - // Simplified domain expression for UTF8 characters handling - // Support unicode/punycode in top-level domain part - $utf_domain = '[^?&@"\'\\/()\s\r\t\n]+\\.?([^\\x00-\\x2f\\x3b-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-zA-Z0-9]{2,})'; - $url1 = '.:;,'; - $url2 = 'a-zA-Z0-9%=#$@+?!&\\/_~\\[\\]{}\*-'; - - $this->link_pattern = "/([\w]+:\/\/|\W[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)($utf_domain([$url1]?[$url2]+)*)/"; - $this->mailto_pattern = "/(" - ."[-\w!\#\$%&\'*+~\/^`|{}=]+(?:\.[-\w!\#\$%&\'*+~\/^`|{}=]+)*" // local-part - ."@$utf_domain" // domain-part - ."(\?[$url1$url2]+)?" // e.g. ?subject=test... - .")/"; - } - - /** - * Add a string to the internal list - * - * @param string String value - * @return int Index of value for retrieval - */ - public function add($str) - { - $i = count($this->values); - $this->values[$i] = $str; - return $i; - } - - /** - * Build replacement string - */ - public function get_replacement($i) - { - return '##str_replacement['.$i.']##'; - } - - /** - * Callback function used to build HTML links around URL strings - * - * @param array Matches result from preg_replace_callback - * @return int Index of saved string value - */ - public function link_callback($matches) - { - $i = -1; - $scheme = strtolower($matches[1]); - - if (preg_match('!^(http|ftp|file)s?://!i', $scheme)) { - $url = $matches[1] . $matches[2]; + public static $pattern = '/##str_replacement\[([0-9]+)\]##/'; + public $mailto_pattern; + public $link_pattern; + private $values = array(); + private $options = array(); + + + function __construct($options = array()) + { + // Simplified domain expression for UTF8 characters handling + // Support unicode/punycode in top-level domain part + $utf_domain = '[^?&@"\'\\/()<>\s\r\t\n]+\\.?([^\\x00-\\x2f\\x3b-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-zA-Z0-9]{2,})'; + $url1 = '.:;,'; + $url2 = 'a-zA-Z0-9%=#$@+?!&\\/_~\\[\\]\\(\\){}\*-'; + + $this->link_pattern = "/([\w]+:\/\/|\W[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)($utf_domain([$url1]?[$url2]+)*)/"; + $this->mailto_pattern = "/(" + ."[-\w!\#\$%&\'*+~\/^`|{}=]+(?:\.[-\w!\#\$%&\'*+~\/^`|{}=]+)*" // local-part + ."@$utf_domain" // domain-part + ."(\?[$url1$url2]+)?" // e.g. ?subject=test... + .")/"; + + $this->options = $options; } - else if (preg_match('/^(\W*)(www\.)$/i', $matches[1], $m)) { - $url = $m[2] . $matches[2]; - $url_prefix = 'http://'; - $prefix = $m[1]; + + /** + * Add a string to the internal list + * + * @param string String value + * @return int Index of value for retrieval + */ + public function add($str) + { + $i = count($this->values); + $this->values[$i] = $str; + return $i; } - if ($url) { - $suffix = $this->parse_url_brackets($url); - $i = $this->add($prefix . html::a(array( - 'href' => $url_prefix . $url, - 'target' => '_blank' - ), rcube::Q($url)) . $suffix); + /** + * Build replacement string + */ + public function get_replacement($i) + { + return '##str_replacement['.$i.']##'; } - // Return valid link for recognized schemes, otherwise, return the unmodified string for unrecognized schemes. - return $i >= 0 ? $this->get_replacement($i) : $matches[0]; - } - - /** - * Callback function used to build mailto: links around e-mail strings - * - * @param array Matches result from preg_replace_callback - * @return int Index of saved string value - */ - public function mailto_callback($matches) - { - $href = $matches[1]; - $suffix = $this->parse_url_brackets($href); - $i = $this->add(html::a('mailto:' . $href, rcube::Q($href)) . $suffix); - - return $i >= 0 ? $this->get_replacement($i) : ''; - } - - /** - * Look up the index from the preg_replace matches array - * and return the substitution value. - * - * @param array Matches result from preg_replace_callback - * @return string Value at index $matches[1] - */ - public function replace_callback($matches) - { - return $this->values[$matches[1]]; - } - - /** - * Replace all defined (link|mailto) patterns with replacement string - * - * @param string $str Text - * - * @return string Text - */ - public function replace($str) - { - // search for patterns like links and e-mail addresses - $str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str); - $str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str); - - return $str; - } - - /** - * Replace substituted strings with original values - */ - public function resolve($str) - { - return preg_replace_callback(self::$pattern, array($this, 'replace_callback'), $str); - } - - /** - * Fixes bracket characters in URL handling - */ - public static function parse_url_brackets(&$url) - { - // #1487672: special handling of square brackets, - // URL regexp allows [] characters in URL, for example: - // "http://example.com/?a[b]=c". However we need to handle - // properly situation when a bracket is placed at the end - // of the link e.g. "[http://example.com]" - if (preg_match('/(\\[|\\])/', $url)) { - $in = false; - for ($i=0, $len=strlen($url); $i<$len; $i++) { - if ($url[$i] == '[') { - if ($in) - break; - $in = true; + /** + * Callback function used to build HTML links around URL strings + * + * @param array Matches result from preg_replace_callback + * @return int Index of saved string value + */ + public function link_callback($matches) + { + $i = -1; + $scheme = strtolower($matches[1]); + + if (preg_match('!^(http|ftp|file)s?://!i', $scheme)) { + $url = $matches[1] . $matches[2]; + } + else if (preg_match('/^(\W*)(www\.)$/i', $matches[1], $m)) { + $url = $m[2] . $matches[2]; + $url_prefix = 'http://'; + $prefix = $m[1]; } - else if ($url[$i] == ']') { - if (!$in) - break; - $in = false; + + if ($url) { + $suffix = $this->parse_url_brackets($url); + $attrib = (array)$this->options['link_attribs']; + $attrib['href'] = $url_prefix . $url; + + $i = $this->add($prefix . html::a($attrib, rcube::Q($url)) . $suffix); } - } - if ($i<$len) { - $suffix = substr($url, $i); - $url = substr($url, 0, $i); - } + // Return valid link for recognized schemes, otherwise + // return the unmodified string for unrecognized schemes. + return $i >= 0 ? $this->get_replacement($i) : $matches[0]; } - return $suffix; - } + /** + * Callback function used to build mailto: links around e-mail strings + * + * @param array Matches result from preg_replace_callback + * @return int Index of saved string value + */ + public function mailto_callback($matches) + { + $href = $matches[1]; + $suffix = $this->parse_url_brackets($href); + $i = $this->add(html::a('mailto:' . $href, rcube::Q($href)) . $suffix); + + return $i >= 0 ? $this->get_replacement($i) : ''; + } + + /** + * Look up the index from the preg_replace matches array + * and return the substitution value. + * + * @param array Matches result from preg_replace_callback + * @return string Value at index $matches[1] + */ + public function replace_callback($matches) + { + return $this->values[$matches[1]]; + } + /** + * Replace all defined (link|mailto) patterns with replacement string + * + * @param string $str Text + * + * @return string Text + */ + public function replace($str) + { + // search for patterns like links and e-mail addresses + $str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str); + $str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str); + + return $str; + } + + /** + * Replace substituted strings with original values + */ + public function resolve($str) + { + return preg_replace_callback(self::$pattern, array($this, 'replace_callback'), $str); + } + + /** + * Fixes bracket characters in URL handling + */ + public static function parse_url_brackets(&$url) + { + // #1487672: special handling of square brackets, + // URL regexp allows [] characters in URL, for example: + // "http://example.com/?a[b]=c". However we need to handle + // properly situation when a bracket is placed at the end + // of the link e.g. "[http://example.com]" + // Yes, this is not perfect handles correctly only paired characters + // but it should work for common cases + + if (preg_match('/(\\[|\\])/', $url)) { + $in = false; + for ($i=0, $len=strlen($url); $i<$len; $i++) { + if ($url[$i] == '[') { + if ($in) + break; + $in = true; + } + else if ($url[$i] == ']') { + if (!$in) + break; + $in = false; + } + } + + if ($i < $len) { + $suffix = substr($url, $i); + $url = substr($url, 0, $i); + } + } + + // Do the same for parentheses + if (preg_match('/(\\(|\\))/', $url)) { + $in = false; + for ($i=0, $len=strlen($url); $i<$len; $i++) { + if ($url[$i] == '(') { + if ($in) + break; + $in = true; + } + else if ($url[$i] == ')') { + if (!$in) + break; + $in = false; + } + } + + if ($i < $len) { + $suffix = substr($url, $i); + $url = substr($url, 0, $i); + } + } + + return $suffix; + } } diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php index 864f2e098..505b190d1 100644 --- a/program/lib/Roundcube/rcube_user.php +++ b/program/lib/Roundcube/rcube_user.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_user.inc | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -14,14 +12,12 @@ | PURPOSE: | | This class represents a system user linked and provides access | | to the related database records. | - | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Class representing a system user * @@ -240,10 +236,12 @@ class rcube_user /** * Return a list of all identities linked with this user * - * @param string $sql_add Optional WHERE clauses + * @param string $sql_add Optional WHERE clauses + * @param bool $formatted Format identity email and name + * * @return array List of identities */ - function list_identities($sql_add = '') + function list_identities($sql_add = '', $formatted = false) { $result = array(); @@ -255,6 +253,15 @@ class rcube_user $this->ID); while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + if ($formatted) { + $ascii_email = format_email($sql_arr['email']); + $utf8_email = format_email(rcube_utils::idn_to_utf8($ascii_email)); + + $sql_arr['email_ascii'] = $ascii_email; + $sql_arr['email'] = $utf8_email; + $sql_arr['ident'] = format_email_recipient($ascii_email, $sql_arr['name']); + } + $result[] = $sql_arr; } diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index 500f2c371..1ae782a25 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_utils.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2012, The Roundcube Dev Team | | Copyright (C) 2011-2012, Kolab Systems AG | @@ -20,7 +18,6 @@ +-----------------------------------------------------------------------+ */ - /** * Utility class providing common functions * @@ -159,7 +156,7 @@ class rcube_utils { // IPv6, but there's no build-in IPv6 support if (strpos($ip, ':') !== false && !defined('AF_INET6')) { - $parts = explode(':', $domain_part); + $parts = explode(':', $ip); $count = count($parts); if ($count > 8 || $count < 2) { diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php index 45ee601e5..54bb9521d 100644 --- a/program/lib/Roundcube/rcube_vcard.php +++ b/program/lib/Roundcube/rcube_vcard.php @@ -2,8 +2,6 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_vcard.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2012, The Roundcube Dev Team | | | @@ -19,7 +17,6 @@ +-----------------------------------------------------------------------+ */ - /** * Logical representation of a vcard-based address record * Provides functions to parse and export vCard data format @@ -29,765 +26,826 @@ */ class rcube_vcard { - private static $values_decoded = false; - private $raw = array( - 'FN' => array(), - 'N' => array(array('','','','','')), - ); - private static $fieldmap = array( - 'phone' => 'TEL', - 'birthday' => 'BDAY', - 'website' => 'URL', - 'notes' => 'NOTE', - 'email' => 'EMAIL', - 'address' => 'ADR', - 'jobtitle' => 'TITLE', - 'department' => 'X-DEPARTMENT', - 'gender' => 'X-GENDER', - 'maidenname' => 'X-MAIDENNAME', - 'anniversary' => 'X-ANNIVERSARY', - 'assistant' => 'X-ASSISTANT', - 'manager' => 'X-MANAGER', - 'spouse' => 'X-SPOUSE', - 'edit' => 'X-AB-EDIT', - ); - private $typemap = array('IPHONE' => 'mobile', 'CELL' => 'mobile', 'WORK,FAX' => 'workfax'); - private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'BUSINESSFAX' => 'WORK,FAX', 'MOBILE' => 'CELL'); - private $addresstypemap = array('BUSINESS' => 'WORK'); - private $immap = array('X-JABBER' => 'jabber', 'X-ICQ' => 'icq', 'X-MSN' => 'msn', 'X-AIM' => 'aim', 'X-YAHOO' => 'yahoo', 'X-SKYPE' => 'skype', 'X-SKYPE-USERNAME' => 'skype'); - - public $business = false; - public $displayname; - public $surname; - public $firstname; - public $middlename; - public $nickname; - public $organization; - public $email = array(); - - public static $eol = "\r\n"; - - /** - * Constructor - */ - public function __construct($vcard = null, $charset = RCUBE_CHARSET, $detect = false, $fieldmap = array()) - { - if (!empty($fielmap)) - $this->extend_fieldmap($fieldmap); - - if (!empty($vcard)) - $this->load($vcard, $charset, $detect); - } - - - /** - * Load record from (internal, unfolded) vcard 3.0 format - * - * @param string vCard string to parse - * @param string Charset of string values - * @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required - */ - public function load($vcard, $charset = RCUBE_CHARSET, $detect = false) - { - self::$values_decoded = false; - $this->raw = self::vcard_decode($vcard); - - // resolve charset parameters - if ($charset == null) { - $this->raw = self::charset_convert($this->raw); + private static $values_decoded = false; + private $raw = array( + 'FN' => array(), + 'N' => array(array('','','','','')), + ); + private static $fieldmap = array( + 'phone' => 'TEL', + 'birthday' => 'BDAY', + 'website' => 'URL', + 'notes' => 'NOTE', + 'email' => 'EMAIL', + 'address' => 'ADR', + 'jobtitle' => 'TITLE', + 'department' => 'X-DEPARTMENT', + 'gender' => 'X-GENDER', + 'maidenname' => 'X-MAIDENNAME', + 'anniversary' => 'X-ANNIVERSARY', + 'assistant' => 'X-ASSISTANT', + 'manager' => 'X-MANAGER', + 'spouse' => 'X-SPOUSE', + 'edit' => 'X-AB-EDIT', + ); + private $typemap = array( + 'IPHONE' => 'mobile', + 'CELL' => 'mobile', + 'WORK,FAX' => 'workfax', + ); + private $phonetypemap = array( + 'HOME1' => 'HOME', + 'BUSINESS1' => 'WORK', + 'BUSINESS2' => 'WORK2', + 'BUSINESSFAX' => 'WORK,FAX', + 'MOBILE' => 'CELL', + ); + private $addresstypemap = array( + 'BUSINESS' => 'WORK', + ); + private $immap = array( + 'X-JABBER' => 'jabber', + 'X-ICQ' => 'icq', + 'X-MSN' => 'msn', + 'X-AIM' => 'aim', + 'X-YAHOO' => 'yahoo', + 'X-SKYPE' => 'skype', + 'X-SKYPE-USERNAME' => 'skype', + ); + + public $business = false; + public $displayname; + public $surname; + public $firstname; + public $middlename; + public $nickname; + public $organization; + public $email = array(); + + public static $eol = "\r\n"; + + + /** + * Constructor + */ + public function __construct($vcard = null, $charset = RCUBE_CHARSET, $detect = false, $fieldmap = array()) + { + if (!empty($fielmap)) { + $this->extend_fieldmap($fieldmap); + } + + if (!empty($vcard)) { + $this->load($vcard, $charset, $detect); + } } - // vcard has encoded values and charset should be detected - else if ($detect && self::$values_decoded && - ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw))) && $detected_charset != RCUBE_CHARSET) { - $this->raw = self::charset_convert($this->raw, $detected_charset); + + /** + * Load record from (internal, unfolded) vcard 3.0 format + * + * @param string vCard string to parse + * @param string Charset of string values + * @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required + */ + public function load($vcard, $charset = RCUBE_CHARSET, $detect = false) + { + self::$values_decoded = false; + $this->raw = self::vcard_decode($vcard); + + // resolve charset parameters + if ($charset == null) { + $this->raw = self::charset_convert($this->raw); + } + // vcard has encoded values and charset should be detected + else if ($detect && self::$values_decoded + && ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw))) + && $detected_charset != RCUBE_CHARSET + ) { + $this->raw = self::charset_convert($this->raw, $detected_charset); + } + + // consider FN empty if the same as the primary e-mail address + if ($this->raw['FN'][0][0] == $this->raw['EMAIL'][0][0]) { + $this->raw['FN'][0][0] = ''; + } + + // find well-known address fields + $this->displayname = $this->raw['FN'][0][0]; + $this->surname = $this->raw['N'][0][0]; + $this->firstname = $this->raw['N'][0][1]; + $this->middlename = $this->raw['N'][0][2]; + $this->nickname = $this->raw['NICKNAME'][0][0]; + $this->organization = $this->raw['ORG'][0][0]; + $this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization)); + + foreach ((array)$this->raw['EMAIL'] as $i => $raw_email) { + $this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email; + } + + // make the pref e-mail address the first entry in $this->email + $pref_index = $this->get_type_index('EMAIL', 'pref'); + if ($pref_index > 0) { + $tmp = $this->email[0]; + $this->email[0] = $this->email[$pref_index]; + $this->email[$pref_index] = $tmp; + } } - // consider FN empty if the same as the primary e-mail address - if ($this->raw['FN'][0][0] == $this->raw['EMAIL'][0][0]) - $this->raw['FN'][0][0] = ''; - - // find well-known address fields - $this->displayname = $this->raw['FN'][0][0]; - $this->surname = $this->raw['N'][0][0]; - $this->firstname = $this->raw['N'][0][1]; - $this->middlename = $this->raw['N'][0][2]; - $this->nickname = $this->raw['NICKNAME'][0][0]; - $this->organization = $this->raw['ORG'][0][0]; - $this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization)); - - foreach ((array)$this->raw['EMAIL'] as $i => $raw_email) - $this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email; - - // make the pref e-mail address the first entry in $this->email - $pref_index = $this->get_type_index('EMAIL', 'pref'); - if ($pref_index > 0) { - $tmp = $this->email[0]; - $this->email[0] = $this->email[$pref_index]; - $this->email[$pref_index] = $tmp; + /** + * Return vCard data as associative array to be unsed in Roundcube address books + * + * @return array Hash array with key-value pairs + */ + public function get_assoc() + { + $out = array('name' => $this->displayname); + $typemap = $this->typemap; + + // copy name fields to output array + foreach (array('firstname','surname','middlename','nickname','organization') as $col) { + if (strlen($this->$col)) { + $out[$col] = $this->$col; + } + } + + if ($this->raw['N'][0][3]) + $out['prefix'] = $this->raw['N'][0][3]; + if ($this->raw['N'][0][4]) + $out['suffix'] = $this->raw['N'][0][4]; + + // convert from raw vcard data into associative data for Roundcube + foreach (array_flip(self::$fieldmap) as $tag => $col) { + foreach ((array)$this->raw[$tag] as $i => $raw) { + if (is_array($raw)) { + $k = -1; + $key = $col; + $subtype = ''; + + if (!empty($raw['type'])) { + $combined = join(',', self::array_filter((array)$raw['type'], 'internet,pref', true)); + $combined = strtoupper($combined); + + if ($typemap[$combined]) { + $subtype = $typemap[$combined]; + } + else if ($typemap[$raw['type'][++$k]]) { + $subtype = $typemap[$raw['type'][$k]]; + } + else { + $subtype = strtolower($raw['type'][$k]); + } + + while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref')) { + $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]); + } + } + + // read vcard 2.1 subtype + if (!$subtype) { + foreach ($raw as $k => $v) { + if (!is_numeric($k) && $v === true && ($k = strtolower($k)) + && !in_array($k, array('pref','internet','voice','base64')) + ) { + $k_uc = strtoupper($k); + $subtype = $typemap[$k_uc] ? $typemap[$k_uc] : $k; + break; + } + } + } + + // force subtype if none set + if (!$subtype && preg_match('/^(email|phone|address|website)/', $key)) { + $subtype = 'other'; + } + + if ($subtype) { + $key .= ':' . $subtype; + } + + // split ADR values into assoc array + if ($tag == 'ADR') { + list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw; + $out[$key][] = $value; + } + else { + $out[$key][] = $raw[0]; + } + } + else { + $out[$col][] = $raw; + } + } + } + + // handle special IM fields as used by Apple + foreach ($this->immap as $tag => $type) { + foreach ((array)$this->raw[$tag] as $i => $raw) { + $out['im:'.$type][] = $raw[0]; + } + } + + // copy photo data + if ($this->raw['PHOTO']) { + $out['photo'] = $this->raw['PHOTO'][0][0]; + } + + return $out; } - } - - - /** - * Return vCard data as associative array to be unsed in Roundcube address books - * - * @return array Hash array with key-value pairs - */ - public function get_assoc() - { - $out = array('name' => $this->displayname); - $typemap = $this->typemap; - - // copy name fields to output array - foreach (array('firstname','surname','middlename','nickname','organization') as $col) { - if (strlen($this->$col)) - $out[$col] = $this->$col; + + /** + * Convert the data structure into a vcard 3.0 string + */ + public function export($folded = true) + { + $vcard = self::vcard_encode($this->raw); + return $folded ? self::rfc2425_fold($vcard) : $vcard; } - if ($this->raw['N'][0][3]) - $out['prefix'] = $this->raw['N'][0][3]; - if ($this->raw['N'][0][4]) - $out['suffix'] = $this->raw['N'][0][4]; - - // convert from raw vcard data into associative data for Roundcube - foreach (array_flip(self::$fieldmap) as $tag => $col) { - foreach ((array)$this->raw[$tag] as $i => $raw) { - if (is_array($raw)) { - $k = -1; - $key = $col; - $subtype = ''; - - if (!empty($raw['type'])) { - $combined = join(',', self::array_filter((array)$raw['type'], 'internet,pref', true)); - $combined = strtoupper($combined); - - if ($typemap[$combined]) { - $subtype = $typemap[$combined]; - } - else if ($typemap[$raw['type'][++$k]]) { - $subtype = $typemap[$raw['type'][$k]]; + /** + * Clear the given fields in the loaded vcard data + * + * @param array List of field names to be reset + */ + public function reset($fields = null) + { + if (!$fields) { + $fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap), + array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY')); + } + + foreach ($fields as $f) { + unset($this->raw[$f]); + } + + if (!$this->raw['N']) { + $this->raw['N'] = array(array('','','','','')); + } + if (!$this->raw['FN']) { + $this->raw['FN'] = array(); + } + + $this->email = array(); + } + + /** + * Setter for address record fields + * + * @param string Field name + * @param string Field value + * @param string Type/section name + */ + public function set($field, $value, $type = 'HOME') + { + $field = strtolower($field); + $type_uc = strtoupper($type); + + switch ($field) { + case 'name': + case 'displayname': + $this->raw['FN'][0][0] = $this->displayname = $value; + break; + + case 'surname': + $this->raw['N'][0][0] = $this->surname = $value; + break; + + case 'firstname': + $this->raw['N'][0][1] = $this->firstname = $value; + break; + + case 'middlename': + $this->raw['N'][0][2] = $this->middlename = $value; + break; + + case 'prefix': + $this->raw['N'][0][3] = $value; + break; + + case 'suffix': + $this->raw['N'][0][4] = $value; + break; + + case 'nickname': + $this->raw['NICKNAME'][0][0] = $this->nickname = $value; + break; + + case 'organization': + $this->raw['ORG'][0][0] = $this->organization = $value; + break; + + case 'photo': + if (strpos($value, 'http:') === 0) { + // TODO: fetch file from URL and save it locally? + $this->raw['PHOTO'][0] = array(0 => $value, 'url' => true); } else { - $subtype = strtolower($raw['type'][$k]); + $this->raw['PHOTO'][0] = array(0 => $value, 'base64' => (bool) preg_match('![^a-z0-9/=+-]!i', $value)); } + break; + + case 'email': + $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc))); + $this->email[] = $value; + break; + + case 'im': + // save IM subtypes into extension fields + $typemap = array_flip($this->immap); + if ($field = $typemap[strtolower($type)]) { + $this->raw[$field][] = array(0 => $value); + } + break; - while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref')) - $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]); - } - - // read vcard 2.1 subtype - if (!$subtype) { - foreach ($raw as $k => $v) { - if (!is_numeric($k) && $v === true && ($k = strtolower($k)) - && !in_array($k, array('pref','internet','voice','base64')) - ) { - $k_uc = strtoupper($k); - $subtype = $typemap[$k_uc] ? $typemap[$k_uc] : $k; - break; - } + case 'birthday': + case 'anniversary': + if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) { + $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date')); } - } + break; - // force subtype if none set - if (!$subtype && preg_match('/^(email|phone|address|website)/', $key)) - $subtype = 'other'; + case 'address': + if ($this->addresstypemap[$type_uc]) { + $type = $this->addresstypemap[$type_uc]; + } - if ($subtype) - $key .= ':' . $subtype; + $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']); - // split ADR values into assoc array - if ($tag == 'ADR') { - list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw; - $out[$key][] = $value; - } - else - $out[$key][] = $raw[0]; - } - else { - $out[$col][] = $raw; + // fall through if not empty + if (!strlen(join('', $value))) { + break; + } + + default: + if ($field == 'phone' && $this->phonetypemap[$type_uc]) { + $type = $this->phonetypemap[$type_uc]; + } + + if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) { + $index = count($this->raw[$tag]); + $this->raw[$tag][$index] = (array)$value; + if ($type) { + $typemap = array_flip($this->typemap); + $this->raw[$tag][$index]['type'] = explode(',', ($typemap[$type_uc] ? $typemap[$type_uc] : $type)); + } + } + break; } - } } - // handle special IM fields as used by Apple - foreach ($this->immap as $tag => $type) { - foreach ((array)$this->raw[$tag] as $i => $raw) { - $out['im:'.$type][] = $raw[0]; - } + /** + * Setter for individual vcard properties + * + * @param string VCard tag name + * @param array Value-set of this vcard property + * @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set + */ + public function set_raw($tag, $value, $append = false) + { + $index = $append ? count($this->raw[$tag]) : 0; + $this->raw[$tag][$index] = (array)$value; } - // copy photo data - if ($this->raw['PHOTO']) - $out['photo'] = $this->raw['PHOTO'][0][0]; - - return $out; - } - - - /** - * Convert the data structure into a vcard 3.0 string - */ - public function export($folded = true) - { - $vcard = self::vcard_encode($this->raw); - return $folded ? self::rfc2425_fold($vcard) : $vcard; - } - - - /** - * Clear the given fields in the loaded vcard data - * - * @param array List of field names to be reset - */ - public function reset($fields = null) - { - if (!$fields) - $fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY')); - - foreach ($fields as $f) - unset($this->raw[$f]); - - if (!$this->raw['N']) - $this->raw['N'] = array(array('','','','','')); - if (!$this->raw['FN']) - $this->raw['FN'] = array(); - - $this->email = array(); - } - - - /** - * Setter for address record fields - * - * @param string Field name - * @param string Field value - * @param string Type/section name - */ - public function set($field, $value, $type = 'HOME') - { - $field = strtolower($field); - $type_uc = strtoupper($type); - - switch ($field) { - case 'name': - case 'displayname': - $this->raw['FN'][0][0] = $this->displayname = $value; - break; - - case 'surname': - $this->raw['N'][0][0] = $this->surname = $value; - break; - - case 'firstname': - $this->raw['N'][0][1] = $this->firstname = $value; - break; - - case 'middlename': - $this->raw['N'][0][2] = $this->middlename = $value; - break; - - case 'prefix': - $this->raw['N'][0][3] = $value; - break; - - case 'suffix': - $this->raw['N'][0][4] = $value; - break; - - case 'nickname': - $this->raw['NICKNAME'][0][0] = $this->nickname = $value; - break; - - case 'organization': - $this->raw['ORG'][0][0] = $this->organization = $value; - break; - - case 'photo': - if (strpos($value, 'http:') === 0) { - // TODO: fetch file from URL and save it locally? - $this->raw['PHOTO'][0] = array(0 => $value, 'url' => true); - } - else { - $this->raw['PHOTO'][0] = array(0 => $value, 'base64' => (bool) preg_match('![^a-z0-9/=+-]!i', $value)); - } - break; - - case 'email': - $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc))); - $this->email[] = $value; - break; - - case 'im': - // save IM subtypes into extension fields - $typemap = array_flip($this->immap); - if ($field = $typemap[strtolower($type)]) - $this->raw[$field][] = array(0 => $value); - break; - - case 'birthday': - case 'anniversary': - if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) - $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date')); - break; - - case 'address': - if ($this->addresstypemap[$type_uc]) - $type = $this->addresstypemap[$type_uc]; - - $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']); - - // fall through if not empty - if (!strlen(join('', $value))) - break; - - default: - if ($field == 'phone' && $this->phonetypemap[$type_uc]) - $type = $this->phonetypemap[$type_uc]; - - if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) { - $index = count($this->raw[$tag]); - $this->raw[$tag][$index] = (array)$value; - if ($type) { - $typemap = array_flip($this->typemap); - $this->raw[$tag][$index]['type'] = explode(',', ($typemap[$type_uc] ? $typemap[$type_uc] : $type)); - } - } - break; - } - } - - /** - * Setter for individual vcard properties - * - * @param string VCard tag name - * @param array Value-set of this vcard property - * @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set - */ - public function set_raw($tag, $value, $append = false) - { - $index = $append ? count($this->raw[$tag]) : 0; - $this->raw[$tag][$index] = (array)$value; - } - - - /** - * Find index with the '$type' attribute - * - * @param string Field name - * @return int Field index having $type set - */ - private function get_type_index($field, $type = 'pref') - { - $result = 0; - if ($this->raw[$field]) { - foreach ($this->raw[$field] as $i => $data) { - if (is_array($data['type']) && in_array_nocase('pref', $data['type'])) - $result = $i; - } + /** + * Find index with the '$type' attribute + * + * @param string Field name + * @return int Field index having $type set + */ + private function get_type_index($field, $type = 'pref') + { + $result = 0; + if ($this->raw[$field]) { + foreach ($this->raw[$field] as $i => $data) { + if (is_array($data['type']) && in_array_nocase('pref', $data['type'])) { + $result = $i; + } + } + } + + return $result; } - return $result; - } - - - /** - * Convert a whole vcard (array) to UTF-8. - * If $force_charset is null, each member value that has a charset parameter will be converted - */ - private static function charset_convert($card, $force_charset = null) - { - foreach ($card as $key => $node) { - foreach ($node as $i => $subnode) { - if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) { - foreach ($subnode as $j => $value) { - if (is_numeric($j) && is_string($value)) - $card[$key][$i][$j] = rcube_charset::convert($value, $charset); - } - unset($card[$key][$i]['charset']); - } - } + /** + * Convert a whole vcard (array) to UTF-8. + * If $force_charset is null, each member value that has a charset parameter will be converted + */ + private static function charset_convert($card, $force_charset = null) + { + foreach ($card as $key => $node) { + foreach ($node as $i => $subnode) { + if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) { + foreach ($subnode as $j => $value) { + if (is_numeric($j) && is_string($value)) { + $card[$key][$i][$j] = rcube_charset::convert($value, $charset); + } + } + unset($card[$key][$i]['charset']); + } + } + } + + return $card; } - return $card; - } - - - /** - * Extends fieldmap definition - */ - public function extend_fieldmap($map) - { - if (is_array($map)) - self::$fieldmap = array_merge($map, self::$fieldmap); - } - - - /** - * Factory method to import a vcard file - * - * @param string vCard file content - * @return array List of rcube_vcard objects - */ - public static function import($data) - { - $out = array(); - - // check if charsets are specified (usually vcard version < 3.0 but this is not reliable) - if (preg_match('/charset=/i', substr($data, 0, 2048))) - $charset = null; - // detect charset and convert to utf-8 - else if (($charset = self::detect_encoding($data)) && $charset != RCUBE_CHARSET) { - $data = rcube_charset::convert($data, $charset); - $data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM - $charset = RCUBE_CHARSET; + /** + * Extends fieldmap definition + */ + public function extend_fieldmap($map) + { + if (is_array($map)) { + self::$fieldmap = array_merge($map, self::$fieldmap); + } } - $vcard_block = ''; - $in_vcard_block = false; + /** + * Factory method to import a vcard file + * + * @param string vCard file content + * + * @return array List of rcube_vcard objects + */ + public static function import($data) + { + $out = array(); + + // check if charsets are specified (usually vcard version < 3.0 but this is not reliable) + if (preg_match('/charset=/i', substr($data, 0, 2048))) { + $charset = null; + } + // detect charset and convert to utf-8 + else if (($charset = self::detect_encoding($data)) && $charset != RCUBE_CHARSET) { + $data = rcube_charset::convert($data, $charset); + $data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM + $charset = RCUBE_CHARSET; + } - foreach (preg_split("/[\r\n]+/", $data) as $i => $line) { - if ($in_vcard_block && !empty($line)) - $vcard_block .= $line . "\n"; + $vcard_block = ''; + $in_vcard_block = false; - $line = trim($line); + foreach (preg_split("/[\r\n]+/", $data) as $i => $line) { + if ($in_vcard_block && !empty($line)) { + $vcard_block .= $line . "\n"; + } - if (preg_match('/^END:VCARD$/i', $line)) { - // parse vcard - $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap); - if (!empty($obj->displayname) || !empty($obj->email)) - $out[] = $obj; + $line = trim($line); - $in_vcard_block = false; - } - else if (preg_match('/^BEGIN:VCARD$/i', $line)) { - $vcard_block = $line . "\n"; - $in_vcard_block = true; - } + if (preg_match('/^END:VCARD$/i', $line)) { + // parse vcard + $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap); + // FN and N is required by vCard format (RFC 2426) + // on import we can be less restrictive, let's addressbook decide + if (!empty($obj->displayname) || !empty($obj->surname) || !empty($obj->firstname) || !empty($obj->email)) { + $out[] = $obj; + } + + $in_vcard_block = false; + } + else if (preg_match('/^BEGIN:VCARD$/i', $line)) { + $vcard_block = $line . "\n"; + $in_vcard_block = true; + } + } + + return $out; } - return $out; - } - - - /** - * Normalize vcard data for better parsing - * - * @param string vCard block - * @return string Cleaned vcard block - */ - private static function cleanup($vcard) - { - // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;) - $vcard = preg_replace( - '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', - '\2;type=\5\3:\4', - $vcard); - - // convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility - $vcard = preg_replace_callback( - '/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', - array('self', 'x_abrelatednames_callback'), - $vcard); - - // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines - $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard); - - // convert X-WAB-GENDER to X-GENDER - if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) { - $value = $matches[1] == '2' ? 'male' : 'female'; - $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard); + /** + * Normalize vcard data for better parsing + * + * @param string vCard block + * + * @return string Cleaned vcard block + */ + public static function cleanup($vcard) + { + // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;) + $vcard = preg_replace( + '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', + '\2;type=\5\3:\4', + $vcard); + + // convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility + $vcard = preg_replace_callback( + '/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', + array('self', 'x_abrelatednames_callback'), + $vcard); + + // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines + $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard); + + // convert X-WAB-GENDER to X-GENDER + if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) { + $value = $matches[1] == '2' ? 'male' : 'female'; + $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard); + } + + // if N doesn't have any semicolons, add some + $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard); + + return $vcard; } - // if N doesn't have any semicolons, add some - $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard); - - return $vcard; - } - - private static function x_abrelatednames_callback($matches) - { - return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4]; - } - - private static function rfc2425_fold_callback($matches) - { - // chunk_split string and avoid lines breaking multibyte characters - $c = 71; - $out .= substr($matches[1], 0, $c); - for ($n = $c; $c < strlen($matches[1]); $c++) { - // break if length > 75 or mutlibyte character starts after position 71 - if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) { - $out .= "\r\n "; - $n = 0; - } - $out .= $matches[1][$c]; - $n++; + private static function x_abrelatednames_callback($matches) + { + return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4]; } - return $out; - } - - public static function rfc2425_fold($val) - { - return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val); - } - - - /** - * Decodes a vcard block (vcard 3.0 format, unfolded) - * into an array structure - * - * @param string vCard block to parse - * @return array Raw data structure - */ - private static function vcard_decode($vcard) - { - // Perform RFC2425 line unfolding and split lines - $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard); - $lines = explode("\n", $vcard); - $data = array(); - - for ($i=0; $i < count($lines); $i++) { - if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line)) - continue; - - if (preg_match('/^(BEGIN|END)$/i', $line[1])) - continue; - - // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:" - if (($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) { - $line[1] = $regs2[1]; - foreach (explode(';', $regs2[2]) as $prop) - $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop); - } - - if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) { - $entry = array(); - $field = strtoupper($regs2[1][0]); - $enc = null; - - foreach($regs2[1] as $attrid => $attr) { - if ((list($key, $value) = explode('=', $attr)) && $value) { - $value = trim($value); - if ($key == 'ENCODING') { - $value = strtoupper($value); - // add next line(s) to value string if QP line end detected - if ($value == 'QUOTED-PRINTABLE') { - while (preg_match('/=$/', $lines[$i])) - $line[2] .= "\n" . $lines[++$i]; - } - $enc = $value; - } - else { - $lc_key = strtolower($key); - $entry[$lc_key] = array_merge((array)$entry[$lc_key], (array)self::vcard_unquote($value, ',')); + private static function rfc2425_fold_callback($matches) + { + // chunk_split string and avoid lines breaking multibyte characters + $c = 71; + $out .= substr($matches[1], 0, $c); + for ($n = $c; $c < strlen($matches[1]); $c++) { + // break if length > 75 or mutlibyte character starts after position 71 + if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) { + $out .= "\r\n "; + $n = 0; } - } - else if ($attrid > 0) { - $entry[strtolower($key)] = true; // true means attr without =value - } + $out .= $matches[1][$c]; + $n++; } - // decode value - if ($enc || !empty($entry['base64'])) { - // save encoding type (#1488432) - if ($enc == 'B') { - $entry['encoding'] = 'B'; - // should we use vCard 3.0 instead? - // $entry['base64'] = true; - } - $line[2] = self::decode_value($line[2], $enc ? $enc : 'base64'); - } + return $out; + } + + public static function rfc2425_fold($val) + { + return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val); + } - if ($enc != 'B' && empty($entry['base64'])) { - $line[2] = self::vcard_unquote($line[2]); + /** + * Decodes a vcard block (vcard 3.0 format, unfolded) + * into an array structure + * + * @param string vCard block to parse + * + * @return array Raw data structure + */ + private static function vcard_decode($vcard) + { + // Perform RFC2425 line unfolding and split lines + $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard); + $lines = explode("\n", $vcard); + $data = array(); + + for ($i=0; $i < count($lines); $i++) { + if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line)) + continue; + + if (preg_match('/^(BEGIN|END)$/i', $line[1])) + continue; + + // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:" + if ($data['VERSION'][0] == "2.1" + && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) + && !preg_match('/^TYPE=/i', $regs2[2]) + ) { + $line[1] = $regs2[1]; + foreach (explode(';', $regs2[2]) as $prop) { + $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop); + } + } + + if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) { + $entry = array(); + $field = strtoupper($regs2[1][0]); + $enc = null; + + foreach($regs2[1] as $attrid => $attr) { + if ((list($key, $value) = explode('=', $attr)) && $value) { + $value = trim($value); + if ($key == 'ENCODING') { + $value = strtoupper($value); + // add next line(s) to value string if QP line end detected + if ($value == 'QUOTED-PRINTABLE') { + while (preg_match('/=$/', $lines[$i])) { + $line[2] .= "\n" . $lines[++$i]; + } + } + $enc = $value; + } + else { + $lc_key = strtolower($key); + $entry[$lc_key] = array_merge((array)$entry[$lc_key], (array)self::vcard_unquote($value, ',')); + } + } + else if ($attrid > 0) { + $entry[strtolower($key)] = true; // true means attr without =value + } + } + + // decode value + if ($enc || !empty($entry['base64'])) { + // save encoding type (#1488432) + if ($enc == 'B') { + $entry['encoding'] = 'B'; + // should we use vCard 3.0 instead? + // $entry['base64'] = true; + } + $line[2] = self::decode_value($line[2], $enc ? $enc : 'base64'); + } + + if ($enc != 'B' && empty($entry['base64'])) { + $line[2] = self::vcard_unquote($line[2]); + } + + $entry = array_merge($entry, (array) $line[2]); + $data[$field][] = $entry; + } } - $entry = array_merge($entry, (array) $line[2]); - $data[$field][] = $entry; - } + unset($data['VERSION']); + return $data; } - unset($data['VERSION']); - return $data; - } - - - /** - * Decode a given string with the encoding rule from ENCODING attributes - * - * @param string String to decode - * @param string Encoding type (quoted-printable and base64 supported) - * @return string Decoded 8bit value - */ - private static function decode_value($value, $encoding) - { - switch (strtolower($encoding)) { - case 'quoted-printable': - self::$values_decoded = true; - return quoted_printable_decode($value); - - case 'base64': - case 'b': - self::$values_decoded = true; - return base64_decode($value); - - default: - return $value; + /** + * Decode a given string with the encoding rule from ENCODING attributes + * + * @param string String to decode + * @param string Encoding type (quoted-printable and base64 supported) + * + * @return string Decoded 8bit value + */ + private static function decode_value($value, $encoding) + { + switch (strtolower($encoding)) { + case 'quoted-printable': + self::$values_decoded = true; + return quoted_printable_decode($value); + + case 'base64': + case 'b': + self::$values_decoded = true; + return base64_decode($value); + + default: + return $value; + } } - } - - - /** - * Encodes an entry for storage in our database (vcard 3.0 format, unfolded) - * - * @param array Raw data structure to encode - * @return string vCard encoded string - */ - static function vcard_encode($data) - { - foreach((array)$data as $type => $entries) { - /* valid N has 5 properties */ - while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5) - $entries[0][] = ""; - - // make sure FN is not empty (required by RFC2426) - if ($type == "FN" && empty($entries)) - $entries[0] = $data['EMAIL'][0][0]; - - foreach((array)$entries as $entry) { - $attr = ''; - if (is_array($entry)) { - $value = array(); - foreach($entry as $attrname => $attrvalues) { - if (is_int($attrname)) { - if (!empty($entry['base64']) || $entry['encoding'] == 'B') { - $attrvalues = base64_encode($attrvalues); - } - $value[] = $attrvalues; + + /** + * Encodes an entry for storage in our database (vcard 3.0 format, unfolded) + * + * @param array Raw data structure to encode + * + * @return string vCard encoded string + */ + static function vcard_encode($data) + { + foreach ((array)$data as $type => $entries) { + // valid N has 5 properties + while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5) { + $entries[0][] = ""; } - else if (is_bool($attrvalues)) { - if ($attrvalues) { - $attr .= strtoupper(";$attrname"); // true means just tag, not tag=value, as in PHOTO;BASE64:... - } + + // make sure FN is not empty (required by RFC2426) + if ($type == "FN" && empty($entries)) { + $entries[0] = $data['EMAIL'][0][0]; } - else { - foreach((array)$attrvalues as $attrvalue) - $attr .= strtoupper(";$attrname=") . self::vcard_quote($attrvalue, ','); + + foreach ((array)$entries as $entry) { + $attr = ''; + if (is_array($entry)) { + $value = array(); + foreach ($entry as $attrname => $attrvalues) { + if (is_int($attrname)) { + if (!empty($entry['base64']) || $entry['encoding'] == 'B') { + $attrvalues = base64_encode($attrvalues); + } + $value[] = $attrvalues; + } + else if (is_bool($attrvalues)) { + // true means just tag, not tag=value, as in PHOTO;BASE64:... + if ($attrvalues) { + $attr .= strtoupper(";$attrname"); + } + } + else { + foreach((array)$attrvalues as $attrvalue) { + $attr .= strtoupper(";$attrname=") . self::vcard_quote($attrvalue, ','); + } + } + } + } + else { + $value = $entry; + } + + // skip empty entries + if (self::is_empty($value)) { + continue; + } + + $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol; } - } } - else { - $value = $entry; - } - - // skip empty entries - if (self::is_empty($value)) - continue; - $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol; - } + return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD'; } - return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD'; - } - - - /** - * Join indexed data array to a vcard quoted string - * - * @param array Field data - * @param string Separator - * @return string Joined and quoted string - */ - private static function vcard_quote($s, $sep = ';') - { - if (is_array($s)) { - foreach($s as $part) { - $r[] = self::vcard_quote($part, $sep); - } - return(implode($sep, (array)$r)); - } - else { - return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;')); - } - } - - - /** - * Split quoted string - * - * @param string vCard string to split - * @param string Separator char/string - * @return array List with splited values - */ - private static function vcard_unquote($s, $sep = ';') - { - // break string into parts separated by $sep, but leave escaped $sep alone - if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) { - foreach($parts as $s) { - $result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep); - } - return $result; + /** + * Join indexed data array to a vcard quoted string + * + * @param array Field data + * @param string Separator + * + * @return string Joined and quoted string + */ + private static function vcard_quote($s, $sep = ';') + { + if (is_array($s)) { + foreach($s as $part) { + $r[] = self::vcard_quote($part, $sep); + } + return(implode($sep, (array)$r)); + } + + return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;')); } - else { - return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';', '\:' => ':')); + + /** + * Split quoted string + * + * @param string vCard string to split + * @param string Separator char/string + * + * @return array List with splited values + */ + private static function vcard_unquote($s, $sep = ';') + { + // break string into parts separated by $sep + if (!empty($sep)) { + // Handle properly backslash escaping (#1488896) + $rep1 = array("\\\\" => "\010", "\\$sep" => "\007"); + $rep2 = array("\007" => "\\$sep", "\010" => "\\\\"); + + if (count($parts = explode($sep, strtr($s, $rep1))) > 1) { + foreach ($parts as $s) { + $result[] = self::vcard_unquote(strtr($s, $rep2)); + } + return $result; + } + } + + return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';')); } - } - - - /** - * Check if vCard entry is empty: empty string or an array with - * all entries empty. - * - * @param mixed $value Attribute value (string or array) - * - * @return bool True if the value is empty, False otherwise - */ - private static function is_empty($value) - { - foreach ((array)$value as $v) { - if (((string)$v) !== '') { - return false; - } + + /** + * Check if vCard entry is empty: empty string or an array with + * all entries empty. + * + * @param mixed $value Attribute value (string or array) + * + * @return bool True if the value is empty, False otherwise + */ + private static function is_empty($value) + { + foreach ((array)$value as $v) { + if (((string)$v) !== '') { + return false; + } + } + + return true; } - return true; - } - - /** - * Extract array values by a filter - * - * @param array Array to filter - * @param keys Array or comma separated list of values to keep - * @param boolean Invert key selection: remove the listed values - * @return array The filtered array - */ - private static function array_filter($arr, $values, $inverse = false) - { - if (!is_array($values)) - $values = explode(',', $values); - - $result = array(); - $keep = array_flip((array)$values); - foreach ($arr as $key => $val) - if ($inverse != isset($keep[strtolower($val)])) - $result[$key] = $val; - - return $result; - } - - /** - * Returns UNICODE type based on BOM (Byte Order Mark) - * - * @param string Input string to test - * @return string Detected encoding - */ - private static function detect_encoding($string) - { - $fallback = rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); // fallback to Latin-1 - - return rcube_charset::detect($string, $fallback); - } + /** + * Extract array values by a filter + * + * @param array Array to filter + * @param keys Array or comma separated list of values to keep + * @param boolean Invert key selection: remove the listed values + * + * @return array The filtered array + */ + private static function array_filter($arr, $values, $inverse = false) + { + if (!is_array($values)) { + $values = explode(',', $values); + } + + $result = array(); + $keep = array_flip((array)$values); + foreach ($arr as $key => $val) { + if ($inverse != isset($keep[strtolower($val)])) { + $result[$key] = $val; + } + } + + return $result; + } + + /** + * Returns UNICODE type based on BOM (Byte Order Mark) + * + * @param string Input string to test + * + * @return string Detected encoding + */ + private static function detect_encoding($string) + { + $fallback = rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); // fallback to Latin-1 + + return rcube_charset::detect($string, $fallback); + } } diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php new file mode 100644 index 000000000..27dff9f48 --- /dev/null +++ b/program/lib/Roundcube/rcube_washtml.php @@ -0,0 +1,453 @@ +<?php + +/** + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2008-2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Utility class providing HTML sanityzer (based on Washtml class) | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + | Author: Frederic Motte <fmotte@ubixis.com> | + +-----------------------------------------------------------------------+ + */ + +/** + * Washtml, a HTML sanityzer. + * + * Copyright (c) 2007 Frederic Motte <fmotte@ubixis.com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * OVERVIEW: + * + * Wahstml take an untrusted HTML and return a safe html string. + * + * SYNOPSIS: + * + * $washer = new washtml($config); + * $washer->wash($html); + * 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['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. + * $config['charset'] is a string containing the charset of the HTML document if it is not defined in it. + * $washer->extlinks is a reference to a boolean that is set to true if remote images were removed. (FE: show remote images link) + * + * INTERNALS: + * + * Only tags and attributes in the static lists $html_elements and $html_attributes + * are kept, inline styles are also filtered: all style identifiers matching + * /[a-z\-]/i are allowed. Values matching colors, sizes, /[a-z\-]/i and safe + * urls if allowed and cid urls if mapped are kept. + * + * Roundcube Changes: + * - added $block_elements + * - changed $ignore_elements behaviour + * - added RFC2397 support + * - base URL support + * - invalid HTML comments removal before parsing + * - "fixing" unitless CSS values for XHTML output + * - base url resolving + */ + +/** + * Utility class providing HTML sanityzer + * + * @package Framework + * @subpackage Utils + */ +class rcube_washtml +{ + /* Allowed HTML elements (default) */ + static $html_elements = array('a', 'abbr', 'acronym', 'address', 'area', 'b', + 'basefont', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center', + 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', + 'dt', 'em', 'fieldset', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', + 'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q', + 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table', + 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img', + // form elements + 'button', 'input', 'textarea', 'select', 'option', 'optgroup' + ); + + /* Ignore these HTML tags and their content */ + static $ignore_elements = array('script', 'applet', 'embed', 'object', 'style'); + + /* Allowed HTML attributes */ + static $html_attribs = array('name', 'class', 'title', 'alt', 'width', 'height', + 'align', 'nowrap', 'col', 'row', 'id', 'rowspan', 'colspan', 'cellspacing', + 'cellpadding', 'valign', 'bgcolor', 'color', 'border', 'bordercolorlight', + 'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border', + 'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace', + 'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media', + // attributes of form elements + 'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value' + ); + + /* Block elements which could be empty but cannot be returned in short form (<tag />) */ + static $block_elements = array('div', 'p', 'pre', 'blockquote', 'a', 'font', 'center', + 'table', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'dl', 'strong', + 'i', 'b', 'u', 'span', + ); + + /* State for linked objects in HTML */ + public $extlinks = false; + + /* Current settings */ + private $config = array(); + + /* Registered callback functions for tags */ + private $handlers = array(); + + /* Allowed HTML elements */ + private $_html_elements = array(); + + /* Ignore these HTML tags but process their content */ + private $_ignore_elements = array(); + + /* Block elements which could be empty but cannot be returned in short form (<tag />) */ + private $_block_elements = array(); + + /* Allowed HTML attributes */ + private $_html_attribs = array(); + + + /** + * Class constructor + */ + public function __construct($p = array()) + { + $this->_html_elements = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements) ; + $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->_block_elements = array_flip((array)$p['block_elements']) + array_flip(self::$block_elements); + + unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['block_elements']); + + $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array()); + } + + /** + * Register a callback function for a certain tag + */ + public function add_callback($tagName, $callback) + { + $this->handlers[$tagName] = $callback; + } + + /** + * Check CSS style + */ + private function wash_style($style) + { + $s = ''; + + foreach (explode(';', $style) as $declaration) { + if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) { + $cssid = $match[1]; + $str = $match[2]; + $value = ''; + + while (sizeof($str) > 0 && + preg_match('/^(url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)'./*1,2*/ + '|rgb\(\s*[0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*\)'. + '|-?[0-9.]+\s*(em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)?'. + '|#[0-9a-f]{3,6}'. + '|[a-z0-9", -]+'. + ')\s*/i', $str, $match) + ) { + if ($match[2]) { + if (($src = $this->config['cid_map'][$match[2]]) + || ($src = $this->config['cid_map'][$this->config['base_url'].$match[2]]) + ) { + $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')'; + } + else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $match[2], $url)) { + if ($this->config['allow_remote']) { + $value .= ' url('.htmlspecialchars($url[0], ENT_QUOTES).')'; + } + else { + $this->extlinks = true; + } + } + else if (preg_match('/^data:.+/i', $match[2])) { // RFC2397 + $value .= ' url('.htmlspecialchars($match[2], ENT_QUOTES).')'; + } + } + else { + // whitelist ? + $value .= ' ' . $match[0]; + + // #1488535: Fix size units, so width:800 would be changed to width:800px + if (preg_match('/(left|right|top|bottom|width|height)/i', $cssid) + && preg_match('/^[0-9]+$/', $match[0]) + ) { + $value .= 'px'; + } + } + + $str = substr($str, strlen($match[0])); + } + + if (isset($value[0])) { + $s .= ($s?' ':'') . $cssid . ':' . $value . ';'; + } + } + } + + return $s; + } + + /** + * Take a node and return allowed attributes and check values + */ + private function wash_attribs($node) + { + $t = ''; + $washed = ''; + + foreach ($node->attributes as $key => $plop) { + $key = strtolower($key); + $value = $node->getAttribute($key); + + if (isset($this->_html_attribs[$key]) || + ($key == 'href' && ($value = trim($value)) + && !preg_match('!^(javascript|vbscript|data:text)!i', $value) + && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value)) + ) { + $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; + } + else if ($key == 'style' && ($style = $this->wash_style($value))) { + $quot = strpos($style, '"') !== false ? "'" : '"'; + $t .= ' style=' . $quot . $style . $quot; + } + else if ($key == 'background' || ($key == 'src' && strtolower($node->tagName) == 'img')) { //check tagName anyway + if (($src = $this->config['cid_map'][$value]) + || ($src = $this->config['cid_map'][$this->config['base_url'].$value]) + ) { + $t .= ' ' . $key . '="' . htmlspecialchars($src, ENT_QUOTES) . '"'; + } + else if (preg_match('/^(http|https|ftp):.+/i', $value)) { + if ($this->config['allow_remote']) { + $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; + } + else { + $this->extlinks = true; + if ($this->config['blocked_src']) { + $t .= ' ' . $key . '="' . htmlspecialchars($this->config['blocked_src'], ENT_QUOTES) . '"'; + } + } + } + else if (preg_match('/^data:.+/i', $value)) { // RFC2397 + $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; + } + } + else { + $washed .= ($washed ? ' ' : '') . $key; + } + } + + return $t . ($washed && $this->config['show_washed'] ? ' x-washed="'.$washed.'"' : ''); + } + + /** + * The main loop that recurse on a node tree. + * It output only allowed tags with allowed attributes + * and allowed inline styles + */ + private function dumpHtml($node) + { + if (!$node->hasChildNodes()) { + return ''; + } + + $node = $node->firstChild; + $dump = ''; + + do { + switch($node->nodeType) { + case XML_ELEMENT_NODE: //Check element + $tagName = strtolower($node->tagName); + if ($callback = $this->handlers[$tagName]) { + $dump .= call_user_func($callback, $tagName, + $this->wash_attribs($node), $this->dumpHtml($node), $this); + } + else if (isset($this->_html_elements[$tagName])) { + $content = $this->dumpHtml($node); + $dump .= '<' . $tagName . $this->wash_attribs($node) . + ($content != '' || isset($this->_block_elements[$tagName]) ? ">$content</$tagName>" : ' />'); + } + else if (isset($this->_ignore_elements[$tagName])) { + $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' not allowed -->'; + } + else { + $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' ignored -->'; + $dump .= $this->dumpHtml($node); // ignore tags not its content + } + break; + + case XML_CDATA_SECTION_NODE: + $dump .= $node->nodeValue; + break; + + case XML_TEXT_NODE: + $dump .= htmlspecialchars($node->nodeValue); + break; + + case XML_HTML_DOCUMENT_NODE: + $dump .= $this->dumpHtml($node); + break; + + case XML_DOCUMENT_TYPE_NODE: + break; + + default: + $dump . '<!-- node type ' . $node->nodeType . ' -->'; + } + } while($node = $node->nextSibling); + + return $dump; + } + + /** + * Main function, give it untrusted HTML, tell it if you allow loading + * remote images and give it a map to convert "cid:" urls. + */ + public function wash($html) + { + // Charset seems to be ignored (probably if defined in the HTML document) + $node = new DOMDocument('1.0', $this->config['charset']); + $this->extlinks = false; + + $html = $this->cleanup($html); + + // Find base URL for images + if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) { + $this->config['base_url'] = $matches[1]; + } + else { + $this->config['base_url'] = ''; + } + + @$node->loadHTML($html); + return $this->dumpHtml($node); + } + + /** + * Getter for config parameters + */ + public function get_config($prop) + { + return $this->config[$prop]; + } + + /** + * Clean HTML input + */ + private function cleanup($html) + { + // special replacements (not properly handled by washtml class) + $html_search = array( + '/(<\/nobr>)(\s+)(<nobr>)/i', // space(s) between <NOBR> + '/<title[^>]*>[^<]*<\/title>/i', // PHP bug #32547 workaround: remove title tag + '/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/', // byte-order mark (only outlook?) + '/<html\s[^>]+>/i', // washtml/DOMDocument cannot handle xml namespaces + ); + + $html_replace = array( + '\\1'.' '.'\\3', + '', + '', + '<html>', + ); + $html = preg_replace($html_search, $html_replace, trim($html)); + + // PCRE errors handling (#1486856), should we use something like for every preg_* use? + if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) { + $errstr = "Could not clean up HTML message! PCRE Error: $preg_error."; + + if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) { + $errstr .= " Consider raising pcre.backtrack_limit!"; + } + if ($preg_error == PREG_RECURSION_LIMIT_ERROR) { + $errstr .= " Consider raising pcre.recursion_limit!"; + } + + rcube::raise_error(array('code' => 620, 'type' => 'php', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $errstr), true, false); + return ''; + } + + // fix (unknown/malformed) HTML tags before "wash" + $html = preg_replace_callback('/(<[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html); + + // Remove invalid HTML comments (#1487759) + // Don't remove valid conditional comments + // Don't remove MSOutlook (<!-->) conditional comments (#1489004) + $html = preg_replace('/<!--[^->\[\n]+>/', '', $html); + + // turn relative into absolute urls + $html = self::resolve_base($html); + + return $html; + } + + /** + * Callback function for HTML tags fixing + */ + public static function html_tag_callback($matches) + { + $tagname = $matches[2]; + $tagname = preg_replace(array( + '/:.*$/', // Microsoft's Smart Tags <st1:xxxx> + '/[^a-z0-9_\[\]\!-]/i', // forbidden characters + ), '', $tagname); + + return $matches[1] . $tagname; + } + + /** + * Convert all relative URLs according to a <base> in HTML + */ + public static function resolve_base($body) + { + // check for <base href=...> + if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) { + $replacer = new rcube_base_replacer($regs[2]); + $body = $replacer->replace($body); + } + + return $body; + } +} + |