diff options
Diffstat (limited to 'program/lib/Roundcube')
39 files changed, 2624 insertions, 731 deletions
diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php index 8cea48122..68d314270 100644 --- a/program/lib/Roundcube/bootstrap.php +++ b/program/lib/Roundcube/bootstrap.php @@ -31,21 +31,29 @@ $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, + '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" - ."Check your PHP configuration (including php_admin_flag)."); + $error = "ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n" + . "Check your PHP configuration (including php_admin_flag)."; + if (defined('STDERR')) fwrite(STDERR, $error); else echo $error; + exit(1); } } // framework constants -define('RCUBE_VERSION', '0.9-git'); +define('RCUBE_VERSION', '1.0-git'); define('RCUBE_CHARSET', 'UTF-8'); if (!defined('RCUBE_LIB_DIR')) { @@ -284,32 +292,6 @@ function is_ascii($str, $control_chars = true) /** - * Remove single and double quotes from a given string - * - * @param string Input value - * - * @return string Dequoted string - */ -function strip_quotes($str) -{ - return str_replace(array("'", '"'), '', $str); -} - - -/** - * Remove new lines characters from given string - * - * @param string $str Input value - * - * @return string Stripped string - */ -function strip_newlines($str) -{ - return preg_replace('/[\r\n]/', '', $str); -} - - -/** * Compose a valid representation of name and e-mail address * * @param string $email E-mail address diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php index a44f4d518..3e6e47a56 100644 --- a/program/lib/Roundcube/html.php +++ b/program/lib/Roundcube/html.php @@ -21,7 +21,7 @@ * Class for HTML code creation * * @package Framework - * @subpackage HTML + * @subpackage View */ class html { @@ -35,6 +35,7 @@ class html public static $common_attrib = array('id','class','style','title','align'); public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script'); + /** * Constructor * @@ -217,7 +218,7 @@ class html $attr = array('src' => $attr); } return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib, - array('src','name','width','height','border','frameborder'))); + array('src','name','width','height','border','frameborder','onload'))); } /** @@ -332,7 +333,16 @@ class html */ public static function quote($str) { - return @htmlspecialchars($str, ENT_COMPAT, RCUBE_CHARSET); + static $flags; + + if (!$flags) { + $flags = ENT_COMPAT; + if (defined('ENT_SUBSTITUTE')) { + $flags |= ENT_SUBSTITUTE; + } + } + + return @htmlspecialchars($str, $flags, RCUBE_CHARSET); } } @@ -340,7 +350,8 @@ class html /** * Class to create an HTML input field * - * @package HTML + * @package Framework + * @subpackage View */ class html_inputfield extends html { @@ -349,8 +360,8 @@ class html_inputfield extends html protected $allowed = array( 'type','name','value','size','tabindex','autocapitalize', 'autocomplete','checked','onchange','onclick','disabled','readonly', - 'spellcheck','results','maxlength','src','multiple','placeholder', - 'autofocus', + 'spellcheck','results','maxlength','src','multiple','accept', + 'placeholder','autofocus', ); /** @@ -396,7 +407,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 { @@ -406,9 +418,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'; @@ -456,7 +468,8 @@ class html_hiddenfield extends html /** * Class to create HTML radio buttons * - * @package HTML + * @package Framework + * @subpackage View */ class html_radiobutton extends html_inputfield { @@ -486,7 +499,8 @@ class html_radiobutton extends html_inputfield /** * Class to create HTML checkboxes * - * @package HTML + * @package Framework + * @subpackage View */ class html_checkbox extends html_inputfield { @@ -516,7 +530,8 @@ class html_checkbox extends html_inputfield /** * Class to create an HTML textarea * - * @package HTML + * @package Framework + * @subpackage View */ class html_textarea extends html { @@ -574,7 +589,8 @@ class html_textarea extends html * print $select->show('CH'); * </pre> * - * @package HTML + * @package Framework + * @subpackage View */ class html_select extends html { @@ -639,7 +655,8 @@ class html_select extends html /** * Class to build an HTML table * - * @package HTML + * @package Framework + * @subpackage View */ class html_table extends html { @@ -661,6 +678,11 @@ class html_table extends html { $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array(); $this->attrib = array_merge($attrib, $default_attrib); + + if (!empty($attrib['tagname']) && $attrib['tagname'] != 'table') { + $this->tagname = $attrib['tagname']; + $this->allowed = self::$common_attrib; + } } /** @@ -762,6 +784,11 @@ class html_table extends html $index = $this->rowindex; } + // make sure row object exists (#1489094) + if (!$this->rows[$index]) { + $this->rows[$index] = new stdClass; + } + $this->rows[$index]->attrib = $attr; } @@ -799,19 +826,20 @@ class html_table extends html if (!empty($this->header)) { $rowcontent = ''; foreach ($this->header as $c => $col) { - $rowcontent .= self::tag('td', $col->attrib, $col->content); + $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content); } - $thead = self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)); + $thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) : + self::tag($this->_row_tagname(), array('class' => 'thead'), $rowcontent, parent::$common_attrib); } foreach ($this->rows as $r => $row) { $rowcontent = ''; foreach ($row->cells as $c => $col) { - $rowcontent .= self::tag('td', $col->attrib, $col->content); + $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content); } if ($r < $this->rowindex || count($row->cells)) { - $tbody .= self::tag('tr', $row->attrib, $rowcontent, parent::$common_attrib); + $tbody .= self::tag($this->_row_tagname(), $row->attrib, $rowcontent, parent::$common_attrib); } } @@ -820,7 +848,7 @@ class html_table extends html } // add <tbody> - $this->content = $thead . self::tag('tbody', null, $tbody); + $this->content = $thead . ($this->tagname == 'table' ? self::tag('tbody', null, $tbody) : $tbody); unset($this->attrib['cols'], $this->attrib['rowsonly']); return parent::show(); @@ -845,4 +873,22 @@ class html_table extends html $this->rowindex = 0; } + /** + * Getter for the corresponding tag name for table row elements + */ + private function _row_tagname() + { + static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div'); + return $row_tagnames[$this->tagname] ?: $row_tagnames['*']; + } + + /** + * Getter for the corresponding tag name for table cell elements + */ + private function _col_tagname() + { + static $col_tagnames = array('table' => 'td', '*' => 'span'); + return $col_tagnames[$this->tagname] ?: $col_tagnames['*']; + } + } diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index a914ae65a..21b49f49b 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -99,7 +99,6 @@ class rcube protected $texts; protected $caches = array(); protected $shutdown_functions = array(); - protected $expunge_cache = false; /** @@ -258,6 +257,39 @@ class rcube /** + * Initialize and get shared cache object + * + * @param string $name Cache identifier + * @param bool $packed Enables/disables data serialization + * + * @return rcube_cache_shared Cache object + */ + public function get_cache_shared($name, $packed=true) + { + $shared_name = "shared_$name"; + + if (!array_key_exists($shared_name, $this->caches)) { + $opt = strtolower($name) . '_cache'; + $type = $this->config->get($opt); + $ttl = $this->config->get($opt . '_ttl'); + + if (!$type) { + // cache is disabled + return $this->caches[$shared_name] = null; + } + + if ($ttl === null) { + $ttl = $this->config->get('shared_cache_ttl', '10d'); + } + + $this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed); + } + + return $this->caches[$shared_name]; + } + + + /** * Create SMTP object and connect to server * * @param boolean True if connection should be established @@ -405,6 +437,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) { @@ -419,34 +452,44 @@ 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); - ini_set('session.serialize_handler', 'php'); ini_set('session.cookie_httponly', 1); // use database for storing session data $this->session = new rcube_session($this->get_dbh(), $this->config); - $this->session->register_gc_handler(array($this, 'temp_gc')); - $this->session->register_gc_handler(array($this, 'cache_gc')); - + $this->session->register_gc_handler(array($this, 'gc')); $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); $this->session->set_ip_check($this->config->get('ip_check')); // start PHP session (if not in CLI mode) if ($_SERVER['REMOTE_ADDR']) { - session_start(); + $this->session->start(); } } /** + * Garbage collector - cache/temp cleaner + */ + public function gc() + { + rcube_cache::gc(); + rcube_cache_shared::gc(); + $this->get_storage()->cache_gc(); + + $this->gc_temp(); + } + + + /** * Garbage collector function for temp files. * Remove temp files older than two days */ - public function temp_gc() + public function gc_temp() { $tmp = unslashify($this->config->get('temp_dir')); $expire = time() - 172800; // expire in 48 hours @@ -468,14 +511,21 @@ class rcube /** - * Garbage collector for cache entries. - * Set flag to expunge caches on shutdown + * Runs garbage collector with probability based on + * session settings. This is intended for environments + * without a session. */ - public function cache_gc() + public function gc_run() { - // because this gc function is called before storage is initialized, - // we just set a flag to expunge storage cache on shutdown. - $this->expunge_cache = true; + $probability = (int) ini_get('session.gc_probability'); + $divisor = (int) ini_get('session.gc_divisor'); + + if ($divisor > 0 && $probability > 0) { + $random = mt_rand(1, $divisor); + if ($random <= $probability) { + $this->gc(); + } + } } @@ -859,6 +909,14 @@ class rcube call_user_func($function); } + // write session data as soon as possible and before + // closing database connection, don't do this before + // registered shutdown functions, they may need the session + // Note: this will run registered gc handlers (ie. cache gc) + if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) { + $this->session->write_close(); + } + if (is_object($this->smtp)) { $this->smtp->disconnect(); } @@ -870,9 +928,6 @@ class rcube } if (is_object($this->storage)) { - if ($this->expunge_cache) { - $this->storage->expunge_cache(); - } $this->storage->close(); } } @@ -1073,14 +1128,20 @@ 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; + } + else if (is_string($arg)) { + $arg = array('message' => $arg, 'type' => 'php'); + } + + if (empty($arg['code'])) { + $arg['code'] = 500; } // installer @@ -1090,14 +1151,24 @@ class rcube return; } - if (($log || $terminate) && $arg['type'] && $arg['message']) { + $cli = php_sapi_name() == 'cli'; + + if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) { $arg['fatal'] = $terminate; self::log_bug($arg); } - // display error page and terminate script - if ($terminate && is_object(self::$instance->output)) { - self::$instance->output->raise_error($arg['code'], $arg['message']); + // terminate script + if ($terminate) { + // display error page + if (is_object(self::$instance->output)) { + self::$instance->output->raise_error($arg['code'], $arg['message']); + } + else if ($cli) { + fwrite(STDERR, 'ERROR: ' . $arg['message']); + } + + exit(1); } } @@ -1136,7 +1207,7 @@ class rcube if (!self::write_log('errors', $log_entry)) { // send error to PHPs error handler if write_log didn't succeed - trigger_error($arg_arr['message']); + trigger_error($arg_arr['message'], E_USER_WARNING); } } @@ -1274,6 +1345,188 @@ class rcube return $_SESSION['language']; } } + + /** + * Unique Message-ID generator. + * + * @return string Message-ID + */ + public function gen_message_id() + { + $local_part = md5(uniqid('rcube'.mt_rand(), true)); + $domain_part = $this->user->get_username('domain'); + + // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924) + if (!preg_match('/\.[a-z]+$/i', $domain_part)) { + foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) { + $host = preg_replace('/:[0-9]+$/', '', $host); + if ($host && preg_match('/\.[a-z]+$/i', $host)) { + $domain_part = $host; + } + } + } + + return sprintf('<%s@%s>', $local_part, $domain_part); + } + + /** + * Send the given message using the configured method. + * + * @param object $message Reference to Mail_MIME object + * @param string $from Sender address string + * @param array $mailto Array of recipient address strings + * @param array $error SMTP error array (reference) + * @param string $body_file Location of file with saved message body (reference), + * used when delay_file_io is enabled + * @param array $options SMTP options (e.g. DSN request) + * + * @return boolean Send status. + */ + public function deliver_message(&$message, $from, $mailto, &$error, &$body_file = null, $options = null) + { + $plugin = $this->plugins->exec_hook('message_before_send', array( + 'message' => $message, + 'from' => $from, + 'mailto' => $mailto, + 'options' => $options, + )); + + $from = $plugin['from']; + $mailto = $plugin['mailto']; + $options = $plugin['options']; + $message = $plugin['message']; + $headers = $message->headers(); + + // send thru SMTP server using custom SMTP library + if ($this->config->get('smtp_server')) { + // generate list of recipients + $a_recipients = array($mailto); + + if (strlen($headers['Cc'])) + $a_recipients[] = $headers['Cc']; + if (strlen($headers['Bcc'])) + $a_recipients[] = $headers['Bcc']; + + // clean Bcc from header for recipients + $send_headers = $headers; + unset($send_headers['Bcc']); + // here too, it because txtHeaders() below use $message->_headers not only $send_headers + unset($message->_headers['Bcc']); + + $smtp_headers = $message->txtHeaders($send_headers, true); + + if ($message->getParam('delay_file_io')) { + // use common temp dir + $temp_dir = $this->config->get('temp_dir'); + $body_file = tempnam($temp_dir, 'rcmMsg'); + if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) { + self::raise_error(array('code' => 650, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not create message: ".$mime_result->getMessage()), + TRUE, FALSE); + return false; + } + $msg_body = fopen($body_file, 'r'); + } + else { + $msg_body = $message->get(); + } + + // send message + if (!is_object($this->smtp)) { + $this->smtp_init(true); + } + + $sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options); + $response = $this->smtp->get_response(); + $error = $this->smtp->get_error(); + + // log error + if (!$sent) { + self::raise_error(array('code' => 800, 'type' => 'smtp', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "SMTP error: ".join("\n", $response)), TRUE, FALSE); + } + } + // send mail using PHP's mail() function + else { + // unset some headers because they will be added by the mail() function + $headers_enc = $message->headers($headers); + $headers_php = $message->_headers; + unset($headers_php['To'], $headers_php['Subject']); + + // reset stored headers and overwrite + $message->_headers = array(); + $header_str = $message->txtHeaders($headers_php); + + // #1485779 + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) { + $headers_enc['To'] = implode(', ', $m[1]); + } + } + + $msg_body = $message->get(); + + if (PEAR::isError($msg_body)) { + self::raise_error(array('code' => 650, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not create message: ".$msg_body->getMessage()), + TRUE, FALSE); + } + else { + $delim = $this->config->header_delimiter(); + $to = $headers_enc['To']; + $subject = $headers_enc['Subject']; + $header_str = rtrim($header_str); + + if ($delim != "\r\n") { + $header_str = str_replace("\r\n", $delim, $header_str); + $msg_body = str_replace("\r\n", $delim, $msg_body); + $to = str_replace("\r\n", $delim, $to); + $subject = str_replace("\r\n", $delim, $subject); + } + + if (ini_get('safe_mode')) + $sent = mail($to, $subject, $msg_body, $header_str); + else + $sent = mail($to, $subject, $msg_body, $header_str, "-f$from"); + } + } + + if ($sent) { + $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body)); + + // remove MDN headers after sending + unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']); + + // get all recipients + if ($headers['Cc']) + $mailto .= $headers['Cc']; + if ($headers['Bcc']) + $mailto .= $headers['Bcc']; + if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m)) + $mailto = implode(', ', array_unique($m[1])); + + if ($this->config->get('smtp_log')) { + self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s", + $this->user->get_username(), + $_SERVER['REMOTE_ADDR'], + $mailto, + !empty($response) ? join('; ', $response) : '')); + } + } + + if (is_resource($msg_body)) { + fclose($msg_body); + } + + $message->_headers = array(); + $message->headers($headers); + + return $sent; + } + } diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index cbc3c6773..d23ad3687 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -309,9 +309,14 @@ abstract class rcube_addressbook * List all active contact groups of this source * * @param string Optional search string to match group name + * @param int Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * * @return array Indexed list of contact groups, each a hash array */ - function list_groups($search = null) + function list_groups($search = null, $mode = 0) { /* empty for address books don't supporting groups */ return array(); @@ -370,9 +375,10 @@ abstract class rcube_addressbook /** * Add the given contact records the a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added + * @param string Group identifier + * @param array|string List of contact identifiers to be added + * + * @return int Number of contacts added */ function add_to_group($group_id, $ids) { @@ -383,9 +389,10 @@ abstract class rcube_addressbook /** * Remove the given contact records from a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members + * @param string Group identifier + * @param array|string List of contact identifiers to be removed + * + * @return int Number of deleted group members */ function remove_from_group($group_id, $ids) { @@ -425,7 +432,7 @@ abstract class rcube_addressbook $out = array_merge($out, (array)$values); } else { - list($f, $type) = explode(':', $c); + list(, $type) = explode(':', $c); $out[$type] = array_merge((array)$out[$type], (array)$values); } } @@ -528,7 +535,7 @@ abstract class rcube_addressbook */ public static function compose_contact_key($contact, $sort_col) { - $key = $contact[$sort_col] . ':' . $row['sourceid']; + $key = $contact[$sort_col] . ':' . $contact['sourceid']; // add email to a key to not skip contacts with the same name (#1488375) if (!empty($contact['email'])) { @@ -538,7 +545,6 @@ abstract class rcube_addressbook return $key; } - /** * Compare search value with contact data * diff --git a/program/lib/Roundcube/rcube_base_replacer.php b/program/lib/Roundcube/rcube_base_replacer.php index fcd85c2c8..e41ccb1d9 100644 --- a/program/lib/Roundcube/rcube_base_replacer.php +++ b/program/lib/Roundcube/rcube_base_replacer.php @@ -21,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 d10fe2a2c..34128291b 100644 --- a/program/lib/Roundcube/rcube_browser.php +++ b/program/lib/Roundcube/rcube_browser.php @@ -20,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 92f12a8bf..a708cb292 100644 --- a/program/lib/Roundcube/rcube_cache.php +++ b/program/lib/Roundcube/rcube_cache.php @@ -38,6 +38,7 @@ class rcube_cache private $type; private $userid; private $prefix; + private $table; private $ttl; private $packed; private $index; @@ -71,8 +72,9 @@ class rcube_cache $this->db = function_exists('apc_exists'); // APC 3.1.4 required } else { - $this->type = 'db'; - $this->db = $rcube->get_dbh(); + $this->type = 'db'; + $this->db = $rcube->get_dbh(); + $this->table = $this->db->table_name('cache'); } // convert ttl string to seconds @@ -145,7 +147,7 @@ class rcube_cache */ function write($key, $data) { - return $this->write_record($key, $this->packed ? serialize($data) : $data); + return $this->write_record($key, $this->serialize($data)); } @@ -192,20 +194,31 @@ class rcube_cache */ function expunge() { - if ($this->type == 'db' && $this->db) { + if ($this->type == 'db' && $this->db && $this->ttl) { $this->db->query( - "DELETE FROM ".$this->db->table_name('cache'). + "DELETE FROM ".$this->table. " WHERE user_id = ?". " AND cache_key LIKE ?". - " AND " . $this->db->unixtimestamp('created')." < ?", + " AND expires < " . $this->db->now(), $this->userid, - $this->prefix.'.%', - time() - $this->ttl); + $this->prefix.'.%'); } } /** + * Remove expired records of all caches + */ + static function gc() + { + $rcube = rcube::get_instance(); + $db = $rcube->get_dbh(); + + $db->query("DELETE FROM " . $db->table_name('cache') . " WHERE expires < " . $db->now()); + } + + + /** * Writes the cache back to the DB. */ function close() @@ -219,7 +232,7 @@ class rcube_cache if ($this->cache_changes[$key]) { // Make sure we're not going to write unchanged data // by comparing current md5 sum with the sum calculated on DB read - $data = $this->packed ? serialize($data) : $data; + $data = $this->serialize($data); if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) { $this->write_record($key, $data); @@ -255,7 +268,7 @@ class rcube_cache if ($data) { $md5sum = md5($data); - $data = $this->packed ? unserialize($data) : $data; + $data = $this->unserialize($data); if ($nostore) { return $data; @@ -271,7 +284,7 @@ class rcube_cache else { $sql_result = $this->db->limitquery( "SELECT data, cache_key". - " FROM ".$this->db->table_name('cache'). + " FROM " . $this->table. " WHERE user_id = ?". " AND cache_key = ?". // for better performance we allow more records for one key @@ -283,7 +296,7 @@ class rcube_cache $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1); $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null; if ($sql_arr['data']) { - $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data']; + $data = $this->unserialize($sql_arr['data']); } if ($nostore) { @@ -326,7 +339,7 @@ class rcube_cache // Remove NULL rows (here we don't need to check if the record exist) if ($data == 'N;') { $this->db->query( - "DELETE FROM ".$this->db->table_name('cache'). + "DELETE FROM " . $this->table. " WHERE user_id = ?". " AND cache_key = ?", $this->userid, $key); @@ -337,8 +350,10 @@ class rcube_cache // update existing cache record if ($key_exists) { $result = $this->db->query( - "UPDATE ".$this->db->table_name('cache'). - " SET created = ". $this->db->now().", data = ?". + "UPDATE " . $this->table. + " SET created = " . $this->db->now(). + ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL'). + ", data = ?". " WHERE user_id = ?". " AND cache_key = ?", $data, $this->userid, $key); @@ -348,9 +363,9 @@ class rcube_cache // for better performance we allow more records for one key // so, no need to check if record exist (see rcube_cache::read_record()) $result = $this->db->query( - "INSERT INTO ".$this->db->table_name('cache'). - " (created, user_id, cache_key, data)". - " VALUES (".$this->db->now().", ?, ?, ?)", + "INSERT INTO " . $this->table. + " (created, expires, user_id, cache_key, data)". + " VALUES (" . $this->db->now() . ", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?, ?)", $this->userid, $key, $data); } @@ -364,7 +379,6 @@ class rcube_cache * @param string $key Cache key name or pattern * @param boolean $prefix_mode Enable it to clear all keys starting * with prefix specified in $key - * */ private function remove_record($key=null, $prefix_mode=false) { @@ -412,7 +426,7 @@ class rcube_cache } $this->db->query( - "DELETE FROM ".$this->db->table_name('cache'). + "DELETE FROM " . $this->table. " WHERE user_id = ?" . $where, $this->userid); } @@ -553,4 +567,28 @@ class rcube_cache // This way each cache will have its own index return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX'); } + + /** + * Serializes data for storing + */ + private function serialize($data) + { + if ($this->type == 'db') { + return $this->db->encode($data, $this->packed); + } + + return $this->packed ? serialize($data) : $data; + } + + /** + * Unserializes serialized data + */ + private function unserialize($data) + { + if ($this->type == 'db') { + return $this->db->decode($data, $this->packed); + } + + return $this->packed ? @unserialize($data) : $data; + } } diff --git a/program/lib/Roundcube/rcube_cache_shared.php b/program/lib/Roundcube/rcube_cache_shared.php new file mode 100644 index 000000000..8f2574046 --- /dev/null +++ b/program/lib/Roundcube/rcube_cache_shared.php @@ -0,0 +1,581 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2011-2013, The Roundcube Dev Team | + | Copyright (C) 2011-2013, Kolab Systems AG | + | | + | 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: | + | Shared (cross-user) caching engine | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Interface class for accessing Roundcube shared cache + * + * @package Framework + * @subpackage Cache + * @author Thomas Bruederli <roundcube@gmail.com> + * @author Aleksander Machniak <alec@alec.pl> + */ +class rcube_cache_shared +{ + /** + * Instance of database handler + * + * @var rcube_db|Memcache|bool + */ + private $db; + private $type; + private $prefix; + private $ttl; + private $packed; + private $index; + private $table; + private $cache = array(); + private $cache_changes = array(); + private $cache_sums = array(); + + + /** + * Object constructor. + * + * @param string $type Engine type ('db' or 'memcache' or 'apc') + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + function __construct($type, $prefix='', $ttl=0, $packed=true) + { + $rcube = rcube::get_instance(); + $type = strtolower($type); + + if ($type == 'memcache') { + $this->type = 'memcache'; + $this->db = $rcube->get_memcache(); + } + else if ($type == 'apc') { + $this->type = 'apc'; + $this->db = function_exists('apc_exists'); // APC 3.1.4 required + } + else { + $this->type = 'db'; + $this->db = $rcube->get_dbh(); + $this->table = $this->db->table_name('cache_shared'); + } + + // convert ttl string to seconds + $ttl = get_offset_sec($ttl); + if ($ttl > 2592000) $ttl = 2592000; + + $this->ttl = $ttl; + $this->packed = $packed; + $this->prefix = $prefix; + } + + + /** + * Returns cached value. + * + * @param string $key Cache key name + * + * @return mixed Cached value + */ + function get($key) + { + if (!array_key_exists($key, $this->cache)) { + return $this->read_record($key); + } + + return $this->cache[$key]; + } + + + /** + * Sets (add/update) value in cache. + * + * @param string $key Cache key name + * @param mixed $data Cache data + */ + function set($key, $data) + { + $this->cache[$key] = $data; + $this->cache_changed = true; + $this->cache_changes[$key] = true; + } + + + /** + * Returns cached value without storing it in internal memory. + * + * @param string $key Cache key name + * + * @return mixed Cached value + */ + function read($key) + { + if (array_key_exists($key, $this->cache)) { + return $this->cache[$key]; + } + + return $this->read_record($key, true); + } + + + /** + * Sets (add/update) value in cache and immediately saves + * it in the backend, no internal memory will be used. + * + * @param string $key Cache key name + * @param mixed $data Cache data + * + * @param boolean True on success, False on failure + */ + function write($key, $data) + { + return $this->write_record($key, $this->serialize($data)); + } + + + /** + * Clears the cache. + * + * @param string $key Cache key name or pattern + * @param boolean $prefix_mode Enable it to clear all keys starting + * with prefix specified in $key + */ + function remove($key=null, $prefix_mode=false) + { + // Remove all keys + if ($key === null) { + $this->cache = array(); + $this->cache_changed = false; + $this->cache_changes = array(); + $this->cache_sums = array(); + } + // Remove keys by name prefix + else if ($prefix_mode) { + foreach (array_keys($this->cache) as $k) { + if (strpos($k, $key) === 0) { + $this->cache[$k] = null; + $this->cache_changes[$k] = false; + unset($this->cache_sums[$k]); + } + } + } + // Remove one key by name + else { + $this->cache[$key] = null; + $this->cache_changes[$key] = false; + unset($this->cache_sums[$key]); + } + + // Remove record(s) from the backend + $this->remove_record($key, $prefix_mode); + } + + + /** + * Remove cache records older than ttl + */ + function expunge() + { + if ($this->type == 'db' && $this->db && $this->ttl) { + $this->db->query( + "DELETE FROM " . $this->table + . " WHERE cache_key LIKE ?" + . " AND expires < " . $this->db->now(), + $this->prefix . '.%'); + } + } + + + /** + * Remove expired records of all caches + */ + static function gc() + { + $rcube = rcube::get_instance(); + $db = $rcube->get_dbh(); + + $db->query("DELETE FROM " . $db->table_name('cache_shared') . " WHERE expires < " . $db->now()); + } + + + /** + * Writes the cache back to the DB. + */ + function close() + { + if (!$this->cache_changed) { + return; + } + + foreach ($this->cache as $key => $data) { + // The key has been used + if ($this->cache_changes[$key]) { + // Make sure we're not going to write unchanged data + // by comparing current md5 sum with the sum calculated on DB read + $data = $this->serialize($data); + + if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) { + $this->write_record($key, $data); + } + } + } + + $this->write_index(); + } + + + /** + * Reads cache entry. + * + * @param string $key Cache key name + * @param boolean $nostore Enable to skip in-memory store + * + * @return mixed Cached value + */ + private function read_record($key, $nostore=false) + { + if (!$this->db) { + return null; + } + + if ($this->type != 'db') { + if ($this->type == 'memcache') { + $data = $this->db->get($this->ckey($key)); + } + else if ($this->type == 'apc') { + $data = apc_fetch($this->ckey($key)); + } + + if ($data) { + $md5sum = md5($data); + $data = $this->unserialize($data); + + if ($nostore) { + return $data; + } + + $this->cache_sums[$key] = $md5sum; + $this->cache[$key] = $data; + } + else { + $this->cache[$key] = null; + } + } + else { + $sql_result = $this->db->limitquery( + "SELECT data, cache_key". + " FROM " . $this->table . + " WHERE cache_key = ?". + // for better performance we allow more records for one key + // get the newer one + " ORDER BY created DESC", + 0, 1, $this->prefix . '.' . $key); + + if ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null; + if ($sql_arr['data']) { + $data = $this->unserialize($sql_arr['data']); + } + + if ($nostore) { + return $data; + } + + $this->cache[$key] = $data; + $this->cache_sums[$key] = $md5sum; + } + else { + $this->cache[$key] = null; + } + } + + return $this->cache[$key]; + } + + + /** + * Writes single cache record into DB. + * + * @param string $key Cache key name + * @param mxied $data Serialized cache data + * + * @param boolean True on success, False on failure + */ + private function write_record($key, $data) + { + if (!$this->db) { + return false; + } + + if ($this->type == 'memcache' || $this->type == 'apc') { + return $this->add_record($this->ckey($key), $data); + } + + $key_exists = array_key_exists($key, $this->cache_sums); + $key = $this->prefix . '.' . $key; + + // Remove NULL rows (here we don't need to check if the record exist) + if ($data == 'N;') { + $this->db->query("DELETE FROM " . $this->table . " WHERE cache_key = ?", $key); + return true; + } + + // update existing cache record + if ($key_exists) { + $result = $this->db->query( + "UPDATE " . $this->table . + " SET created = " . $this->db->now() . + ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . + ", data = ?". + " WHERE cache_key = ?", + $data, $key); + } + // add new cache record + else { + // for better performance we allow more records for one key + // so, no need to check if record exist (see rcube_cache::read_record()) + $result = $this->db->query( + "INSERT INTO ".$this->table. + " (created, expires, cache_key, data)". + " VALUES (".$this->db->now().", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?)", + $key, $data); + } + + return $this->db->affected_rows($result); + } + + + /** + * Deletes the cache record(s). + * + * @param string $key Cache key name or pattern + * @param boolean $prefix_mode Enable it to clear all keys starting + * with prefix specified in $key + */ + private function remove_record($key=null, $prefix_mode=false) + { + if (!$this->db) { + return; + } + + if ($this->type != 'db') { + $this->load_index(); + + // Remove all keys + if ($key === null) { + foreach ($this->index as $key) { + $this->delete_record($key, false); + } + $this->index = array(); + } + // Remove keys by name prefix + else if ($prefix_mode) { + foreach ($this->index as $k) { + if (strpos($k, $key) === 0) { + $this->delete_record($k); + } + } + } + // Remove one key by name + else { + $this->delete_record($key); + } + + return; + } + + // Remove all keys (in specified cache) + if ($key === null) { + $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.%'); + } + // Remove keys by name prefix + else if ($prefix_mode) { + $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); + } + // Remove one key by name + else { + $where = " WHERE cache_key = " . $this->db->quote($this->prefix.'.'.$key); + } + + $this->db->query("DELETE FROM " . $this->table . $where); + } + + + /** + * Adds entry into memcache/apc DB. + * + * @param string $key Cache key name + * @param mxied $data Serialized cache data + * @param bollean $index Enables immediate index update + * + * @param boolean True on success, False on failure + */ + private function add_record($key, $data, $index=false) + { + if ($this->type == 'memcache') { + $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); + if (!$result) { + $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); + } + } + else if ($this->type == 'apc') { + if (apc_exists($key)) { + apc_delete($key); + } + $result = apc_store($key, $data, $this->ttl); + } + + // Update index + if ($index && $result) { + $this->load_index(); + + if (array_search($key, $this->index) === false) { + $this->index[] = $key; + $data = serialize($this->index); + $this->add_record($this->ikey(), $data); + } + } + + return $result; + } + + + /** + * Deletes entry from memcache/apc DB. + */ + private function delete_record($key, $index=true) + { + if ($this->type == 'memcache') { + // #1488592: use 2nd argument + $this->db->delete($this->ckey($key), 0); + } + else { + apc_delete($this->ckey($key)); + } + + if ($index) { + if (($idx = array_search($key, $this->index)) !== false) { + unset($this->index[$idx]); + } + } + } + + + /** + * Writes the index entry into memcache/apc DB. + */ + private function write_index() + { + if (!$this->db) { + return; + } + + if ($this->type == 'db') { + return; + } + + $this->load_index(); + + // Make sure index contains new keys + foreach ($this->cache as $key => $value) { + if ($value !== null) { + if (array_search($key, $this->index) === false) { + $this->index[] = $key; + } + } + } + + $data = serialize($this->index); + $this->add_record($this->ikey(), $data); + } + + + /** + * Gets the index entry from memcache/apc DB. + */ + private function load_index() + { + if (!$this->db) { + return; + } + + if ($this->index !== null) { + return; + } + + $index_key = $this->ikey(); + + if ($this->type == 'memcache') { + $data = $this->db->get($index_key); + } + else if ($this->type == 'apc') { + $data = apc_fetch($index_key); + } + + $this->index = $data ? unserialize($data) : array(); + } + + + /** + * Creates cache key name (for memcache and apc) + * + * @param string $key Cache key name + * + * @return string Cache key + */ + private function ckey($key) + { + return $this->prefix . ':' . $key; + } + + + /** + * Creates index cache key name (for memcache and apc) + * + * @return string Cache key + */ + private function ikey() + { + // This way each cache will have its own index + return $this->prefix . 'INDEX'; + } + + /** + * Serializes data for storing + */ + private function serialize($data) + { + if ($this->type == 'db') { + return $this->db->encode($data, $this->packed); + } + + return $this->packed ? serialize($data) : $data; + } + + /** + * Unserializes serialized data + */ + private function unserialize($data) + { + if ($this->type == 'db') { + return $this->db->decode($data, $this->packed); + } + + return $this->packed ? @unserialize($data) : $data; + } +} diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php index 2190dc4c2..18055f77d 100644 --- a/program/lib/Roundcube/rcube_config.php +++ b/program/lib/Roundcube/rcube_config.php @@ -43,6 +43,8 @@ class rcube_config 'reply_mode' => 'top_posting', 'refresh_interval' => 'keep_alive', 'min_refresh_interval' => 'min_keep_alive', + 'messages_cache_ttl' => 'message_cache_lifetime', + 'redundant_attachments_cache_ttl' => 'redundant_attachments_memcache_ttl', ); @@ -174,7 +176,7 @@ class rcube_config ob_end_clean(); if (is_array($rcmail_config)) { - $this->prop = array_merge($this->prop, $rcmail_config, $this->userprefs); + $this->merge($rcmail_config); return true; } } @@ -195,9 +197,6 @@ class rcube_config if (isset($this->prop[$name])) { $result = $this->prop[$name]; } - else if (isset($this->legacy_props[$name])) { - return $this->get($this->legacy_props[$name], $def); - } else { $result = $def; } @@ -241,6 +240,7 @@ class rcube_config public function merge($prefs) { $this->prop = array_merge($this->prop, $prefs, $this->userprefs); + $this->fix_legacy_props(); } @@ -273,6 +273,8 @@ class rcube_config $this->userprefs = $prefs; $this->prop = array_merge($this->prop, $prefs); + $this->fix_legacy_props(); + // override timezone settings with client values if ($this->prop['timezone'] == 'auto') { $this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $this->client_timezone() : $this->prop['_timezone_value']; @@ -435,4 +437,18 @@ class rcube_config return date_default_timezone_get(); } + /** + * Convert legacy options into new ones + */ + private function fix_legacy_props() + { + foreach ($this->legacy_props as $new => $old) { + if (isset($this->prop[$old])) { + if (!isset($this->prop[$new])) { + $this->prop[$new] = $this->prop[$old]; + } + unset($this->prop[$old]); + } + } + } } diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php index c66e98687..3919cdc6e 100644 --- a/program/lib/Roundcube/rcube_contacts.php +++ b/program/lib/Roundcube/rcube_contacts.php @@ -137,16 +137,34 @@ class rcube_contacts extends rcube_addressbook * List all active contact groups of this source * * @param string Search string to match group name + * @param int Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * * @return array Indexed list of contact groups, each a hash array */ - function list_groups($search = null) + function list_groups($search = null, $mode = 0) { $results = array(); if (!$this->groups) return $results; - $sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : ''; + if ($search) { + switch (intval($mode)) { + case 1: + $sql_filter = $this->db->ilike('name', $search); + break; + case 2: + $sql_filter = $this->db->ilike('name', $search . '%'); + break; + default: + $sql_filter = $this->db->ilike('name', '%' . $search . '%'); + } + + $sql_filter = " AND $sql_filter"; + } $sql_result = $this->db->query( "SELECT * FROM ".$this->db->table_name($this->db_groups). @@ -626,10 +644,6 @@ class rcube_contacts extends rcube_addressbook $insert_id = $this->db->insert_id($this->db_name); } - // also add the newly created contact to the active group - if ($insert_id && $this->group_id) - $this->add_to_group($this->group_id, $insert_id); - $this->cache = null; return $insert_id; @@ -883,9 +897,10 @@ class rcube_contacts extends rcube_addressbook /** * Add the given contact records the a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added + * @param string Group identifier + * @param array|string List of contact identifiers to be added + * + * @return int Number of contacts added */ function add_to_group($group_id, $ids) { @@ -930,9 +945,10 @@ class rcube_contacts extends rcube_addressbook /** * Remove the given contact records from a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members + * @param string Group identifier + * @param array|string List of contact identifiers to be removed + * + * @return int Number of deleted group members */ function remove_from_group($group_id, $ids) { diff --git a/program/lib/Roundcube/rcube_content_filter.php b/program/lib/Roundcube/rcube_content_filter.php index b814bb71d..ae6617d1b 100644 --- a/program/lib/Roundcube/rcube_content_filter.php +++ b/program/lib/Roundcube/rcube_content_filter.php @@ -20,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 0d3276b84..fb8d8f103 100644 --- a/program/lib/Roundcube/rcube_csv2vcard.php +++ b/program/lib/Roundcube/rcube_csv2vcard.php @@ -130,6 +130,21 @@ class rcube_csv2vcard 'work_state' => 'region:work', 'home_city_short' => 'locality:home', 'home_state_short' => 'region:home', + + // Atmail + 'date_of_birth' => 'birthday', + 'email' => 'email:pref', + 'home_mobile' => 'phone:cell', + 'home_zip' => 'zipcode:home', + 'info' => 'notes', + 'user_photo' => 'photo', + 'url' => 'website:homepage', + 'work_company' => 'organization', + 'work_dept' => 'departament', + 'work_fax' => 'phone:work,fax', + 'work_mobile' => 'phone:work,cell', + 'work_title' => 'jobtitle', + 'work_zip' => 'zipcode:work', ); /** @@ -230,8 +245,29 @@ class rcube_csv2vcard 'work_phone' => "Work Phone", 'work_address' => "Work Address", //'work_address_2' => "Work Address 2", + 'work_city' => "Work City", 'work_country' => "Work Country", + 'work_state' => "Work State", 'work_zipcode' => "Work ZipCode", + + // Atmail + 'date_of_birth' => "Date of Birth", + 'email' => "Email", + //'email_2' => "Email2", + //'email_3' => "Email3", + //'email_4' => "Email4", + //'email_5' => "Email5", + 'home_mobile' => "Home Mobile", + 'home_zip' => "Home Zip", + 'info' => "Info", + 'user_photo' => "User Photo", + 'url' => "URL", + 'work_company' => "Work Company", + 'work_dept' => "Work Dept", + 'work_fax' => "Work Fax", + 'work_mobile' => "Work Mobile", + 'work_title' => "Work Title", + 'work_zip' => "Work Zip", ); protected $local_label_map = array(); @@ -268,7 +304,6 @@ class rcube_csv2vcard { // convert to UTF-8 $head = substr($csv, 0, 4096); - $fallback = rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); // fallback to Latin-1? $charset = rcube_charset::detect($head, RCUBE_CHARSET); $csv = rcube_charset::convert($csv, $charset); $head = ''; @@ -276,7 +311,7 @@ class rcube_csv2vcard $this->map = array(); // Parse file - foreach (preg_split("/[\r\n]+/", $csv) as $i => $line) { + foreach (preg_split("/[\r\n]+/", $csv) as $line) { $elements = $this->parse_line($line); if (empty($elements)) { continue; @@ -353,6 +388,12 @@ class rcube_csv2vcard if (!empty($this->local_label_map)) { for ($i = 0; $i < $size; $i++) { $label = $this->local_label_map[$elements[$i]]; + + // special localization label + if ($label && $label[0] == '_') { + $label = substr($label, 1); + } + if ($label && !empty($this->csv2vcard_map[$label])) { $map2[$i] = $this->csv2vcard_map[$label]; } @@ -384,9 +425,13 @@ class rcube_csv2vcard $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d']; } + // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00" foreach (array('birthday', 'anniversary') as $key) { - if (!empty($contact[$key]) && $contact[$key] == '0/0/00') { // @TODO: localization? - unset($contact[$key]); + if (!empty($contact[$key])) { + $date = preg_replace('/[0[:^word:]]/', '', $contact[$key]); + if (empty($date)) { + unset($contact[$key]); + } } } diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index a3475a2fd..852070073 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -47,6 +47,7 @@ class rcube_db 'identifier_end' => '"', ); + const DEBUG_LINE_LENGTH = 4096; /** * Factory, returns driver-specific instance of the class @@ -99,27 +100,15 @@ class rcube_db $this->db_dsnw_array = self::parse_dsn($db_dsnw); $this->db_dsnr_array = self::parse_dsn($db_dsnr); - - // Initialize driver class - $this->init(); - } - - /** - * Initialization of the object with driver specific code - */ - protected function init() - { - // To be used by driver classes } /** * Connect to specific database * - * @param array $dsn DSN for DB connections - * - * @return PDO database handle + * @param array $dsn DSN for DB connections + * @param string $mode Connection mode (r|w) */ - protected function dsn_connect($dsn) + protected function dsn_connect($dsn, $mode) { $this->db_error = false; $this->db_error_msg = null; @@ -128,7 +117,7 @@ class rcube_db $dsn_string = $this->dsn_string($dsn); $dsn_options = $this->dsn_options($dsn); - if ($db_pconn) { + if ($this->db_pconn) { $dsn_options[PDO::ATTR_PERSISTENT] = true; } @@ -157,9 +146,10 @@ class rcube_db return null; } + $this->dbh = $dbh; + $this->db_mode = $mode; + $this->db_connected = true; $this->conn_configure($dsn, $dbh); - - return $dbh; } /** @@ -182,16 +172,6 @@ class rcube_db } /** - * Driver-specific database character set setting - * - * @param string $charset Character set name - */ - protected function set_charset($charset) - { - $this->query("SET NAMES 'utf8'"); - } - - /** * Connect to appropriate database depending on the operation * * @param string $mode Connection mode (r|w) @@ -218,23 +198,14 @@ class rcube_db $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array; - $this->dbh = $this->dsn_connect($dsn); - $this->db_connected = is_object($this->dbh); + $this->dsn_connect($dsn, $mode); // use write-master when read-only fails - if (!$this->db_connected && $mode == 'r') { - $mode = 'w'; - $this->dbh = $this->dsn_connect($this->db_dsnw_array); - $this->db_connected = is_object($this->dbh); + if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) { + $this->dsn_connect($this->db_dsnw_array, 'w'); } - if ($this->db_connected) { - $this->db_mode = $mode; - $this->set_charset('utf8'); - } - else { - $this->conn_failure = true; - } + $this->conn_failure = !$this->db_connected; } /** @@ -255,6 +226,11 @@ class rcube_db protected function debug($query) { if ($this->options['debug_mode']) { + if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) { + $diff = $len - self::DEBUG_LINE_LENGTH; + $query = substr($query, 0, self::DEBUG_LINE_LENGTH) + . "... [truncated $diff bytes]"; + } rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';'); } } @@ -362,8 +338,10 @@ class rcube_db */ protected function _query($query, $offset, $numrows, $params) { + $query = trim($query); + // Read or write ? - $mode = preg_match('/^(select|show)/i', ltrim($query)) ? 'r' : 'w'; + $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w'; $this->db_connect($mode); @@ -405,21 +383,25 @@ class rcube_db $this->db_error_msg = null; // send query - $query = $this->dbh->query($query); + $result = $this->dbh->query($query); - if ($query === false) { + if ($result === false) { $error = $this->dbh->errorInfo(); - $this->db_error = true; - $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); + if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') { + $this->db_error = true; + $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg . " (SQL Query: $query)" + ), true, false); + } } - $this->last_result = $query; + $this->last_result = $result; - return $query; + return $result; } /** @@ -439,6 +421,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) @@ -571,7 +579,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 */ @@ -586,6 +594,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'); @@ -604,6 +616,22 @@ class rcube_db } /** + * Escapes a string so it can be safely used in a query + * + * @param string $str A string to escape + * + * @return string Escaped string for use in a query + */ + public function escape($str) + { + if (is_null($str)) { + return 'NULL'; + } + + return substr($this->quote($str), 1, -1); + } + + /** * Quotes a string so it can be safely used as a table or column name * * @param string $str Value to quote @@ -618,6 +646,20 @@ class rcube_db } /** + * Escapes a string so it can be safely used in a query + * + * @param string $str A string to escape + * + * @return string Escaped string for use in a query + * @deprecated Replaced by rcube_db::escape + * @see rcube_db::escape + */ + public function escapeSimple($str) + { + return $this->escape($str); + } + + /** * Quotes a string so it can be safely used as a table or column name * * @param string $str Value to quote @@ -635,24 +677,32 @@ class rcube_db $name[] = $start . $elem . $end; } - return implode($name, '.'); + return implode($name, '.'); } /** * Return SQL function for current time and date * + * @param int $interval Optional interval (in seconds) to add/subtract + * * @return string SQL function to use in query */ - public function now() + public function now($interval = 0) { - return "now()"; + if ($interval) { + $add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL '; + $add .= $interval > 0 ? intval($interval) : intval($interval) * -1; + $add .= ' SECOND'; + } + + return "now()" . $add; } /** * 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 */ @@ -728,12 +778,19 @@ class rcube_db /** * Encodes non-UTF-8 characters in string/array/object (recursive) * - * @param mixed $input Data to fix + * @param mixed $input Data to fix + * @param bool $serialized Enable serialization * * @return mixed Properly UTF-8 encoded data */ - public static function encode($input) + public static function encode($input, $serialized = false) { + // use Base64 encoding to workaround issues with invalid + // or null characters in serialized string (#1489142) + if ($serialized) { + return base64_encode(serialize($input)); + } + if (is_object($input)) { foreach (get_object_vars($input) as $idx => $value) { $input->$idx = self::encode($value); @@ -744,6 +801,7 @@ class rcube_db foreach ($input as $idx => $value) { $input[$idx] = self::encode($value); } + return $input; } @@ -753,12 +811,24 @@ class rcube_db /** * Decodes encoded UTF-8 string/object/array (recursive) * - * @param mixed $input Input data + * @param mixed $input Input data + * @param bool $serialized Enable serialization * * @return mixed Decoded data */ - public static function decode($input) + public static function decode($input, $serialized = false) { + // use Base64 encoding to workaround issues with invalid + // or null characters in serialized string (#1489142) + if ($serialized) { + // Keep backward compatybility where base64 wasn't used + if (strpos(substr($input, 0, 16), ':') !== false) { + return self::decode(@unserialize($input)); + } + + return @unserialize(base64_decode($input)); + } + if (is_object($input)) { foreach (get_object_vars($input) as $idx => $value) { $input->$idx = self::decode($value); @@ -786,17 +856,26 @@ class rcube_db { $rcube = rcube::get_instance(); - // return table name if configured - $config_key = 'db_table_'.$table; - - if ($name = $rcube->config->get($config_key)) { - return $name; + // add prefix to the table name if configured + if ($prefix = $rcube->config->get('db_prefix')) { + return $prefix . $table; } return $table; } /** + * Set class option value + * + * @param string $name Option name + * @param mixed $value Option value + */ + public function set_option($name, $value) + { + $this->options[$name] = $value; + } + + /** * MDB2 DSN string parser * * @param string $sequence Secuence name diff --git a/program/lib/Roundcube/rcube_db_mssql.php b/program/lib/Roundcube/rcube_db_mssql.php index 84fe22bbc..3c1b9d71f 100644 --- a/program/lib/Roundcube/rcube_db_mssql.php +++ b/program/lib/Roundcube/rcube_db_mssql.php @@ -29,38 +29,52 @@ class rcube_db_mssql extends rcube_db public $db_provider = 'mssql'; /** - * Driver initialization + * Object constructor + * + * @param string $db_dsnw DSN for read/write operations + * @param string $db_dsnr Optional DSN for read only operations + * @param bool $pconn Enables persistent connections */ - protected function init() + public function __construct($db_dsnw, $db_dsnr = '', $pconn = false) { + parent::__construct($db_dsnw, $db_dsnr, $pconn); + $this->options['identifier_start'] = '['; $this->options['identifier_end'] = ']'; } /** - * Character setting + * Driver-specific configuration of database connection + * + * @param array $dsn DSN for DB connections + * @param PDO $dbh Connection handler */ - protected function set_charset($charset) + protected function conn_configure($dsn, $dbh) { - // UTF-8 is default + // Set date format in case of non-default language (#1488918) + $this->query("SET DATEFORMAT ymd"); } /** * Return SQL function for current time and date * + * @param int $interval Optional interval (in seconds) to add/subtract + * * @return string SQL function to use in query */ - public function now() + public function now($interval = 0) { + if ($interval) { + $interval = intval($interval); + return "dateadd(second, $interval, getdate())"; + } + return "getdate()"; } /** * Return SQL statement to convert a field value into a unix timestamp * - * This method is deprecated and should not be used anymore due to limitations - * of timestamp functions in Mysql (year 2038 problem) - * * @param string $field Field name * * @return string SQL statement to use in query @@ -100,26 +114,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 8ab6403c8..6fa5ad768 100644 --- a/program/lib/Roundcube/rcube_db_mysql.php +++ b/program/lib/Roundcube/rcube_db_mysql.php @@ -30,16 +30,40 @@ class rcube_db_mysql extends rcube_db public $db_provider = 'mysql'; /** - * Driver initialization/configuration + * Object constructor + * + * @param string $db_dsnw DSN for read/write operations + * @param string $db_dsnr Optional DSN for read only operations + * @param bool $pconn Enables persistent connections */ - protected function init() + public function __construct($db_dsnw, $db_dsnr = '', $pconn = false) { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + rcube::raise_error(array('code' => 600, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "MySQL driver requires PHP >= 5.3, current version is " . PHP_VERSION), + true, true); + } + + parent::__construct($db_dsnw, $db_dsnr, $pconn); + // SQL identifiers quoting $this->options['identifier_start'] = '`'; $this->options['identifier_end'] = '`'; } /** + * Driver-specific configuration of database connection + * + * @param array $dsn DSN for DB connections + * @param PDO $dbh Connection handler + */ + protected function conn_configure($dsn, $dbh) + { + $this->query("SET NAMES 'utf8'"); + } + + /** * Abstract SQL statement for value concatenation * * @return string SQL statement to be used in query @@ -127,7 +151,7 @@ class rcube_db_mysql extends rcube_db $result[PDO::MYSQL_ATTR_FOUND_ROWS] = true; // Enable AUTOCOMMIT mode (#1488902) - $dsn_options[PDO::ATTR_AUTOCOMMIT] = true; + $result[PDO::ATTR_AUTOCOMMIT] = true; return $result; } @@ -147,7 +171,7 @@ class rcube_db_mysql extends rcube_db $result = $this->query('SHOW VARIABLES'); - while ($sql_arr = $this->fetch_array($result)) { + while ($row = $this->fetch_array($result)) { $this->variables[$row[0]] = $row[1]; } } diff --git a/program/lib/Roundcube/rcube_db_pgsql.php b/program/lib/Roundcube/rcube_db_pgsql.php index cf23c5e48..d72c9d6b3 100644 --- a/program/lib/Roundcube/rcube_db_pgsql.php +++ b/program/lib/Roundcube/rcube_db_pgsql.php @@ -29,6 +29,17 @@ class rcube_db_pgsql extends rcube_db public $db_provider = 'postgres'; /** + * Driver-specific configuration of database connection + * + * @param array $dsn DSN for DB connections + * @param PDO $dbh Connection handler + */ + protected function conn_configure($dsn, $dbh) + { + $this->query("SET NAMES 'utf8'"); + } + + /** * Get last inserted record ID * * @param string $table Table name (to find the incremented sequence) @@ -53,19 +64,20 @@ class rcube_db_pgsql extends rcube_db /** * Return correct name for a specific database sequence * - * @param string $sequence Secuence name + * @param string $table Table name * * @return string Translated sequence name */ - protected function sequence_name($sequence) + protected function sequence_name($table) { - $rcube = rcube::get_instance(); + // Note: we support only one sequence per table + // Note: The sequence name must be <table_name>_seq + $sequence = $table . '_seq'; + $rcube = rcube::get_instance(); // return sequence name if configured - $config_key = 'db_sequence_'.$sequence; - - if ($name = $rcube->config->get($config_key)) { - return $name; + if ($prefix = $rcube->config->get('db_prefix')) { + return $prefix . $sequence; } return $sequence; @@ -74,9 +86,6 @@ class rcube_db_pgsql extends rcube_db /** * Return SQL statement to convert a field value into a unix timestamp * - * This method is deprecated and should not be used anymore due to limitations - * of timestamp functions in Mysql (year 2038 problem) - * * @param string $field Field name * * @return string SQL statement to use in query @@ -88,6 +97,24 @@ class rcube_db_pgsql extends rcube_db } /** + * Return SQL function for current time and date + * + * @param int $interval Optional interval (in seconds) to add/subtract + * + * @return string SQL function to use in query + */ + public function now($interval = 0) + { + if ($interval) { + $add = ' ' . ($interval > 0 ? '+' : '-') . " interval '"; + $add .= $interval > 0 ? intval($interval) : intval($interval) * -1; + $add .= " seconds'"; + } + + return "now()" . $add; + } + + /** * Return SQL statement for case insensitive LIKE * * @param string $column Field name @@ -129,4 +156,38 @@ class rcube_db_pgsql extends rcube_db return isset($this->variables[$varname]) ? $this->variables[$varname] : $default; } + /** + * Returns PDO DSN string from DSN array + * + * @param array $dsn DSN parameters + * + * @return string DSN string + */ + protected function dsn_string($dsn) + { + $params = array(); + $result = 'pgsql:'; + + if ($dsn['hostspec']) { + $params[] = 'host=' . $dsn['hostspec']; + } + else if ($dsn['socket']) { + $params[] = 'host=' . $dsn['socket']; + } + + if ($dsn['port']) { + $params[] = 'port=' . $dsn['port']; + } + + if ($dsn['database']) { + $params[] = 'dbname=' . $dsn['database']; + } + + if (!empty($params)) { + $result .= implode(';', $params); + } + + return $result; + } + } diff --git a/program/lib/Roundcube/rcube_db_sqlite.php b/program/lib/Roundcube/rcube_db_sqlite.php index 145b8a371..b66c56097 100644 --- a/program/lib/Roundcube/rcube_db_sqlite.php +++ b/program/lib/Roundcube/rcube_db_sqlite.php @@ -29,13 +29,6 @@ class rcube_db_sqlite extends rcube_db public $db_provider = 'sqlite'; /** - * Database character set - */ - protected function set_charset($charset) - { - } - - /** * Prepare connection */ protected function conn_prepare($dsn) @@ -56,10 +49,6 @@ class rcube_db_sqlite extends rcube_db */ protected function conn_configure($dsn, $dbh) { - // we emulate via callback some missing functions - $dbh->sqliteCreateFunction('unix_timestamp', array('rcube_db_sqlite', 'sqlite_unix_timestamp'), 1); - $dbh->sqliteCreateFunction('now', array('rcube_db_sqlite', 'sqlite_now'), 0); - // Initialize database structure in file is empty if (!empty($dsn['database']) && !filesize($dsn['database'])) { $data = file_get_contents(RCUBE_INSTALL_PATH . 'SQL/sqlite.initial.sql'); @@ -83,30 +72,32 @@ class rcube_db_sqlite extends rcube_db } /** - * Callback for sqlite: unix_timestamp() + * Return SQL statement to convert a field value into a unix timestamp + * + * @param string $field Field name + * + * @return string SQL statement to use in query + * @deprecated */ - public static function sqlite_unix_timestamp($timestamp = '') + public function unixtimestamp($field) { - $timestamp = trim($timestamp); - if (!$timestamp) { - $ret = time(); - } - else if (!preg_match('/^[0-9]+$/s', $timestamp)) { - $ret = strtotime($timestamp); - } - else { - $ret = $timestamp; - } - - return $ret; + return "strftime('%s', $field)"; } /** - * Callback for sqlite: now() + * Return SQL function for current time and date + * + * @param int $interval Optional interval (in seconds) to add/subtract + * + * @return string SQL function to use in query */ - public static function sqlite_now() + public function now($interval = 0) { - return date("Y-m-d H:i:s"); + if ($interval) { + $add = ($interval > 0 ? '+' : '') . intval($interval) . ' seconds'; + } + + return "datetime('now'" . ($add ? ",'$add'" : "") . ")"; } /** diff --git a/program/lib/Roundcube/rcube_db_sqlsrv.php b/program/lib/Roundcube/rcube_db_sqlsrv.php index e69678025..45c41cdaf 100644 --- a/program/lib/Roundcube/rcube_db_sqlsrv.php +++ b/program/lib/Roundcube/rcube_db_sqlsrv.php @@ -29,29 +29,46 @@ class rcube_db_sqlsrv extends rcube_db public $db_provider = 'mssql'; /** - * Driver initialization + * Object constructor + * + * @param string $db_dsnw DSN for read/write operations + * @param string $db_dsnr Optional DSN for read only operations + * @param bool $pconn Enables persistent connections */ - protected function init() + public function __construct($db_dsnw, $db_dsnr = '', $pconn = false) { + parent::__construct($db_dsnw, $db_dsnr, $pconn); + $this->options['identifier_start'] = '['; $this->options['identifier_end'] = ']'; } /** - * Database character set setting + * Driver-specific configuration of database connection + * + * @param array $dsn DSN for DB connections + * @param PDO $dbh Connection handler */ - protected function set_charset($charset) + protected function conn_configure($dsn, $dbh) { - // UTF-8 is default + // Set date format in case of non-default language (#1488918) + $this->query("SET DATEFORMAT ymd"); } /** * Return SQL function for current time and date * + * @param int $interval Optional interval (in seconds) to add/subtract + * * @return string SQL function to use in query */ - public function now() + public function now($interval = 0) { + if ($interval) { + $interval = intval($interval); + return "dateadd(second, $interval, getdate())"; + } + return "getdate()"; } @@ -100,26 +117,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 index 8c628c912..12deb33ce 100644 --- a/program/lib/Roundcube/rcube_enriched.php +++ b/program/lib/Roundcube/rcube_enriched.php @@ -118,7 +118,7 @@ class rcube_enriched $quoted = ''; $lines = explode('<br>', $a[2]); - foreach ($lines as $n => $line) + foreach ($lines as $line) $quoted .= '>'.$line.'<br>'; $body = $a[1].'<span class="quotes">'.$quoted.'</span>'.$a[3]; diff --git a/program/lib/Roundcube/rcube_image.php b/program/lib/Roundcube/rcube_image.php index 9695022da..09bb4e81b 100644 --- a/program/lib/Roundcube/rcube_image.php +++ b/program/lib/Roundcube/rcube_image.php @@ -77,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 @@ -92,6 +93,10 @@ class rcube_image $convert = $rcube->config->get('im_convert_path', false); $props = $this->props(); + if (empty($props)) { + return false; + } + if (!$filename) { $filename = $this->image_file; } @@ -123,6 +128,7 @@ class rcube_image } if ($result === '') { + @chmod($filename, 0600); return $type; } } @@ -131,19 +137,34 @@ class rcube_image if ($props['gd_type']) { if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) { $image = imagecreatefromjpeg($this->image_file); + $type = 'jpg'; } else if($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) { $image = imagecreatefromgif($this->image_file); + $type = 'gid'; } 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']); + if ($image === false) { + 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; + } + $width = $props['width'] * $scale; $height = $props['height'] * $scale; @@ -162,18 +183,16 @@ 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) { + @chmod($filename, 0600); return $type; } } @@ -214,6 +233,7 @@ class rcube_image $result = rcube::exec($convert . ' 2>&1 -colorspace RGB -quality 75 {in} {type}:{out}', $p); if ($result === '') { + @chmod($filename, 0600); return true; } } @@ -245,6 +265,11 @@ class rcube_image else if ($type == self::TYPE_PNG) { $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS); } + + if ($result) { + @chmod($filename, 0600); + 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 74c1f5324..3ca8a07c9 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -308,14 +308,7 @@ class rcube_imap extends rcube_storage */ public function set_folder($folder) { - if ($this->folder == $folder) { - return; - } - $this->folder = $folder; - - // clear messagecount cache for this folder - $this->clear_messagecount($folder); } @@ -626,7 +619,7 @@ class rcube_imap extends rcube_storage } if ($mode == 'THREADS') { - $res = $this->fetch_threads($folder, $force); + $res = $this->threads($folder); $count = $res->count(); if ($status) { @@ -656,11 +649,11 @@ class rcube_imap extends rcube_storage $keys[] = 'ALL'; } if ($status) { - $keys[] = 'MAX'; + $keys[] = 'MAX'; } } - // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here + // @TODO: if $mode == 'ALL' we could try to use cache index here // get message count using (E)SEARCH // not very performant but more precise (using UNDELETED) @@ -791,7 +784,7 @@ class rcube_imap extends rcube_storage $threads = $mcache->get_thread($folder); } else { - $threads = $this->fetch_threads($folder); + $threads = $this->threads($folder); } return $this->fetch_thread_headers($folder, $threads, $page, $slice); @@ -800,32 +793,47 @@ class rcube_imap extends rcube_storage /** * Method for fetching threads data * - * @param string $folder Folder name - * @param bool $force Use IMAP server, no cache + * @param string $folder Folder name * * @return rcube_imap_thread Thread data object */ - function fetch_threads($folder, $force = false) + function threads($folder) { - if (!$force && ($mcache = $this->get_mcache_engine())) { + if ($mcache = $this->get_mcache_engine()) { // don't store in self's internal cache, cache has it's own internal cache return $mcache->get_thread($folder); } - if (empty($this->icache['threads'])) { - if (!$this->check_connection()) { - return new rcube_result_thread(); + if (!empty($this->icache['threads'])) { + if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) { + return $this->icache['threads']; } + } + + // get all threads + $result = $this->threads_direct($folder); + + // add to internal (fast) cache + return $this->icache['threads'] = $result; + } - // get all threads - $result = $this->conn->thread($folder, $this->threading, - $this->options['skip_deleted'] ? 'UNDELETED' : '', true); - // add to internal (fast) cache - $this->icache['threads'] = $result; + /** + * Method for direct fetching of threads data + * + * @param string $folder Folder name + * + * @return rcube_imap_thread Thread data object + */ + function threads_direct($folder) + { + if (!$this->check_connection()) { + return new rcube_result_thread(); } - return $this->icache['threads']; + // get all threads + return $this->conn->thread($folder, $this->threading, + $this->options['skip_deleted'] ? 'UNDELETED' : '', true); } @@ -981,7 +989,7 @@ class rcube_imap extends rcube_storage // use memory less expensive (and quick) method for big result set $index = clone $this->index('', $this->sort_field, $this->sort_order); // get messages uids for one page... - $index->slice($start_msg, min($cnt-$from, $this->page_size)); + $index->slice($from, min($cnt-$from, $this->page_size)); if ($slice) { $index->slice(-$slice, $slice); @@ -1096,16 +1104,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 +1135,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']) { @@ -1176,12 +1188,13 @@ class rcube_imap extends rcube_storage * @param string $folder Folder to get index from * @param string $sort_field Sort column * @param string $sort_order Sort order [ASC, DESC] + * @param bool $no_threads Get not threaded index * * @return rcube_result_index|rcube_result_thread List of messages (UIDs) */ - public function index($folder = '', $sort_field = NULL, $sort_order = NULL) + public function index($folder = '', $sort_field = NULL, $sort_order = NULL, $no_threads = false) { - if ($this->threading) { + if (!$no_threads && $this->threading) { return $this->thread_index($folder, $sort_field, $sort_order); } @@ -1240,17 +1253,13 @@ class rcube_imap extends rcube_storage * @param string $folder Folder to get index from * @param string $sort_field Sort column * @param string $sort_order Sort order [ASC, DESC] - * @param bool $skip_cache Disables cache usage * * @return rcube_result_index Sorted list of message UIDs */ - public function index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true) + public function index_direct($folder, $sort_field = null, $sort_order = null) { - if (!$skip_cache && ($mcache = $this->get_mcache_engine())) { - $index = $mcache->get_index($folder, $sort_field, $sort_order); - } // use message index sort as default sorting - else if (!$sort_field) { + if (!$sort_field) { // use search result from count() if possible if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx']->get_parameters('ALL') !== null @@ -1311,7 +1320,7 @@ class rcube_imap extends rcube_storage } else { // get all threads (default sort order) - $threads = $this->fetch_threads($folder); + $threads = $this->threads($folder); } $this->set_sort_order($sort_field, $sort_order); @@ -1322,9 +1331,10 @@ class rcube_imap extends rcube_storage /** - * Sort threaded result, using THREAD=REFS method + * Sort threaded result, using THREAD=REFS method if available. + * If not, use any method and re-sort the result in THREAD=REFS way. * - * @param rcube_result_thread $threads Threads result set + * @param rcube_result_thread $threads Threads result set */ protected function sort_threads($threads) { @@ -1336,17 +1346,16 @@ class rcube_imap extends rcube_storage // THREAD=REFERENCES: sorting by sent date of root message // THREAD=REFS: sorting by the most recent date in each thread - if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) { - $index = $this->index_direct($this->folder, $this->sort_field, $this->sort_order, false); + if ($this->get_capability('THREAD') != 'REFS') { + $sortby = $this->sort_field ? $this->sort_field : 'date'; + $index = $this->index($this->folder, $sortby, $this->sort_order, true); if (!$index->is_empty()) { $threads->sort($index); } } - else { - if ($this->sort_order != $threads->get_parameters('ORDER')) { - $threads->revert(); - } + else if ($this->sort_order != $threads->get_parameters('ORDER')) { + $threads->revert(); } } @@ -1419,8 +1428,6 @@ class rcube_imap extends rcube_storage */ protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL) { - $orig_criteria = $criteria; - if (!$this->check_connection()) { if ($this->threading) { return new rcube_result_thread(); @@ -1634,9 +1641,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 +1673,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; @@ -2317,10 +2340,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); } @@ -2710,7 +2730,7 @@ class rcube_imap extends rcube_storage // filter folders list according to rights requirements if ($rights && $this->get_capability('ACL')) { - $a_folders = $this->filter_rights($a_folders, $rights); + $a_mboxes = $this->filter_rights($a_mboxes, $rights); } // filter folders and sort them @@ -2766,7 +2786,6 @@ class rcube_imap extends rcube_storage */ private function list_folders_update(&$result, $type = null) { - $delim = $this->get_hierarchy_delimiter(); $namespace = $this->get_namespace(); $search = array(); @@ -3355,7 +3374,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(); @@ -3365,7 +3383,7 @@ class rcube_imap extends rcube_storage $headers = array_merge($headers, $this->all_headers); } - return implode(' ', array_unique($headers)); + return $headers; } @@ -3678,7 +3696,7 @@ class rcube_imap extends rcube_storage { if ($this->caching && !$this->cache) { $rcube = rcube::get_instance(); - $ttl = $rcube->config->get('message_cache_lifetime', '10d'); + $ttl = $rcube->config->get('imap_cache_ttl', '10d'); $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl); } @@ -3726,21 +3744,6 @@ class rcube_imap extends rcube_storage } } - /** - * Delete outdated cache entries - */ - public function expunge_cache() - { - if ($this->mcache) { - $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d'); - $this->mcache->expunge($ttl); - } - - if ($this->cache) { - $this->cache->expunge(); - } - } - /* -------------------------------- * message caching methods @@ -3774,8 +3777,9 @@ class rcube_imap extends rcube_storage if ($this->messages_caching && !$this->mcache) { $rcube = rcube::get_instance(); if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) { + $ttl = $rcube->config->get('messages_cache_ttl', '10d'); $this->mcache = new rcube_imap_cache( - $dbh, $this, $userid, $this->options['skip_deleted']); + $dbh, $this, $userid, $this->options['skip_deleted'], $ttl); } } @@ -3797,6 +3801,15 @@ class rcube_imap extends rcube_storage } + /** + * Delete outdated cache entries + */ + function cache_gc() + { + rcube_imap_cache::gc(); + } + + /* -------------------------------- * protected methods * --------------------------------*/ @@ -3830,7 +3843,7 @@ class rcube_imap extends rcube_storage $delimiter = $this->get_hierarchy_delimiter(); // find default folders and skip folders starting with '.' - foreach ($a_folders as $i => $folder) { + foreach ($a_folders as $folder) { if ($folder[0] == '.') { continue; } @@ -4090,9 +4103,9 @@ class rcube_imap extends rcube_storage return $this->index($folder, $sort_field, $sort_order); } - public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true) + public function message_index_direct($folder, $sort_field = null, $sort_order = null) { - return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache); + return $this->index_direct($folder, $sort_field, $sort_order); } public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php index f33ac076c..061ac546d 100644 --- a/program/lib/Roundcube/rcube_imap_cache.php +++ b/program/lib/Roundcube/rcube_imap_cache.php @@ -49,6 +49,13 @@ class rcube_imap_cache private $userid; /** + * Expiration time in seconds + * + * @var int + */ + private $ttl; + + /** * Internal (in-memory) cache * * @var array @@ -83,13 +90,25 @@ class rcube_imap_cache /** * Object constructor. + * + * @param rcube_db $db DB handler + * @param rcube_imap $imap IMAP handler + * @param int $userid User identifier + * @param bool $skip_deleted skip_deleted flag + * @param string $ttl Expiration time of memcache/apc items + * */ - function __construct($db, $imap, $userid, $skip_deleted) + function __construct($db, $imap, $userid, $skip_deleted, $ttl=0) { + // convert ttl string to seconds + $ttl = get_offset_sec($ttl); + if ($ttl > 2592000) $ttl = 2592000; + $this->db = $db; $this->imap = $imap; $this->userid = $userid; $this->skip_deleted = $skip_deleted; + $this->ttl = $ttl; } @@ -215,9 +234,7 @@ class rcube_imap_cache * Return messages thread. * If threaded index doesn't exist or is invalid, will be updated. * - * @param string $mailbox Folder name - * @param string $sort_field Sorting column - * @param string $sort_order Sorting order (ASC|DESC) + * @param string $mailbox Folder name * * @return array Messages threaded index */ @@ -256,19 +273,11 @@ class rcube_imap_cache if ($index === null) { // Get mailbox data (UIDVALIDITY, counters, etc.) for status check $mbox_data = $this->imap->folder_data($mailbox); - - if ($mbox_data['EXISTS']) { - // get all threads (default sort order) - $threads = $this->imap->fetch_threads($mailbox, true); - } - else { - $threads = new rcube_result_thread($mailbox, '* THREAD'); - } - - $index['object'] = $threads; + // Get THREADS result + $index['object'] = $this->get_thread_data($mailbox, $mbox_data); // insert/update - $this->add_thread_row($mailbox, $threads, $mbox_data, $exists); + $this->add_thread_row($mailbox, $index['object'], $mbox_data, $exists); } $this->icache[$mailbox]['thread'] = $index; @@ -407,8 +416,8 @@ class rcube_imap_cache return; } - $msg = serialize($this->db->encode(clone $message)); $flags = 0; + $msg = clone $message; if (!empty($message->flags)) { foreach ($this->flags as $idx => $flag) { @@ -417,30 +426,49 @@ class rcube_imap_cache } } } + unset($msg->flags); + $msg = $this->db->encode($msg, true); // update cache record (even if it exists, the update // here will work as select, assume row exist if affected_rows=0) if (!$force) { $res = $this->db->query( "UPDATE ".$this->db->table_name('cache_messages') - ." SET flags = ?, data = ?, changed = ".$this->db->now() + ." SET flags = ?, data = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') ." WHERE user_id = ?" ." AND mailbox = ?" ." AND uid = ?", $flags, $msg, $this->userid, $mailbox, (int) $message->uid); - if ($this->db->affected_rows()) { + if ($this->db->affected_rows($res)) { return; } } + $this->db->set_option('ignore_key_errors', true); + // insert new record - $this->db->query( + $res = $this->db->query( "INSERT INTO ".$this->db->table_name('cache_messages') - ." (user_id, mailbox, uid, flags, changed, data)" - ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)", + ." (user_id, mailbox, uid, flags, expires, data)" + ." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)", $this->userid, $mailbox, (int) $message->uid, $flags, $msg); + + // race-condition, insert failed so try update (#1489146) + // thanks to ignore_key_errors "duplicate row" errors will be ignored + if ($force && !$res && !$this->db->is_error($res)) { + $this->db->query( + "UPDATE ".$this->db->table_name('cache_messages') + ." SET expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') + .", flags = ?, data = ?" + ." WHERE user_id = ?" + ." AND mailbox = ?" + ." AND uid = ?", + $flags, $msg, $this->userid, $mailbox, (int) $message->uid); + } + + $this->db->set_option('ignore_key_errors', false); } @@ -481,11 +509,11 @@ class rcube_imap_cache $this->db->query( "UPDATE ".$this->db->table_name('cache_messages') - ." SET changed = ".$this->db->now() + ." SET expires = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .", 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); } @@ -604,23 +632,21 @@ class rcube_imap_cache /** - * Delete cache entries older than TTL - * - * @param string $ttl Lifetime of message cache entries + * Delete expired cache entries */ - function expunge($ttl) + static function gc() { - // get expiration timestamp - $ts = get_offset_time($ttl, -1); + $rcube = rcube::get_instance(); + $db = $rcube->get_dbh(); - $this->db->query("DELETE FROM ".$this->db->table_name('cache_messages') - ." WHERE changed < " . $this->db->fromunixtime($ts)); + $db->query("DELETE FROM ".$db->table_name('cache_messages') + ." WHERE expires < " . $db->now()); - $this->db->query("DELETE FROM ".$this->db->table_name('cache_index') - ." WHERE changed < " . $this->db->fromunixtime($ts)); + $db->query("DELETE FROM ".$db->table_name('cache_index') + ." WHERE expires < " . $db->now()); - $this->db->query("DELETE FROM ".$this->db->table_name('cache_thread') - ." WHERE changed < " . $this->db->fromunixtime($ts)); + $db->query("DELETE FROM ".$db->table_name('cache_thread') + ." WHERE expires < " . $db->now()); } @@ -639,7 +665,7 @@ class rcube_imap_cache if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $data = explode('@', $sql_arr['data']); - $index = @unserialize($data[0]); + $index = $this->db->decode($data[0], true); unset($data[0]); if (empty($index)) { @@ -676,7 +702,7 @@ class rcube_imap_cache if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $data = explode('@', $sql_arr['data']); - $thread = @unserialize($data[0]); + $thread = $this->db->decode($data[0], true); unset($data[0]); if (empty($thread)) { @@ -702,7 +728,7 @@ class rcube_imap_cache $data, $mbox_data = array(), $exists = false, $modseq = null) { $data = array( - serialize($data), + $this->db->encode($data, true), $sort_field, (int) $this->skip_deleted, (int) $mbox_data['UIDVALIDITY'], @@ -712,20 +738,38 @@ class rcube_imap_cache $data = implode('@', $data); if ($exists) { - $sql_result = $this->db->query( + $res = $this->db->query( "UPDATE ".$this->db->table_name('cache_index') - ." SET data = ?, valid = 1, changed = ".$this->db->now() + ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') ." WHERE user_id = ?" ." AND mailbox = ?", $data, $this->userid, $mailbox); + + if ($this->db->affected_rows($res)) { + return; + } } - else { - $sql_result = $this->db->query( - "INSERT INTO ".$this->db->table_name('cache_index') - ." (user_id, mailbox, data, valid, changed)" - ." VALUES (?, ?, ?, 1, ".$this->db->now().")", - $this->userid, $mailbox, $data); + + $this->db->set_option('ignore_key_errors', true); + + $res = $this->db->query( + "INSERT INTO ".$this->db->table_name('cache_index') + ." (user_id, mailbox, valid, expires, data)" + ." VALUES (?, ?, 1, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .", ?)", + $this->userid, $mailbox, $data); + + // race-condition, insert failed so try update (#1489146) + // thanks to ignore_key_errors "duplicate row" errors will be ignored + if (!$exists && !$res && !$this->db->is_error($res)) { + $res = $this->db->query( + "UPDATE ".$this->db->table_name('cache_index') + ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') + ." WHERE user_id = ?" + ." AND mailbox = ?", + $data, $this->userid, $mailbox); } + + $this->db->set_option('ignore_key_errors', false); } @@ -735,28 +779,48 @@ class rcube_imap_cache private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false) { $data = array( - serialize($data), + $this->db->encode($data, true), (int) $this->skip_deleted, (int) $mbox_data['UIDVALIDITY'], (int) $mbox_data['UIDNEXT'], ); $data = implode('@', $data); + $expires = ($this->ttl ? $this->db->now($this->ttl) : 'NULL'); + if ($exists) { - $sql_result = $this->db->query( + $res = $this->db->query( "UPDATE ".$this->db->table_name('cache_thread') - ." SET data = ?, changed = ".$this->db->now() + ." SET data = ?, expires = $expires" ." WHERE user_id = ?" ." AND mailbox = ?", $data, $this->userid, $mailbox); + + if ($this->db->affected_rows($res)) { + return; + } } - else { - $sql_result = $this->db->query( - "INSERT INTO ".$this->db->table_name('cache_thread') - ." (user_id, mailbox, data, changed)" - ." VALUES (?, ?, ?, ".$this->db->now().")", - $this->userid, $mailbox, $data); + + $this->db->set_option('ignore_key_errors', true); + + $res = $this->db->query( + "INSERT INTO ".$this->db->table_name('cache_thread') + ." (user_id, mailbox, expires, data)" + ." VALUES (?, ?, $expires, ?)", + $this->userid, $mailbox, $data); + + // race-condition, insert failed so try update (#1489146) + // thanks to ignore_key_errors "duplicate row" errors will be ignored + if (!$exists && !$res && !$this->db->is_error($res)) { + $this->db->query( + "UPDATE ".$this->db->table_name('cache_thread') + ." SET expires = $expires, data = ?" + ." WHERE user_id = ?" + ." AND mailbox = ?", + $data, $this->userid, $mailbox); } + + $this->db->set_option('ignore_key_errors', false); } @@ -983,7 +1047,7 @@ class rcube_imap_cache $uids, true, array('FLAGS'), $index['modseq'], $qresync); if (!empty($result)) { - foreach ($result as $id => $msg) { + foreach ($result as $msg) { $uid = $msg->uid; // Remove deleted message if ($this->skip_deleted && !empty($msg->flags['DELETED'])) { @@ -1004,7 +1068,7 @@ class rcube_imap_cache $this->db->query( "UPDATE ".$this->db->table_name('cache_messages') - ." SET flags = ?, changed = ".$this->db->now() + ." SET flags = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') ." WHERE user_id = ?" ." AND mailbox = ?" ." AND uid = ?" @@ -1032,17 +1096,18 @@ class rcube_imap_cache } } - // Invalidate thread index (?) - if (!$index['valid']) { - $this->remove_thread($mailbox); - } - $sort_field = $index['sort_field']; $sort_order = $index['object']->get_parameters('ORDER'); $exists = true; // Validate index if (!$this->validate($mailbox, $index, $exists)) { + // Invalidate (remove) thread index + // if $exists=false it was already removed in validate() + if ($exists) { + $this->remove_thread($mailbox); + } + // Update index $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data); } @@ -1067,7 +1132,7 @@ class rcube_imap_cache */ private function build_message($sql_arr) { - $message = $this->db->decode(unserialize($sql_arr['data'])); + $message = $this->db->decode($sql_arr['data'], true); if ($message) { $message->flags = array(); @@ -1150,6 +1215,25 @@ class rcube_imap_cache return $index; } + + + /** + * Fetches thread data from IMAP server + */ + private function get_thread_data($mailbox, $mbox_data = array()) + { + if (empty($mbox_data)) { + $mbox_data = $this->imap->folder_data($mailbox); + } + + if ($mbox_data['EXISTS']) { + // get all threads (default sort order) + return $this->imap->threads_direct($mailbox); + } + + return new rcube_result_thread($mailbox, '* THREAD'); + } + } // for backward compat. diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index b9a796c33..292b932e1 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -72,6 +72,8 @@ class rcube_imap_generic const COMMAND_CAPABILITY = 2; const COMMAND_LASTLINE = 4; + const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n + /** * Object constructor */ @@ -746,7 +748,7 @@ class rcube_imap_generic } if ($this->prefs['timeout'] <= 0) { - $this->prefs['timeout'] = ini_get('default_socket_timeout'); + $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout'))); } // Connect @@ -1065,8 +1067,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 */ @@ -1077,17 +1079,20 @@ class rcube_imap_generic } if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE'); + $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } // 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 @@ -1649,7 +1654,6 @@ class rcube_imap_generic } if (!empty($criteria)) { - $modseq = stripos($criteria, 'MODSEQ') !== false; $params .= ($params ? ' ' : '') . $criteria; } else { @@ -1788,7 +1792,6 @@ class rcube_imap_generic if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { $flags = explode(' ', strtoupper($matches[1])); if (in_array('\\DELETED', $flags)) { - $deleted[$id] = $id; continue; } } @@ -1933,7 +1936,7 @@ class rcube_imap_generic } if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); + $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } @@ -1980,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 @@ -1995,19 +1997,45 @@ class rcube_imap_generic } if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); + $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); 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; } /** @@ -2144,7 +2172,7 @@ class rcube_imap_generic // create array with header field:data if (!empty($headers)) { $headers = explode("\n", trim($headers)); - foreach ($headers as $hid => $resln) { + foreach ($headers as $resln) { if (ord($resln[0]) <= 32) { $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln); } else { @@ -2152,7 +2180,7 @@ class rcube_imap_generic } } - while (list($lines_key, $str) = each($lines)) { + foreach ($lines as $str) { list($field, $string) = explode(':', $str, 2); $field = strtolower($field); @@ -2237,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); } @@ -2418,6 +2475,7 @@ class rcube_imap_generic $key = $this->nextTag(); $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; $result = false; + $found = false; // send request if (!$this->putLine($request)) { @@ -2437,18 +2495,25 @@ class rcube_imap_generic break; } - if (!preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { + // skip irrelevant untagged responses (we have a result already) + if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { continue; } $line = $m[2]; - $last = substr($line, -1); // handle one line response - if ($line[0] == '(' && $last == ')') { + if ($line[0] == '(' && substr($line, -1) == ')') { // tokenize content inside brackets - $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\$)/', '', $line)); - $result = count($tokens) == 1 ? $tokens[0] : false; + $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line)); + + for ($i=0; $i<count($tokens); $i+=2) { + if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) { + $result = $tokens[$i+1]; + $found = true; + break; + } + } if ($result !== false) { if ($mode == 1) { @@ -2466,6 +2531,7 @@ class rcube_imap_generic else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { $bytes = (int) $m[1]; $prev = ''; + $found = true; while ($bytes > 0) { $line = $this->readLine(8192); @@ -3474,7 +3540,7 @@ class rcube_imap_generic if (is_array($element)) { reset($element); - while (list($key, $value) = each($element)) { + foreach ($element as $value) { $string .= ' ' . self::r_implode($value); } } @@ -3502,7 +3568,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) { @@ -3610,8 +3676,20 @@ class rcube_imap_generic */ static function strToTime($date) { - // support non-standard "GMTXXXX" literal - $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); + // Clean malformed data + $date = preg_replace( + array( + '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal + '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters + '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names + ), + array( + '\\1', + '', + '', + ), $date); + + $date = trim($date); // if date parsing fails, we have a date in non-rfc format // remove token from the end and try again @@ -3681,9 +3759,10 @@ class rcube_imap_generic /** * Set the value of the debugging flag. * - * @param boolean $debug New value for the debugging flag. + * @param boolean $debug New value for the debugging flag. + * @param callback $handler Logging handler function * - * @since 0.5-stable + * @since 0.5-stable */ function setDebug($debug, $handler = null) { @@ -3694,12 +3773,18 @@ class rcube_imap_generic /** * Write the given debug text to the current debug output handler. * - * @param string $message Debug mesage text. + * @param string $message Debug mesage text. * - * @since 0.5-stable + * @since 0.5-stable */ private function debug($message) { + if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { + $diff = $len - self::DEBUG_LINE_LENGTH; + $message = substr($message, 0, self::DEBUG_LINE_LENGTH) + . "... [truncated $diff bytes]"; + } + if ($this->resourceid) { $message = sprintf('[%s] %s', $this->resourceid, $message); } diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index c3c0533f8..8877af322 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -174,7 +174,7 @@ class rcube_ldap extends rcube_addressbook // Build sub_fields filter if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) { $this->sub_filter = ''; - foreach ($this->prop['sub_fields'] as $attr => $class) { + foreach ($this->prop['sub_fields'] as $class) { if (!empty($class)) { $class = is_array($class) ? array_pop($class) : $class; $this->sub_filter .= '(objectClass=' . $class . ')'; @@ -190,8 +190,12 @@ class rcube_ldap extends rcube_addressbook $this->mail_domain = $mail_domain; // initialize cache - $rcube = rcube::get_instance(); - $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600); + $rcube = rcube::get_instance(); + $cache_type = $rcube->config->get('ldap_cache', 'db'); + $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m'); + $cache_name = 'LDAP.' . asciiwords($this->prop['name']); + + $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl); // determine which attributes to fetch $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes); @@ -900,7 +904,6 @@ class rcube_ldap extends rcube_addressbook $mail_field = $this->fieldmap['email']; // try to extract surname and firstname from displayname - $reverse_map = array_flip($this->fieldmap); $name_parts = preg_split('/[\s,.]+/', $save_data['name']); if ($sn_field && $missing[$sn_field]) { @@ -972,7 +975,7 @@ class rcube_ldap extends rcube_addressbook // Remove attributes that need to be added separately (child objects) $xfields = array(); if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) { - foreach ($this->prop['sub_fields'] as $xf => $xclass) { + foreach (array_keys($this->prop['sub_fields']) as $xf) { if (!empty($newentry[$xf])) { $xfields[$xf] = $newentry[$xf]; unset($newentry[$xf]); @@ -1035,7 +1038,7 @@ class rcube_ldap extends rcube_addressbook } } - foreach ($this->fieldmap as $col => $fld) { + foreach ($this->fieldmap as $fld) { if ($fld) { $val = $ldap_data[$fld]; $old = $old_data[$fld]; @@ -1261,6 +1264,10 @@ class rcube_ldap extends rcube_addressbook */ protected function add_autovalues(&$attrs) { + if (empty($this->prop['autovalues'])) { + return; + } + $attrvals = array(); foreach ($attrs as $k => $v) { $attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v; @@ -1268,13 +1275,24 @@ class rcube_ldap extends rcube_addressbook foreach ((array)$this->prop['autovalues'] as $lf => $templ) { if (empty($attrs[$lf])) { - // replace {attr} placeholders with concrete attribute values - $templ = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals)); + if (strpos($templ, '(') !== false) { + // replace {attr} placeholders with (escaped!) attribute values to be safely eval'd + $code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals))); + $fn = create_function('', "return ($code);"); + if (!$fn) { + rcube::raise_error(array( + 'code' => 505, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Expression parse error on: ($code)"), true, false); + continue; + } - if (strpos($templ, '(') !== false) - $attrs[$lf] = eval("return ($templ);"); - else - $attrs[$lf] = $templ; + $attrs[$lf] = $fn(); + } + else { + // replace {attr} placeholders with concrete attribute values + $attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals)); + } } } } @@ -1492,9 +1510,14 @@ class rcube_ldap extends rcube_addressbook * List all active contact groups of this source * * @param string Optional search string to match group name + * @param int Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * * @return array Indexed list of contact groups, each a hash array */ - function list_groups($search = null) + function list_groups($search = null, $mode = 0) { if (!$this->groups) return array(); @@ -1506,10 +1529,10 @@ class rcube_ldap extends rcube_addressbook $groups = array(); if ($search) { - $search = mb_strtolower($search); foreach ($group_cache as $group) { - if (strpos(mb_strtolower($group['name']), $search) !== false) + if ($this->compare_search_value('name', $group['name'], $search, $mode)) { $groups[] = $group; + } } } else @@ -1739,9 +1762,10 @@ class rcube_ldap extends rcube_addressbook /** * Add the given contact records the a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added + * @param string Group identifier + * @param array|string List of contact identifiers to be added + * + * @return int Number of contacts added */ function add_to_group($group_id, $contact_ids) { @@ -1753,8 +1777,8 @@ class rcube_ldap extends rcube_addressbook $member_attr = $group_cache[$group_id]['member_attr']; $group_dn = $group_cache[$group_id]['dn']; + $new_attrs = array(); - $new_attrs = array(); foreach ($contact_ids as $id) $new_attrs[$member_attr][] = self::dn_decode($id); @@ -1771,19 +1795,23 @@ class rcube_ldap extends rcube_addressbook /** * Remove the given contact records from a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members + * @param string Group identifier + * @param array|string List of contact identifiers to be removed + * + * @return int Number of deleted group members */ function remove_from_group($group_id, $contact_ids) { if (($group_cache = $this->cache->get('groups')) === null) $group_cache = $this->_fetch_groups(); + if (!is_array($contact_ids)) + $contact_ids = explode(',', $contact_ids); + $member_attr = $group_cache[$group_id]['member_attr']; $group_dn = $group_cache[$group_id]['dn']; - $del_attrs = array(); + foreach ($contact_ids as $id) $del_attrs[$member_attr][] = self::dn_decode($id); diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php index e0c3e3475..797ca185e 100644 --- a/program/lib/Roundcube/rcube_message.php +++ b/program/lib/Roundcube/rcube_message.php @@ -85,15 +85,16 @@ class rcube_message $this->headers = $this->storage->get_message($uid); - if (!$this->headers) + if (!$this->headers) { return; + } $this->mime = new rcube_mime($this->headers->charset); - $this->subject = $this->mime->decode_mime_string($this->headers->subject); + $this->subject = $this->headers->get('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'), @@ -125,15 +126,11 @@ class rcube_message */ public function get_header($name, $raw = false) { - if (empty($this->headers)) + if (empty($this->headers)) { return null; + } - if ($this->headers->$name) - $value = $this->headers->$name; - else if ($this->headers->others[$name]) - $value = $this->headers->others[$name]; - - return $raw ? $value : $this->mime->decode_header($value); + return $this->headers->get($name, !$raw); } @@ -144,8 +141,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; } @@ -153,12 +149,13 @@ class rcube_message * Compose a valid URL for getting a message part * * @param string $mime_id Part MIME-ID + * @param mixed $embed Mimetype class for parts to be embedded * @return string URL or false if part does not exist */ public function get_part_url($mime_id, $embed = false) { if ($this->mime_parts[$mime_id]) - return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1' : ''); + return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1&_mimeclass=' . $embed : ''); else return false; } @@ -194,39 +191,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; + } } } @@ -322,7 +362,7 @@ class rcube_message // 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)); + list($headers, ) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768)); $structure->headers = rcube_mime::parse_headers($headers); } } @@ -330,7 +370,8 @@ class rcube_message $mimetype = $structure->mimetype; // show message headers - if ($recursive && is_array($structure->headers) && (isset($structure->headers['subject']) || isset($structure->headers['from']))) { + if ($recursive && is_array($structure->headers) && + (isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])) { $c = new stdClass; $c->type = 'headers'; $c->headers = $structure->headers; @@ -444,14 +485,6 @@ 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]; @@ -468,6 +501,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 @@ -525,10 +569,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') { @@ -604,8 +644,8 @@ class rcube_message $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) + $part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary); + 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; @@ -745,7 +785,7 @@ class rcube_message $uupart->size = strlen($uupart->body); $uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid; - $ctype = rcube_mime::content_type($uupart->body, $uupart->filename, 'application/octet-stream', true); + $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true); $uupart->mimetype = $ctype; list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype); diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php index 274ae7f9f..2c5e2b6c8 100644 --- a/program/lib/Roundcube/rcube_message_header.php +++ b/program/lib/Roundcube/rcube_message_header.php @@ -215,7 +215,12 @@ class rcube_message_header $value = $this->others[$name]; } - return $decode ? rcube_mime::decode_header($value, $this->charset) : $value; + if ($decode) { + $value = rcube_mime::decode_header($value, $this->charset); + $value = rcube_charset::clean($value); + } + + return $value; } /** diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index 2f24a1bb3..572540f47 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -127,10 +127,11 @@ class rcube_mime * @param int $max List only this number of addresses * @param boolean $decode Decode address strings * @param string $fallback Fallback charset if none specified + * @param boolean $addronly Return flat array with e-mail addresses only * - * @return array Indexed list of addresses + * @return array Indexed list of addresses */ - static function decode_address_list($input, $max = null, $decode = true, $fallback = null) + static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false) { $a = self::parse_address_list($input, $decode, $fallback); $out = array(); @@ -145,20 +146,21 @@ class rcube_mime foreach ($a as $val) { $j++; $address = trim($val['address']); - $name = trim($val['name']); - if ($name && $address && $name != $address) - $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address); - else if ($address) - $string = $address; - else if ($name) - $string = $name; - - $out[$j] = array( - 'name' => $name, - 'mailto' => $address, - 'string' => $string - ); + if ($addronly) { + $out[$j] = $address; + } + else { + $name = trim($val['name']); + if ($name && $address && $name != $address) + $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address); + else if ($address) + $string = $address; + else if ($name) + $string = $name; + + $out[$j] = array('name' => $name, 'mailto' => $address, 'string' => $string); + } if ($max && $j==$max) break; @@ -359,6 +361,11 @@ class rcube_mime $address = $m[1]; $name = ''; } + // special case (#1489092) + else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) { + $address = 'MAILER-DAEMON'; + $name = substr($val, 0, -strlen($m[1])); + } else { $name = $val; } @@ -476,9 +483,10 @@ class rcube_mime $q_level = 0; foreach ($text as $idx => $line) { - if ($line[0] == '>') { - // remove quote chars, store level in $q - $line = preg_replace('/^>+/', '', $line, -1, $q); + if (preg_match('/^(>+)/', $line, $m)) { + // remove quote chars + $q = strlen($m[1]); + $line = preg_replace('/^>+/', '', $line); // remove (optional) space-staffing $line = preg_replace('/^ /', '', $line); @@ -541,9 +549,10 @@ class rcube_mime foreach ($text as $idx => $line) { if ($line != '-- ') { - if ($line[0] == '>') { - // remove quote chars, store level in $level - $line = preg_replace('/^>+/', '', $line, -1, $level); + if (preg_match('/^(>+)/', $line, $m)) { + // remove quote chars + $level = strlen($m[1]); + $line = preg_replace('/^>+/', '', $line); // remove (optional) space-staffing and spaces before the line end $line = preg_replace('/(^ | +$)/', '', $line); $prefix = str_repeat('>', $level) . ' '; @@ -564,81 +573,122 @@ class rcube_mime /** - * Improved wordwrap function. + * Improved wordwrap function with multibyte support. + * The code is based on Zend_Text_MultiByte::wordWrap(). * - * @param string $string Text to wrap - * @param int $width Line width - * @param string $break Line separator - * @param bool $cut Enable to cut word - * @param string $charset Charset of $string + * @param string $string Text to wrap + * @param int $width Line width + * @param string $break Line separator + * @param bool $cut Enable to cut word + * @param string $charset Charset of $string + * @param bool $wrap_quoted When enabled quoted lines will not be wrapped * * @return string Text */ - public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null) + public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true) { - if ($charset && function_exists('mb_internal_encoding')) { + // Note: Never try to use iconv instead of mbstring functions here + // Iconv's substr/strlen are 100x slower (#1489113) + + if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) { mb_internal_encoding($charset); } - $para = preg_split('/\r?\n/', $string); - $string = ''; + // Convert \r\n to \n, this is our line-separator + $string = str_replace("\r\n", "\n", $string); + $separator = "\n"; // must be 1 character length + $result = array(); - while (count($para)) { - $line = array_shift($para); - if ($line[0] == '>') { - $string .= $line . (count($para) ? $break : ''); - continue; - } + while (($stringLength = mb_strlen($string)) > 0) { + $breakPos = mb_strpos($string, $separator, 0); - $list = explode(' ', $line); - $len = 0; - while (count($list)) { - $line = array_shift($list); - $l = mb_strlen($line); - $newlen = $len + $l + ($len ? 1 : 0); + // quoted line (do not wrap) + if ($wrap_quoted && $string[0] == '>') { + if ($breakPos === $stringLength - 1 || $breakPos === false) { + $subString = $string; + $cutLength = null; + } + else { + $subString = mb_substr($string, 0, $breakPos); + $cutLength = $breakPos + 1; + } + } + // next line found and current line is shorter than the limit + else if ($breakPos !== false && $breakPos < $width) { + if ($breakPos === $stringLength - 1) { + $subString = $string; + $cutLength = null; + } + else { + $subString = mb_substr($string, 0, $breakPos); + $cutLength = $breakPos + 1; + } + } + else { + $subString = mb_substr($string, 0, $width); - if ($newlen <= $width) { - $string .= ($len ? ' ' : '').$line; - $len += (1 + $l); + // last line + if ($breakPos === false && $subString === $string) { + $cutLength = null; } else { - if ($l > $width) { - if ($cut) { - $start = 0; - while ($l) { - $str = mb_substr($line, $start, $width); - $strlen = mb_strlen($str); - $string .= ($len ? $break : '').$str; - $start += $strlen; - $l -= $strlen; - $len = $strlen; + $nextChar = mb_substr($string, $width, 1); + + if ($nextChar === ' ' || $nextChar === $separator) { + $afterNextChar = mb_substr($string, $width + 1, 1); + + if ($afterNextChar === false) { + $subString .= $nextChar; + } + + $cutLength = mb_strlen($subString) + 1; + } + else { + $spacePos = mb_strrpos($subString, ' ', 0); + + if ($spacePos !== false) { + $subString = mb_substr($subString, 0, $spacePos); + $cutLength = $spacePos + 1; + } + else if ($cut === false && $breakPos === false) { + $subString = $string; + $cutLength = null; + } + else if ($cut === false) { + $spacePos = mb_strpos($string, ' ', 0); + + if ($spacePos !== false && $spacePos < $breakPos) { + $subString = mb_substr($string, 0, $spacePos); + $cutLength = $spacePos + 1; + } + else { + $subString = mb_substr($string, 0, $breakPos); + $cutLength = $breakPos + 1; } } else { - $string .= ($len ? $break : '').$line; - if (count($list)) { - $string .= $break; - } - $len = 0; + $subString = mb_substr($subString, 0, $width); + $cutLength = $width; } } - else { - $string .= $break.$line; - $len = $l; - } } } - if (count($para)) { - $string .= $break; + $result[] = $subString; + + if ($cutLength !== null) { + $string = mb_substr($string, $cutLength, ($stringLength - $cutLength)); + } + else { + break; } } - if ($charset && function_exists('mb_internal_encoding')) { + if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) { mb_internal_encoding(RCUBE_CHARSET); } - return $string; + return implode($break, $result); } @@ -671,7 +721,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 @@ -717,21 +776,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/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'; + 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); + if (@is_readable($fp)) { + $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); break; } } @@ -753,11 +818,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/jpeg', '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 b8ae86cf6..7ccf9a02e 100644 --- a/program/lib/Roundcube/rcube_output.php +++ b/program/lib/Roundcube/rcube_output.php @@ -162,7 +162,7 @@ abstract class rcube_output header("Cache-Control: private, must-revalidate"); } else { - header("Cache-Control: private, no-cache, must-revalidate, post-check=0, pre-check=0"); + header("Cache-Control: private, no-cache, no-store, must-revalidate, post-check=0, pre-check=0"); header("Pragma: no-cache"); } } diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php index 66e77cce2..3153a8410 100644 --- a/program/lib/Roundcube/rcube_plugin.php +++ b/program/lib/Roundcube/rcube_plugin.php @@ -60,6 +60,14 @@ abstract class rcube_plugin */ 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; @@ -84,6 +92,16 @@ abstract class rcube_plugin abstract function init(); /** + * Provide information about this + * + * @return array Meta information about a plugin or false if not implemented + */ + public static function info() + { + return false; + } + + /** * Attempt to load the given plugin which is required for the current plugin * * @param string Plugin name @@ -209,7 +227,7 @@ abstract class rcube_plugin $rcube->load_language($lang, $add); // add labels to client - if ($add2client) { + if ($add2client && method_exists($rcube->output, 'add_label')) { if (is_array($add2client)) { $js_labels = array_map(array($this, 'label_map_callback'), $add2client); } @@ -222,6 +240,24 @@ abstract class rcube_plugin } /** + * Wrapper for add_label() adding the plugin ID as domain + */ + public function add_label() + { + $rcube = rcube::get_instance(); + + if (method_exists($rcube->output, 'add_label')) { + $args = func_get_args(); + if (count($args) == 1 && is_array($args[0])) { + $args = $args[0]; + } + + $args = array_map(array($this, 'label_map_callback'), $args); + $rcube->output->add_label($args); + } + } + + /** * Wrapper for rcube::gettext() adding the plugin ID as domain * * @param string $p Message identifier @@ -237,7 +273,7 @@ abstract class rcube_plugin /** * Register this plugin to be responsible for a specific task * - * @param string $task Task name (only characters [a-z0-9_.-] are allowed) + * @param string $task Task name (only characters [a-z0-9_-] are allowed) */ public function register_task($task) { @@ -372,6 +408,10 @@ abstract class rcube_plugin */ private function label_map_callback($key) { + if (strpos($key, $this->ID.'.') === 0) { + return $key; + } + return $this->ID.'.'.$key; } } diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php index 8a4cce215..33f04eaa5 100644 --- a/program/lib/Roundcube/rcube_plugin_api.php +++ b/program/lib/Roundcube/rcube_plugin_api.php @@ -36,6 +36,7 @@ class rcube_plugin_api public $task = ''; public $output; public $handlers = array(); + public $allowed_prefs = array(); protected $plugins = array(); protected $tasks = array(); @@ -202,6 +203,11 @@ class rcube_plugin_api $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; } } @@ -222,6 +228,119 @@ class rcube_plugin_api } /** + * Get information about a specific plugin. + * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file + * + * @param string Plugin name + * @return array Meta information about a plugin or False if plugin was not found + */ + public function get_info($plugin_name) + { + static $composer_lock, $license_uris = array( + 'Apache' => 'http://www.apache.org/licenses/LICENSE-2.0.html', + 'Apache-2' => 'http://www.apache.org/licenses/LICENSE-2.0.html', + 'Apache-1' => 'http://www.apache.org/licenses/LICENSE-1.0', + 'Apache-1.1' => 'http://www.apache.org/licenses/LICENSE-1.1', + 'GPL' => 'http://www.gnu.org/licenses/gpl.html', + 'GPLv2' => 'http://www.gnu.org/licenses/gpl-2.0.html', + 'GPL-2.0' => 'http://www.gnu.org/licenses/gpl-2.0.html', + 'GPLv3' => 'http://www.gnu.org/licenses/gpl-3.0.html', + 'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html', + 'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html', + 'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html', + 'LGPL' => 'http://www.gnu.org/licenses/lgpl.html', + 'LGPLv2' => 'http://www.gnu.org/licenses/lgpl-2.0.html', + 'LGPLv2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html', + 'LGPLv3' => 'http://www.gnu.org/licenses/lgpl.html', + 'LGPL-2.0' => 'http://www.gnu.org/licenses/lgpl-2.0.html', + 'LGPL-2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html', + 'LGPL-3.0' => 'http://www.gnu.org/licenses/lgpl.html', + 'LGPL-3.0+' => 'http://www.gnu.org/licenses/lgpl.html', + 'BSD' => 'http://opensource.org/licenses/bsd-license.html', + 'BSD-2-Clause' => 'http://opensource.org/licenses/BSD-2-Clause', + 'BSD-3-Clause' => 'http://opensource.org/licenses/BSD-3-Clause', + 'FreeBSD' => 'http://opensource.org/licenses/BSD-2-Clause', + 'MIT' => 'http://www.opensource.org/licenses/mit-license.php', + 'PHP' => 'http://opensource.org/licenses/PHP-3.0', + 'PHP-3' => 'http://www.php.net/license/3_01.txt', + 'PHP-3.0' => 'http://www.php.net/license/3_0.txt', + 'PHP-3.01' => 'http://www.php.net/license/3_01.txt', + ); + + $dir = dir($this->dir); + $fn = unslashify($dir->path) . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php'; + $info = false; + + if (!class_exists($plugin_name)) + include($fn); + + if (class_exists($plugin_name)) + $info = $plugin_name::info(); + + // fall back to composer.json file + if (!$info) { + $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json"; + if (file_exists($composer) && ($json = @json_decode(file_get_contents($composer), true))) { + list($info['vendor'], $info['name']) = explode('/', $json['name']); + $info['license'] = $json['license']; + if ($license_uri = $license_uris[$info['license']]) + $info['license_uri'] = $license_uri; + } + + // read local composer.lock file (once) + if (!isset($composer_lock)) { + $composer_lock = @json_decode(@file_get_contents(INSTALL_PATH . "/composer.lock"), true); + if ($composer_lock['packages']) { + foreach ($composer_lock['packages'] as $i => $package) { + $composer_lock['installed'][$package['name']] = $package; + } + } + } + + // load additional information from local composer.lock file + if ($lock = $composer_lock['installed'][$json['name']]) { + $info['version'] = $lock['version']; + $info['uri'] = $lock['homepage'] ? $lock['homepage'] : $lock['source']['uri']; + $info['src_uri'] = $lock['dist']['uri'] ? $lock['dist']['uri'] : $lock['source']['uri']; + } + } + + // fall back to package.xml file + if (!$info) { + $package = INSTALL_PATH . "/plugins/$plugin_name/package.xml"; + if (file_exists($package) && ($file = file_get_contents($package))) { + $doc = new DOMDocument(); + $doc->loadXML($file); + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('rc', "http://pear.php.net/dtd/package-2.0"); + + // XPaths of plugin metadata elements + $metadata = array( + 'name' => 'string(//rc:package/rc:name)', + 'version' => 'string(//rc:package/rc:version/rc:release)', + 'license' => 'string(//rc:package/rc:license)', + 'license_uri' => 'string(//rc:package/rc:license/@uri)', + 'src_uri' => 'string(//rc:package/rc:srcuri)', + 'uri' => 'string(//rc:package/rc:uri)', + ); + + foreach ($metadata as $key => $path) { + $info[$key] = $xpath->evaluate($path); + } + + // dependent required plugins (can be used, but not included in config) + $deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name'); + for ($i = 0; $i < $deps->length; $i++) { + $dn = $deps->item($i)->nodeValue; + $info['requires'][] = $dn; + } + } + } + + return $info; + } + + /** * Allows a plugin object to register a callback for a certain hook * * @param string $hook Hook name @@ -372,7 +491,7 @@ class rcube_plugin_api /** * Register this plugin to be responsible for a specific task * - * @param string $task Task name (only characters [a-z0-9_.-] are allowed) + * @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) @@ -382,7 +501,7 @@ class rcube_plugin_api return true; } - if ($task != asciiwords($task)) { + if ($task != asciiwords($task, true)) { rcube::raise_error(array('code' => 526, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Invalid task name: $task." diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php index 1aa5d5856..615ec6f0e 100644 --- a/program/lib/Roundcube/rcube_session.php +++ b/program/lib/Roundcube/rcube_session.php @@ -32,6 +32,8 @@ class rcube_session private $ip; private $start; private $changed; + private $time_diff = 0; + private $reloaded = false; private $unsets = array(); private $gc_handlers = array(); private $cookiename = 'roundcube_sessauth'; @@ -41,6 +43,7 @@ class rcube_session private $secret = ''; private $ip_check = false; private $logging = false; + private $storage; private $memcache; @@ -58,11 +61,14 @@ class rcube_session $this->set_lifetime($lifetime); // use memcache backend - if ($config->get('session_storage', 'db') == 'memcache') { + $this->storage = $config->get('session_storage', 'db'); + if ($this->storage == 'memcache') { $this->memcache = rcube::get_instance()->get_memcache(); // set custom functions for PHP session management if memcache is available if ($this->memcache) { + ini_set('session.serialize_handler', 'php'); + session_set_save_handler( array($this, 'open'), array($this, 'close'), @@ -78,7 +84,9 @@ class rcube_session true, true); } } - else { + else if ($this->storage != 'php') { + ini_set('session.serialize_handler', 'php'); + // set custom functions for PHP session management session_set_save_handler( array($this, 'open'), @@ -86,7 +94,23 @@ class rcube_session array($this, 'db_read'), array($this, 'db_write'), array($this, 'db_destroy'), - array($this, 'db_gc')); + array($this, 'gc')); + } + } + + + /** + * Wrapper for session_start() + */ + public function start() + { + session_start(); + + // copy some session properties to object vars + if ($this->storage == 'php') { + $this->key = session_id(); + $this->ip = $_SESSION['__IP']; + $this->changed = $_SESSION['__MTIME']; } } @@ -115,6 +139,25 @@ class rcube_session /** + * Wrapper for session_write_close() + */ + public function write_close() + { + if ($this->storage == 'php') { + $_SESSION['__IP'] = $this->ip; + $_SESSION['__MTIME'] = time(); + } + + session_write_close(); + + // write_close() is called on script shutdown, see rcube::shutdown() + // execute cleanup functionality if enabled by session gc handler + // we do this after closing the session for better performance + $this->gc_shutdown(); + } + + + /** * Read session data from database * * @param string Session ID @@ -124,14 +167,16 @@ class rcube_session public function db_read($key) { $sql_result = $this->db->query( - "SELECT vars, ip, changed FROM ".$this->db->table_name('session') - ." WHERE sess_id = ?", $key); + "SELECT vars, ip, changed, " . $this->db->now() . " AS ts" + . " 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; + $this->time_diff = time() - strtotime($sql_arr['ts']); + $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 : ''; } @@ -151,8 +196,9 @@ class rcube_session */ public function db_write($key, $vars) { - $ts = microtime(true); - $now = $this->db->fromunixtime((int)$ts); + $now = $this->db->now(); + $table = $this->db->table_name('session'); + $ts = microtime(true); // no session row in DB (db_read() returns false) if (!$this->key) { @@ -170,22 +216,19 @@ class rcube_session $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); + $this->db->query("UPDATE $table " + . "SET changed = $now, vars = ? WHERE sess_id = ?", + 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 if ($ts - $this->changed + $this->time_diff > $this->lifetime / 2) { + $this->db->query("UPDATE $table 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); + $this->db->query("INSERT INTO $table (sess_id, vars, ip, created, changed)" + . " VALUES (?, ?, ?, $now, $now)", + $key, base64_encode($vars), (string)$this->ip); } return true; @@ -200,8 +243,18 @@ class rcube_session if ($oldvars !== null) { $a_oldvars = $this->unserialize($oldvars); if (is_array($a_oldvars)) { - foreach ((array)$this->unsets as $k) - unset($a_oldvars[$k]); + // 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))); @@ -235,25 +288,6 @@ class rcube_session /** - * 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 @@ -329,11 +363,11 @@ class rcube_session /** * Execute registered garbage collector routines */ - public function gc() + public function gc($maxlifetime) { - foreach ($this->gc_handlers as $fct) { - call_user_func($fct); - } + // move gc execution to the script shutdown function + // see rcube::shutdown() and rcube_session::write_close() + return $this->gc_enabled = $maxlifetime; } @@ -355,6 +389,25 @@ class rcube_session /** + * Garbage collector handler to run on script shutdown + */ + protected function gc_shutdown() + { + if ($this->gc_enabled) { + // just delete all expired sessions + if ($this->storage == 'db') { + $this->db->query("DELETE FROM " . $this->db->table_name('session') + . " WHERE changed < " . $this->db->now(-$this->gc_enabled)); + } + + foreach ($this->gc_handlers as $fct) { + call_user_func($fct); + } + } + } + + + /** * Generate and set new session id * * @param boolean $destroy If enabled the current session will be destroyed @@ -371,9 +424,32 @@ class rcube_session /** + * 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; + } + + + /** * Unset a session variable * - * @param string Varibale name + * @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) @@ -383,7 +459,16 @@ class rcube_session } $this->unsets[] = $var; - unset($_SESSION[$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; } @@ -415,6 +500,23 @@ class rcube_session session_decode($data); } + /** + * 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 diff --git a/program/lib/Roundcube/rcube_smtp.php b/program/lib/Roundcube/rcube_smtp.php index 5c7d2203c..60b1389ea 100644 --- a/program/lib/Roundcube/rcube_smtp.php +++ b/program/lib/Roundcube/rcube_smtp.php @@ -33,6 +33,8 @@ class rcube_smtp // define headers delimiter const SMTP_MIME_CRLF = "\r\n"; + const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n + /** * SMTP Connection and authentication @@ -119,7 +121,7 @@ class rcube_smtp } // try to connect to server and exit on failure - $result = $this->conn->connect($smtp_timeout); + $result = $this->conn->connect($CONFIG['smtp_timeout']); if (PEAR::isError($result)) { $this->response[] = "Connection failed: ".$result->getMessage(); @@ -327,6 +329,12 @@ class rcube_smtp */ public function debug_handler(&$smtp, $message) { + if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { + $diff = $len - self::DEBUG_LINE_LENGTH; + $message = substr($message, 0, self::DEBUG_LINE_LENGTH) + . "... [truncated $diff bytes]"; + } + rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message)); } @@ -433,9 +441,9 @@ class rcube_smtp $recipients = rcube_utils::explode_quoted_string(',', $recipients); reset($recipients); - while (list($k, $recipient) = each($recipients)) { + foreach ($recipients as $recipient) { $a = rcube_utils::explode_quoted_string(' ', $recipient); - while (list($k2, $word) = each($a)) { + foreach ($a as $word) { if (strpos($word, "@") > 0 && $word[strlen($word)-1] != '"') { $word = preg_replace('/^<|>$/', '', trim($word)); if (in_array($word, $addresses) === false) { diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php index 3d4d3a3d6..60aec500f 100644 --- a/program/lib/Roundcube/rcube_spellchecker.php +++ b/program/lib/Roundcube/rcube_spellchecker.php @@ -31,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; @@ -314,11 +314,6 @@ class rcube_spellchecker if (!$this->plink) { if (!extension_loaded('pspell')) { $this->error = "Pspell extension not available"; - rcube::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => $this->error), true, false); - return; } @@ -372,9 +367,19 @@ class rcube_spellchecker fclose($fp); } + // parse HTTP response + if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) { + $http_status = $m[1]; + if ($http_status != '200') + $this->error = 'HTTP ' . $m[1] . $m[2]; + } + if (!$store) { $this->error = "Empty result from spelling engine"; } + else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m)) { + $this->error = "Error code $m[1] returned"; + } preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER); @@ -588,7 +593,7 @@ class rcube_spellchecker if (empty($plugin['abort'])) { $dict = array(); - $this->rc->db->query( + $sql_result = $this->rc->db->query( "SELECT data FROM ".$this->rc->db->table_name('dictionary') ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php index 8a36f1f9d..b17291bdf 100644 --- a/program/lib/Roundcube/rcube_storage.php +++ b/program/lib/Roundcube/rcube_storage.php @@ -807,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()); /** @@ -985,6 +986,6 @@ abstract class rcube_storage /** * Delete outdated cache entries */ - abstract function expunge_cache(); + abstract function cache_gc(); } // end class rcube_storage diff --git a/program/lib/Roundcube/rcube_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php index 49a378166..0fc90a55a 100644 --- a/program/lib/Roundcube/rcube_string_replacer.php +++ b/program/lib/Roundcube/rcube_string_replacer.php @@ -28,9 +28,10 @@ class rcube_string_replacer public $mailto_pattern; public $link_pattern; private $values = array(); + private $options = array(); - function __construct() + function __construct($options = array()) { // Simplified domain expression for UTF8 characters handling // Support unicode/punycode in top-level domain part @@ -44,6 +45,8 @@ class rcube_string_replacer ."@$utf_domain" // domain-part ."(\?[$url1$url2]+)?" // e.g. ?subject=test... .")/"; + + $this->options = $options; } /** @@ -89,15 +92,15 @@ class rcube_string_replacer 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); + $attrib = (array)$this->options['link_attribs']; + $attrib['href'] = $url_prefix . $url; + + $i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix); } // Return valid link for recognized schemes, otherwise // return the unmodified string for unrecognized schemes. - return $i >= 0 ? $this->get_replacement($i) : $matches[0]; + return $i >= 0 ? $prefix . $this->get_replacement($i) : $matches[0]; } /** diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php index 505b190d1..5e9c9af80 100644 --- a/program/lib/Roundcube/rcube_user.php +++ b/program/lib/Roundcube/rcube_user.php @@ -495,9 +495,9 @@ class rcube_user "INSERT INTO ".$dbh->table_name('users'). " (created, last_login, username, mail_host, language)". " VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)", - strip_newlines($data['user']), - strip_newlines($data['host']), - strip_newlines($data['language'])); + $data['user'], + $data['host'], + $data['language']); if ($user_id = $dbh->insert_id('users')) { // create rcube_user instance to make plugin hooks work @@ -517,7 +517,7 @@ class rcube_user if (empty($user_email)) { $user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain); } - $email_list[] = strip_newlines($user_email); + $email_list[] = $user_email; } // identities_level check else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) { @@ -547,7 +547,6 @@ class rcube_user $record['name'] = $user_name != $record['email'] ? $user_name : ''; } - $record['name'] = strip_newlines($record['name']); $record['user_id'] = $user_id; $record['standard'] = $standard; diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index 1ae782a25..29baa82f3 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -404,7 +404,7 @@ class rcube_utils $out = array(); $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST); - foreach ($src as $key => $value) { + foreach (array_keys($src) as $key) { $fname = $key[0] == '_' ? substr($key, 1) : $key; if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) { $out[$fname] = self::get_input_value($key, $mode); @@ -510,17 +510,24 @@ class rcube_utils */ public static function file2class($mimetype, $filename) { + $mimetype = strtolower($mimetype); + $filename = strtolower($filename); + list($primary, $secondary) = explode('/', $mimetype); $classes = array($primary ? $primary : 'unknown'); + if ($secondary) { $classes[] = $secondary; } - if (preg_match('/\.([a-z0-9]+)$/i', $filename, $m)) { - $classes[] = $m[1]; + + if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) { + if (!in_array($m[1], $classes)) { + $classes[] = $m[1]; + } } - return strtolower(join(" ", $classes)); + return join(" ", $classes); } @@ -726,11 +733,23 @@ class rcube_utils return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1])); } else if (is_numeric($date)) { - return $date; + return (int) $date; } - // support non-standard "GMTXXXX" literal - $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); + // Clean malformed data + $date = preg_replace( + array( + '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal + '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters + '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names + ), + array( + '\\1', + '', + '', + ), $date); + + $date = trim($date); // if date parsing fails, we have a date in non-rfc format. // remove token from the end and try again @@ -743,7 +762,7 @@ class rcube_utils $date = implode(' ', $d); } - return $ts; + return (int) $ts; } diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php index c2b30af59..a71305c4b 100644 --- a/program/lib/Roundcube/rcube_vcard.php +++ b/program/lib/Roundcube/rcube_vcard.php @@ -90,7 +90,7 @@ class rcube_vcard */ public function __construct($vcard = null, $charset = RCUBE_CHARSET, $detect = false, $fieldmap = array()) { - if (!empty($fielmap)) { + if (!empty($fieldmap)) { $this->extend_fieldmap($fieldmap); } @@ -481,7 +481,7 @@ class rcube_vcard $vcard_block = ''; $in_vcard_block = false; - foreach (preg_split("/[\r\n]+/", $data) as $i => $line) { + foreach (preg_split("/[\r\n]+/", $data) as $line) { if ($in_vcard_block && !empty($line)) { $vcard_block .= $line . "\n"; } @@ -491,7 +491,9 @@ class rcube_vcard 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)) { + // 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; } @@ -513,7 +515,7 @@ class rcube_vcard * * @return string Cleaned vcard block */ - private static function cleanup($vcard) + public static function cleanup($vcard) { // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;) $vcard = preg_replace( @@ -712,9 +714,15 @@ class rcube_vcard $value[] = $attrvalues; } else if (is_bool($attrvalues)) { - // true means just tag, not tag=value, as in PHOTO;BASE64:... + // true means just a tag, not tag=value, as in PHOTO;BASE64:... if ($attrvalues) { - $attr .= strtoupper(";$attrname"); + // vCard v3 uses ENCODING=B (#1489183) + if ($attrname == 'base64') { + $attr .= ";ENCODING=B"; + } + else { + $attr .= strtoupper(";$attrname"); + } } } else { @@ -782,9 +790,30 @@ class rcube_vcard } return $result; } + + $s = strtr($s, $rep2); + } + + // some implementations (GMail) use non-standard backslash before colon (#1489085) + // we will handle properly any backslashed character - removing dummy backslahes + // return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';')); + + $s = str_replace("\r", '', $s); + $pos = 0; + + while (($pos = strpos($s, '\\', $pos)) !== false) { + $next = substr($s, $pos + 1, 1); + if ($next == 'n' || $next == 'N') { + $s = substr_replace($s, "\n", $pos, 2); + } + else { + $s = substr_replace($s, '', $pos, 1); + } + + $pos += 1; } - return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';')); + return $s; } /** diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php index 715c46047..6b2efcc78 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -113,10 +113,9 @@ class rcube_washtml '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', + /* Elements which could be empty and be returned in short form (<tag />) */ + static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr', + 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ); /* State for linked objects in HTML */ @@ -134,12 +133,15 @@ class rcube_washtml /* 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(); + /* Elements which could be empty and be returned in short form (<tag />) */ + private $_void_elements = array(); /* Allowed HTML attributes */ private $_html_attribs = array(); + /* Max nesting level */ + private $max_nesting_level; + /** * Class constructor @@ -149,9 +151,9 @@ class rcube_washtml $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); + $this->_void_elements = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements); - unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['block_elements']); + unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']); $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array()); } @@ -240,7 +242,8 @@ class rcube_washtml $value = $node->getAttribute($key); if (isset($this->_html_attribs[$key]) || - ($key == 'href' && !preg_match('!^(javascript|vbscript|data:text)!i', $value) + ($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) . '"'; @@ -283,12 +286,26 @@ class rcube_washtml * It output only allowed tags with allowed attributes * and allowed inline styles */ - private function dumpHtml($node) + private function dumpHtml($node, $level = 0) { if (!$node->hasChildNodes()) { return ''; } + $level++; + + if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) { + // log error message once + if (!$this->max_nesting_level_error) { + $this->max_nesting_level_error = true; + rcube::raise_error(array('code' => 500, 'type' => 'php', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})"), + true, false); + } + return '<!-- ignored -->'; + } + $node = $node->firstChild; $dump = ''; @@ -298,19 +315,19 @@ class rcube_washtml $tagName = strtolower($node->tagName); if ($callback = $this->handlers[$tagName]) { $dump .= call_user_func($callback, $tagName, - $this->wash_attribs($node), $this->dumpHtml($node), $this); + $this->wash_attribs($node), $this->dumpHtml($node, $level), $this); } else if (isset($this->_html_elements[$tagName])) { - $content = $this->dumpHtml($node); + $content = $this->dumpHtml($node, $level); $dump .= '<' . $tagName . $this->wash_attribs($node) . - ($content != '' || isset($this->_block_elements[$tagName]) ? ">$content</$tagName>" : ' />'); + ($content === '' && isset($this->_void_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 + $dump .= $this->dumpHtml($node, $level); // ignore tags not its content } break; @@ -323,14 +340,14 @@ class rcube_washtml break; case XML_HTML_DOCUMENT_NODE: - $dump .= $this->dumpHtml($node); + $dump .= $this->dumpHtml($node, $level); break; case XML_DOCUMENT_TYPE_NODE: break; default: - $dump . '<!-- node type ' . $node->nodeType . ' -->'; + $dump .= '<!-- node type ' . $node->nodeType . ' -->'; } } while($node = $node->nextSibling); @@ -357,6 +374,9 @@ class rcube_washtml $this->config['base_url'] = ''; } + // Detect max nesting level (for dumpHTML) (#1489110) + $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level'); + @$node->loadHTML($html); return $this->dumpHtml($node); } @@ -404,6 +424,7 @@ class rcube_washtml rcube::raise_error(array('code' => 620, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => $errstr), true, false); + return ''; } @@ -412,7 +433,8 @@ class rcube_washtml // Remove invalid HTML comments (#1487759) // Don't remove valid conditional comments - $html = preg_replace('/<!--[^->[\n]*>/', '', $html); + // Don't remove MSOutlook (<!-->) conditional comments (#1489004) + $html = preg_replace('/<!--[^->\[\n]+>/', '', $html); // turn relative into absolute urls $html = self::resolve_base($html); |