summaryrefslogtreecommitdiff
path: root/program/lib/Roundcube
diff options
context:
space:
mode:
Diffstat (limited to 'program/lib/Roundcube')
-rw-r--r--program/lib/Roundcube/html.php16
-rw-r--r--program/lib/Roundcube/rcube.php74
-rw-r--r--program/lib/Roundcube/rcube_addressbook.php34
-rw-r--r--program/lib/Roundcube/rcube_browser.php42
-rw-r--r--program/lib/Roundcube/rcube_charset.php10
-rw-r--r--program/lib/Roundcube/rcube_config.php20
-rw-r--r--program/lib/Roundcube/rcube_contacts.php56
-rw-r--r--program/lib/Roundcube/rcube_csv2vcard.php10
-rw-r--r--program/lib/Roundcube/rcube_db.php29
-rw-r--r--program/lib/Roundcube/rcube_html2text.php8
-rw-r--r--program/lib/Roundcube/rcube_imap.php57
-rw-r--r--program/lib/Roundcube/rcube_imap_cache.php6
-rw-r--r--program/lib/Roundcube/rcube_imap_generic.php34
-rw-r--r--program/lib/Roundcube/rcube_ldap.php92
-rw-r--r--program/lib/Roundcube/rcube_ldap_generic.php4
-rw-r--r--program/lib/Roundcube/rcube_message.php14
-rw-r--r--program/lib/Roundcube/rcube_mime.php6
-rw-r--r--program/lib/Roundcube/rcube_plugin.php8
-rw-r--r--program/lib/Roundcube/rcube_plugin_api.php11
-rw-r--r--program/lib/Roundcube/rcube_result_index.php19
-rw-r--r--program/lib/Roundcube/rcube_result_thread.php35
-rw-r--r--program/lib/Roundcube/rcube_session.php45
-rw-r--r--program/lib/Roundcube/rcube_smtp.php14
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_atd.php12
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_enchant.php18
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_engine.php7
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_googie.php36
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_pspell.php29
-rw-r--r--program/lib/Roundcube/rcube_spellchecker.php58
-rw-r--r--program/lib/Roundcube/rcube_storage.php12
-rw-r--r--program/lib/Roundcube/rcube_user.php50
-rw-r--r--program/lib/Roundcube/rcube_utils.php125
-rw-r--r--program/lib/Roundcube/rcube_vcard.php33
-rw-r--r--program/lib/Roundcube/rcube_washtml.php94
34 files changed, 845 insertions, 273 deletions
diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php
index 5911c04d7..33517fbcd 100644
--- a/program/lib/Roundcube/html.php
+++ b/program/lib/Roundcube/html.php
@@ -3,7 +3,7 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
+ | Copyright (C) 2005-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -32,8 +32,8 @@ class html
public static $doctype = 'xhtml';
public static $lc_tags = true;
- 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');
+ public static $common_attrib = array('id','class','style','title','align','unselectable');
+ public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script');
/**
@@ -645,7 +645,7 @@ class html_select extends html
$option_content = self::quote($option_content);
}
- $this->content .= self::tag('option', $attr + $option, $option_content, array('class','style','title','disabled'));
+ $this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected'));
}
return parent::show();
@@ -677,8 +677,8 @@ class html_table extends html
*/
public function __construct($attrib = array())
{
- $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array();
- $this->attrib = array_merge($attrib, $default_attrib);
+ $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'];
@@ -880,7 +880,7 @@ class html_table extends html
private function _row_tagname()
{
static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div');
- return $row_tagnames[$this->tagname] ?: $row_tagnames['*'];
+ return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*'];
}
/**
@@ -889,7 +889,7 @@ class html_table extends html
private function _col_tagname()
{
static $col_tagnames = array('table' => 'td', '*' => 'span');
- return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];
+ return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*'];
}
}
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index 399f84fd8..69d95f023 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -3,8 +3,8 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | Copyright (C) 2011-2014, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -94,6 +94,13 @@ class rcube
*/
public $plugins;
+ /**
+ * Instance of rcube_user class.
+ *
+ * @var rcube_user
+ */
+ public $user;
+
/* private/protected vars */
protected $texts;
@@ -642,10 +649,11 @@ class rcube
/**
* Load a localization package
*
- * @param string Language ID
- * @param array Additional text labels/messages
+ * @param string $lang Language ID
+ * @param array $add Additional text labels/messages
+ * @param array $merge Additional text labels/messages to merge
*/
- public function load_language($lang = null, $add = array())
+ public function load_language($lang = null, $add = array(), $merge = array())
{
$lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
@@ -685,6 +693,11 @@ class rcube
if (is_array($add) && !empty($add)) {
$this->texts += $add;
}
+
+ // merge additional texts (from plugin)
+ if (is_array($merge) && !empty($merge)) {
+ $this->texts = array_merge($this->texts, $merge);
+ }
}
@@ -1108,7 +1121,20 @@ class rcube
// log_driver == 'file' is assumed here
$line = sprintf("[%s]: %s\n", $date, $line);
- $log_dir = self::$instance ? self::$instance->config->get('log_dir') : null;
+ $log_dir = null;
+
+ // per-user logging is activated
+ if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) {
+ $log_dir = self::$instance->get_user_log_dir();
+ if (empty($log_dir))
+ return false;
+ }
+ else if (!empty($log['dir'])) {
+ $log_dir = $log['dir'];
+ }
+ else if (self::$instance) {
+ $log_dir = self::$instance->config->get('log_dir');
+ }
if (empty($log_dir)) {
$log_dir = RCUBE_INSTALL_PATH . 'logs';
@@ -1146,7 +1172,6 @@ class rcube
// handle PHP exceptions
if (is_object($arg) && is_a($arg, 'Exception')) {
$arg = array(
- 'type' => 'php',
'code' => $arg->getCode(),
'line' => $arg->getLine(),
'file' => $arg->getFile(),
@@ -1154,7 +1179,7 @@ class rcube
);
}
else if (is_string($arg)) {
- $arg = array('message' => $arg, 'type' => 'php');
+ $arg = array('message' => $arg);
}
if (empty($arg['code'])) {
@@ -1170,7 +1195,7 @@ class rcube
$cli = php_sapi_name() == 'cli';
- if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) {
+ if (($log || $terminate) && !$cli && $arg['message']) {
$arg['fatal'] = $terminate;
self::log_bug($arg);
}
@@ -1198,7 +1223,7 @@ class rcube
*/
public static function log_bug($arg_arr)
{
- $program = strtoupper($arg_arr['type']);
+ $program = strtoupper(!empty($arg_arr['type']) ? $arg_arr['type'] : 'php');
$level = self::get_instance()->config->get('debug_level');
// disable errors for ajax requests, write to log instead (#1487831)
@@ -1284,6 +1309,20 @@ class rcube
self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
}
+ /**
+ * Setter for system user object
+ *
+ * @param rcube_user Current user instance
+ */
+ public function set_user($user)
+ {
+ if (is_object($user)) {
+ $this->user = $user;
+
+ // overwrite config with user preferences
+ $this->config->set_user_prefs((array)$this->user->get_prefs());
+ }
+ }
/**
* Getter for logged user ID.
@@ -1347,6 +1386,17 @@ class rcube
}
}
+ /**
+ * Get the per-user log directory
+ */
+ protected function get_user_log_dir()
+ {
+ $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
+ $user_name = $this->get_user_name();
+ $user_log_dir = $log_dir . '/' . $user_name;
+
+ return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
+ }
/**
* Getter for logged user language code.
@@ -1537,6 +1587,10 @@ class rcube
!empty($response) ? join('; ', $response) : ''));
}
}
+ else {
+ // allow plugins to catch sending errors with the same parameters as in 'message_before_send'
+ $this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
+ }
if (is_resource($msg_body)) {
fclose($msg_body);
diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php
index 6e2b439d8..4d9fa3db1 100644
--- a/program/lib/Roundcube/rcube_addressbook.php
+++ b/program/lib/Roundcube/rcube_addressbook.php
@@ -209,6 +209,7 @@ abstract class rcube_addressbook
public function validate(&$save_data, $autofix = false)
{
$rcube = rcube::get_instance();
+ $valid = true;
// check validity of email addresses
foreach ($this->get_col_values('email', $save_data, true) as $email) {
@@ -216,12 +217,28 @@ abstract class rcube_addressbook
if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
$error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
$this->set_error(self::ERROR_VALIDATE, $error);
- return false;
+ $valid = false;
+ break;
}
}
}
- return true;
+ // allow plugins to do contact validation and auto-fixing
+ $plugin = $rcube->plugins->exec_hook('contact_validate', array(
+ 'record' => $save_data,
+ 'autofix' => $autofix,
+ 'valid' => $valid,
+ ));
+
+ if ($valid && !$plugin['valid']) {
+ $this->set_error(self::ERROR_VALIDATE, $plugin['error']);
+ }
+
+ if (is_array($plugin['record'])) {
+ $save_data = $plugin['record'];
+ }
+
+ return $plugin['valid'];
}
/**
@@ -264,7 +281,8 @@ abstract class rcube_addressbook
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
- * @return boolean True on success, False on error
+ *
+ * @return mixed On success if ID has been changed returns ID, otherwise True, False on error
*/
function update($id, $save_cols)
{
@@ -294,8 +312,10 @@ abstract class rcube_addressbook
/**
* Mark all records in database as deleted
+ *
+ * @param bool $with_groups Remove also groups
*/
- function delete_all()
+ function delete_all($with_groups = false)
{
/* empty for read-only address books */
}
@@ -515,8 +535,12 @@ abstract class rcube_addressbook
$fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
else if ($compose_mode == 1)
$fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
- else
+ else if ($compose_mode == 0)
$fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
+ else {
+ $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
+ $fn = $plugin['fn'];
+ }
$fn = trim($fn, ', ');
diff --git a/program/lib/Roundcube/rcube_browser.php b/program/lib/Roundcube/rcube_browser.php
index 34128291b..b9642d8f9 100644
--- a/program/lib/Roundcube/rcube_browser.php
+++ b/program/lib/Roundcube/rcube_browser.php
@@ -28,32 +28,30 @@ class rcube_browser
{
$HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']);
- $this->ver = 0;
- $this->win = strpos($HTTP_USER_AGENT, 'win') != false;
- $this->mac = strpos($HTTP_USER_AGENT, 'mac') != false;
+ $this->ver = 0;
+ $this->win = strpos($HTTP_USER_AGENT, 'win') != false;
+ $this->mac = strpos($HTTP_USER_AGENT, 'mac') != false;
$this->linux = strpos($HTTP_USER_AGENT, 'linux') != false;
$this->unix = strpos($HTTP_USER_AGENT, 'unix') != false;
- $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false;
- $this->ns4 = strpos($HTTP_USER_AGENT, 'mozilla/4') !== false && strpos($HTTP_USER_AGENT, 'msie') === false;
- $this->ns = ($this->ns4 || strpos($HTTP_USER_AGENT, 'netscape') !== false);
- $this->ie = !$this->opera && strpos($HTTP_USER_AGENT, 'compatible; msie') !== false;
- $this->khtml = strpos($HTTP_USER_AGENT, 'khtml') !== false;
- $this->mz = !$this->ie && !$this->khtml && strpos($HTTP_USER_AGENT, 'mozilla/5') !== false;
- $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false;
- $this->safari = !$this->chrome && ($this->khtml || strpos($HTTP_USER_AGENT, 'safari') !== false);
+ $this->webkit = strpos($HTTP_USER_AGENT, 'applewebkit') !== false;
+ $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false || ($this->webkit && strpos($HTTP_USER_AGENT, 'opr/') !== false);
+ $this->ns = strpos($HTTP_USER_AGENT, 'netscape') !== false;
+ $this->chrome = !$this->opera && strpos($HTTP_USER_AGENT, 'chrome') !== false;
+ $this->ie = !$this->opera && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false);
+ $this->safari = !$this->opera && !$this->chrome && ($this->webkit || strpos($HTTP_USER_AGENT, 'safari') !== false);
+ $this->mz = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && !$this->opera && strpos($HTTP_USER_AGENT, 'mozilla') !== false;
- if ($this->ns || $this->chrome) {
- $test = preg_match('/(mozilla|chrome)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs);
- $this->ver = $test ? (float)$regs[2] : 0;
+ if ($this->opera) {
+ if (preg_match('/(opera|opr)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+ $this->ver = (float) $regs[2];
+ }
}
- else if ($this->mz) {
- $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs);
- $this->ver = $test ? (float)$regs[1] : 0;
+ else if (preg_match('/(chrome|msie|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+ $this->ver = (float) $regs[3];
}
- else if ($this->ie || $this->opera) {
- $test = preg_match('/(msie|opera) ([0-9.]+)/', $HTTP_USER_AGENT, $regs);
- $this->ver = $test ? (float)$regs[2] : 0;
+ else if (preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+ $this->ver = (float) $regs[1];
}
if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs))
@@ -61,10 +59,10 @@ class rcube_browser
else
$this->lang = 'en';
- $this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7));
+ $this->dom = $this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7);
$this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) ||
($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false;
- $this->imgdata = !$this->ie;
+ $this->imgdata = !$this->ie;
}
}
diff --git a/program/lib/Roundcube/rcube_charset.php b/program/lib/Roundcube/rcube_charset.php
index 19dbf6cbc..8612e7fca 100644
--- a/program/lib/Roundcube/rcube_charset.php
+++ b/program/lib/Roundcube/rcube_charset.php
@@ -199,10 +199,13 @@ class rcube_charset
$iconv_options = '';
}
}
+ else {
+ $iconv_options = false;
+ }
}
// convert charset using iconv module
- if ($iconv_options !== null && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
+ if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
// throw an exception if iconv reports an illegal character in input
// it means that input string has been truncated
set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE);
@@ -224,10 +227,13 @@ class rcube_charset
$mbstring_list = mb_list_encodings();
$mbstring_list = array_map('strtoupper', $mbstring_list);
}
+ else {
+ $mbstring_list = false;
+ }
}
// convert charset using mbstring module
- if ($mbstring_list !== null) {
+ if ($mbstring_list !== false) {
$aliases['WINDOWS-1257'] = 'ISO-8859-13';
// it happens that mbstring supports ASCII but not US-ASCII
if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) {
diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php
index 04b914c3d..afe13e879 100644
--- a/program/lib/Roundcube/rcube_config.php
+++ b/program/lib/Roundcube/rcube_config.php
@@ -63,7 +63,7 @@ class rcube_config
$this->paths = explode(PATH_SEPARATOR, $paths);
// make all paths absolute
foreach ($this->paths as $i => $path) {
- if (!$this->_is_absolute($path)) {
+ if (!rcube_utils::is_absolute_path($path)) {
if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
$this->paths[$i] = unslashify($realpath) . '/';
}
@@ -243,8 +243,8 @@ class rcube_config
*/
public function resolve_paths($file, $use_env = true)
{
- $files = array();
- $abs_path = $this->_is_absolute($file);
+ $files = array();
+ $abs_path = rcube_utils::is_absolute_path($file);
foreach ($this->paths as $basepath) {
$realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
@@ -270,14 +270,6 @@ class rcube_config
}
/**
- * Determine whether the given file path is absolute or relative
- */
- private function _is_absolute($path)
- {
- return $path[0] == DIRECTORY_SEPARATOR || preg_match('!^[a-z]:[\\\\/]!i', $path);
- }
-
- /**
* Getter for a specific config parameter
*
* @param string $name Parameter name
@@ -373,7 +365,11 @@ class rcube_config
*/
public function all()
{
- return $this->prop;
+ $rcube = rcube::get_instance();
+ $plugin = $rcube->plugins->exec_hook('config_get', array(
+ 'name' => '*', 'result' => $this->prop));
+
+ return $plugin['result'];
}
/**
diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php
index 6d01368a1..d215760cf 100644
--- a/program/lib/Roundcube/rcube_contacts.php
+++ b/program/lib/Roundcube/rcube_contacts.php
@@ -350,7 +350,7 @@ class rcube_contacts extends rcube_addressbook
if (in_array($col, $this->table_cols)) {
switch ($mode) {
case 1: // strict
- $where[] = '(' . $this->db->quoteIdentifier($col) . ' = ' . $this->db->quote($val)
+ $where[] = '(' . $this->db->quote_identifier($col) . ' = ' . $this->db->quote($val)
. ' OR ' . $this->db->ilike($col, $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')';
@@ -390,7 +390,7 @@ class rcube_contacts extends rcube_addressbook
}
foreach (array_intersect($required, $this->table_cols) as $col) {
- $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote('');
+ $and_where[] = $this->db->quote_identifier($col).' <> '.$this->db->quote('');
}
if (!empty($where)) {
@@ -592,8 +592,8 @@ class rcube_contacts extends rcube_addressbook
// validate e-mail addresses
$valid = parent::validate($save_data, $autofix);
- // require at least one e-mail address (syntax check is already done)
- if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) {
+ // require at least one email address or a name
+ if ($valid && !strlen($save_data['firstname'].$save_data['surname'].$save_data['name']) && !array_filter($this->get_col_values('email', $save_data, true))) {
$this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
$valid = false;
}
@@ -626,11 +626,11 @@ class rcube_contacts extends rcube_addressbook
}
}
- $save_data = $this->convert_save_data($save_data);
+ $save_data = $this->convert_save_data($save_data);
$a_insert_cols = $a_insert_values = array();
foreach ($save_data as $col => $value) {
- $a_insert_cols[] = $this->db->quoteIdentifier($col);
+ $a_insert_cols[] = $this->db->quote_identifier($col);
$a_insert_values[] = $this->db->quote($value);
}
@@ -655,17 +655,18 @@ class rcube_contacts extends rcube_addressbook
*
* @param mixed Record identifier
* @param array Assoziative array with save data
+ *
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
- $updated = false;
+ $updated = false;
$write_sql = array();
- $record = $this->get_record($id, true);
+ $record = $this->get_record($id, true);
$save_cols = $this->convert_save_data($save_cols, $record);
foreach ($save_cols as $col => $value) {
- $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
+ $write_sql[] = sprintf("%s=%s", $this->db->quote_identifier($col), $this->db->quote($value));
}
if (!empty($write_sql)) {
@@ -683,7 +684,7 @@ class rcube_contacts extends rcube_addressbook
$this->result = null; // clear current result (from get_record())
}
- return $updated;
+ return $updated ? true : false;
}
@@ -812,16 +813,30 @@ class rcube_contacts extends rcube_addressbook
/**
* Remove all records from the database
+ *
+ * @param bool $with_groups Remove also groups
+ *
+ * @return int Number of removed records
*/
- function delete_all()
+ function delete_all($with_groups = false)
{
$this->cache = null;
- $this->db->query("UPDATE ".$this->db->table_name($this->db_name).
- " SET del=1, changed=".$this->db->now().
- " WHERE user_id = ?", $this->user_id);
+ $this->db->query("UPDATE " . $this->db->table_name($this->db_name)
+ . " SET del = 1, changed = " . $this->db->now()
+ . " WHERE user_id = ?", $this->user_id);
- return $this->db->affected_rows();
+ $count = $this->db->affected_rows();
+
+ if ($with_groups) {
+ $this->db->query("UPDATE " . $this->db->table_name($this->db_groups)
+ . " SET del = 1, changed = " . $this->db->now()
+ . " WHERE user_id = ?", $this->user_id);
+
+ $count += $this->db->affected_rows();
+ }
+
+ return $count;
}
@@ -860,11 +875,11 @@ class rcube_contacts extends rcube_addressbook
function delete_group($gid)
{
// flag group record as deleted
- $sql_result = $this->db->query(
- "UPDATE ".$this->db->table_name($this->db_groups).
- " SET del=1, changed=".$this->db->now().
- " WHERE contactgroup_id=?".
- " AND user_id=?",
+ $this->db->query(
+ "UPDATE " . $this->db->table_name($this->db_groups)
+ . " SET del = 1, changed = " . $this->db->now()
+ . " WHERE contactgroup_id = ?"
+ . " AND user_id = ?",
$gid, $this->user_id
);
@@ -873,7 +888,6 @@ class rcube_contacts extends rcube_addressbook
return $this->db->affected_rows();
}
-
/**
* Rename a specific contact group
*
diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php
index 00e6d4e20..aa385dce4 100644
--- a/program/lib/Roundcube/rcube_csv2vcard.php
+++ b/program/lib/Roundcube/rcube_csv2vcard.php
@@ -47,7 +47,7 @@ class rcube_csv2vcard
//'business_street_2' => '',
//'business_street_3' => '',
'car_phone' => 'phone:car',
- 'categories' => 'categories',
+ 'categories' => 'groups',
//'children' => '',
'company' => 'organization',
//'company_main_phone' => '',
@@ -146,6 +146,9 @@ class rcube_csv2vcard
'work_title' => 'jobtitle',
'work_zip' => 'zipcode:work',
'group' => 'groups',
+
+ // GMail
+ 'groups' => 'groups',
);
/**
@@ -427,6 +430,11 @@ class rcube_csv2vcard
$contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
}
+ // categories/groups separator in vCard is ',' not ';'
+ if (!empty($contact['groups'])) {
+ $contact['groups'] = str_replace(';', ',', $contact['groups']);
+ }
+
// Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
foreach (array('birthday', 'anniversary') as $key) {
if (!empty($contact[$key])) {
diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index aaba28172..2828f26ee 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/Roundcube/rcube_db.php
@@ -392,7 +392,7 @@ class rcube_db
*/
protected function _query($query, $offset, $numrows, $params)
{
- $query = trim($query);
+ $query = ltrim($query);
$this->db_connect($this->dsn_select($query), true);
@@ -405,27 +405,28 @@ class rcube_db
$query = $this->set_limit($query, $numrows, $offset);
}
- $params = (array) $params;
-
// Because in Roundcube we mostly use queries that are
// executed only once, we will not use prepared queries
$pos = 0;
$idx = 0;
- while ($pos = strpos($query, '?', $pos)) {
- if ($query[$pos+1] == '?') { // skip escaped ?
- $pos += 2;
- }
- else {
- $val = $this->quote($params[$idx++]);
- unset($params[$idx-1]);
- $query = substr_replace($query, $val, $pos, 1);
- $pos += strlen($val);
+ if (count($params)) {
+ while ($pos = strpos($query, '?', $pos)) {
+ if ($query[$pos+1] == '?') { // skip escaped '?'
+ $pos += 2;
+ }
+ else {
+ $val = $this->quote($params[$idx++]);
+ unset($params[$idx-1]);
+ $query = substr_replace($query, $val, $pos, 1);
+ $pos += strlen($val);
+ }
}
}
- // replace escaped ? back to normal
- $query = rtrim(strtr($query, array('??' => '?')), ';');
+ // replace escaped '?' back to normal, see self::quote()
+ $query = str_replace('??', '?', $query);
+ $query = rtrim($query, " \t\n\r\0\x0B;");
$this->debug($query);
diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php
index 6f79e2f8e..3b4508da9 100644
--- a/program/lib/Roundcube/rcube_html2text.php
+++ b/program/lib/Roundcube/rcube_html2text.php
@@ -608,7 +608,7 @@ class rcube_html2text
$this->width = $p_width;
// Add citation markers and create <pre> block
- $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body));
+ $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body));
$body = '<pre>' . htmlspecialchars($body) . '</pre>';
$text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
@@ -616,6 +616,10 @@ class rcube_html2text
break;
}
+ // abort on invalid tag structure (e.g. no closing tag found)
+ else {
+ break;
+ }
}
while ($end || $next);
}
@@ -624,7 +628,7 @@ class rcube_html2text
/**
* Callback function to correctly add citation markers for blockquote contents
*/
- public function blockquote_citation_ballback($m)
+ public function blockquote_citation_callback($m)
{
$line = ltrim($m[2]);
$space = $line[0] == '>' ? '' : ' ';
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 9faf1bbc6..432227091 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -680,6 +680,41 @@ class rcube_imap extends rcube_storage
/**
+ * Public method for listing message flags
+ *
+ * @param string $folder Folder name
+ * @param array $uids Message UIDs
+ * @param int $mod_seq Optional MODSEQ value (of last flag update)
+ *
+ * @return array Indexed array with message flags
+ */
+ public function list_flags($folder, $uids, $mod_seq = null)
+ {
+ if (!strlen($folder)) {
+ $folder = $this->folder;
+ }
+
+ if (!$this->check_connection()) {
+ return array();
+ }
+
+ // @TODO: when cache was synchronized in this request
+ // we might already have asked for flag updates, use it.
+
+ $flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
+ $result = array();
+
+ if (!empty($flags)) {
+ foreach ($flags as $message) {
+ $result[$message->uid] = $message->flags;
+ }
+ }
+
+ return $result;
+ }
+
+
+ /**
* Public method for listing headers
*
* @param string $folder Folder name
@@ -1409,7 +1444,7 @@ class rcube_imap extends rcube_storage
public function search_once($folder = null, $str = 'ALL')
{
if (!$str) {
- return 'ALL';
+ $str = 'ALL';
}
if (!strlen($folder)) {
@@ -2121,7 +2156,7 @@ class rcube_imap extends rcube_storage
// convert charset (if text or message part)
if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
// Remove NULL characters if any (#1486189)
- if (strpos($body, "\x00") !== false) {
+ if ($formatted && strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
@@ -2843,12 +2878,21 @@ class rcube_imap extends rcube_storage
/**
* Filter the given list of folders according to access rights
+ *
+ * For performance reasons we assume user has full rights
+ * on all personal folders.
*/
protected function filter_rights($a_folders, $rights)
{
$regex = '/('.$rights.')/';
+
foreach ($a_folders as $idx => $folder) {
+ if ($this->folder_namespace($folder) == 'personal') {
+ continue;
+ }
+
$myrights = join('', (array)$this->my_rights($folder));
+
if ($myrights !== null && !preg_match($regex, $myrights)) {
unset($a_folders[$idx]);
}
@@ -3848,9 +3892,12 @@ class rcube_imap extends rcube_storage
/**
* Sort folders first by default folders and then in alphabethical order
*
- * @param array $a_folders Folders list
+ * @param array $a_folders Folders list
+ * @param bool $skip_default Skip default folders handling
+ *
+ * @return array Sorted list
*/
- protected function sort_folder_list($a_folders)
+ public function sort_folder_list($a_folders, $skip_default = false)
{
$a_out = $a_defaults = $folders = array();
@@ -3862,7 +3909,7 @@ class rcube_imap extends rcube_storage
continue;
}
- if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
+ if (!$skip_default && ($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
$a_defaults[$p] = $folder;
}
else {
diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index a8166545e..0c3edeaad 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/Roundcube/rcube_imap_cache.php
@@ -1250,10 +1250,8 @@ class rcube_imap_cache
unset($msg->replaces);
- if (is_array($msg->structure->parts)) {
- foreach ($msg->structure->parts as $part) {
- $this->message_object_prepare($part, $size);
- }
+ if (is_object($msg->structure)) {
+ $this->message_object_prepare($msg->structure, $size);
}
if (is_array($msg->parts)) {
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index f9a62f010..9035840a8 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -73,6 +73,7 @@ class rcube_imap_generic
const COMMAND_NORESPONSE = 1;
const COMMAND_CAPABILITY = 2;
const COMMAND_LASTLINE = 4;
+ const COMMAND_ANONYMIZED = 8;
const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
@@ -88,16 +89,28 @@ class rcube_imap_generic
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
+ * @param bool $anonymized Don't write the given data to log but a placeholder
*
* @param int Number of bytes sent, False on error
*/
- function putLine($string, $endln=true)
+ function putLine($string, $endln=true, $anonymized=false)
{
if (!$this->fp)
return false;
if ($this->_debug) {
- $this->debug('C: '. rtrim($string));
+ // anonymize the sent command for logging
+ $cut = $endln ? 2 : 0;
+ if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
+ $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
+ }
+ else if ($anonymized) {
+ $log = sprintf('****** [%d]', strlen($string) - $cut);
+ }
+ else {
+ $log = rtrim($string);
+ }
+ $this->debug('C: ' . $log);
}
$res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
@@ -116,10 +129,11 @@ class rcube_imap_generic
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
+ * @param bool $anonymized Don't write the given data to log but a placeholder
*
* @return int|bool Number of bytes sent, False on error
*/
- function putLineC($string, $endln=true)
+ function putLineC($string, $endln=true, $anonymized=false)
{
if (!$this->fp) {
return false;
@@ -138,7 +152,7 @@ class rcube_imap_generic
$parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
}
- $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
+ $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
if ($bytes === false)
return false;
$res += $bytes;
@@ -153,7 +167,7 @@ class rcube_imap_generic
$i++;
}
else {
- $bytes = $this->putLine($parts[$i], false);
+ $bytes = $this->putLine($parts[$i], false, $anonymized);
if ($bytes === false)
return false;
$res += $bytes;
@@ -519,7 +533,7 @@ class rcube_imap_generic
$reply = base64_encode($user . ' ' . $hash);
// send result
- $this->putLine($reply);
+ $this->putLine($reply, true, true);
}
else {
// RFC2831: DIGEST-MD5
@@ -537,7 +551,7 @@ class rcube_imap_generic
base64_decode($challenge), $this->host, 'imap', $user));
// send result
- $this->putLine($reply);
+ $this->putLine($reply, true, true);
$line = trim($this->readReply());
if ($line[0] == '+') {
@@ -577,7 +591,7 @@ class rcube_imap_generic
// RFC 4959 (SASL-IR): save one round trip
if ($this->getCapability('SASL-IR')) {
list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
- self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
+ self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
}
else {
$this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
@@ -588,7 +602,7 @@ class rcube_imap_generic
}
// send result, get reply and process it
- $this->putLine($reply);
+ $this->putLine($reply, true, true);
$line = $this->readReply();
$result = $this->parseResult($line);
}
@@ -3419,7 +3433,7 @@ class rcube_imap_generic
}
// Send command
- if (!$this->putLineC($query)) {
+ if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
}
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index 64288f973..de3790e5c 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -52,7 +52,7 @@ class rcube_ldap extends rcube_addressbook
*
* @var array
*/
- private static $group_types = array(
+ private $group_types = array(
'group' => 'member',
'groupofnames' => 'member',
'kolabgroupofnames' => 'member',
@@ -94,6 +94,9 @@ class rcube_ldap extends rcube_addressbook
$this->prop['groups']['name_attr'] = 'cn';
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
+ // extend group objectclass => member attribute mapping
+ if (!empty($this->prop['groups']['class_member_attr']))
+ $this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']);
// add group name attrib to the list of attributes to be fetched
$fetch_attributes[] = $this->prop['groups']['name_attr'];
@@ -292,6 +295,14 @@ class rcube_ldap extends rcube_addressbook
if ($this->prop['search_base_dn'] && $this->prop['search_filter']
&& (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn'))
) {
+ $search_attribs = array('uid');
+ if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) {
+ foreach ($search_bind_attrib as $r => $attr) {
+ $search_attribs[] = $attr;
+ $replaces[$r] = '';
+ }
+ }
+
$search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
$search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
$search_filter = strtr($this->prop['search_filter'], $replaces);
@@ -321,10 +332,18 @@ class rcube_ldap extends rcube_addressbook
}
}
- $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid'));
+ $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
if ($res) {
$res->rewind();
$replaces['%dn'] = $res->get_dn();
+
+ // add more replacements from 'search_bind_attrib' config
+ if ($search_bind_attrib) {
+ $res = $res->current();
+ foreach ($search_bind_attrib as $r => $attr) {
+ $replaces[$r] = $res[$attr][0];
+ }
+ }
}
if ($ldap != $this->ldap) {
@@ -355,6 +374,23 @@ class rcube_ldap extends rcube_addressbook
$this->base_dn = strtr($this->base_dn, $replaces);
$this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
+ // replace placeholders in filter settings
+ if (!empty($this->prop['filter']))
+ $this->prop['filter'] = strtr($this->prop['filter'], $replaces);
+ if (!empty($this->prop['groups']['filter']))
+ $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces);
+ if (!empty($this->prop['groups']['member_filter']))
+ $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces);
+
+ if (!empty($this->prop['group_filters'])) {
+ foreach ($this->prop['group_filters'] as $i => $gf) {
+ if (!empty($gf['base_dn']))
+ $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
+ if (!empty($gf['filter']))
+ $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces);
+ }
+ }
+
if (empty($bind_user)) {
$bind_user = $u;
}
@@ -518,7 +554,7 @@ class rcube_ldap extends rcube_addressbook
}
else {
$prop = $this->group_id ? $this->group_data : $this->prop;
- $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
+ $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn;
// use global search filter
if (!empty($this->filter))
@@ -559,9 +595,10 @@ class rcube_ldap extends rcube_addressbook
/**
* Get all members of the given group
*
- * @param string Group DN
- * @param array Group entries (if called recursively)
- * @return array Accumulated group members
+ * @param string Group DN
+ * @param boolean Count only
+ * @param array Group entries (if called recursively)
+ * @return array Accumulated group members
*/
function list_group_members($dn, $count = false, $entries = null)
{
@@ -569,7 +606,7 @@ class rcube_ldap extends rcube_addressbook
// fetch group object
if (empty($entries)) {
- $attribs = array('dn','objectClass','member','uniqueMember','memberURL');
+ $attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types));
$entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
if ($entries === false) {
return $group_members;
@@ -581,17 +618,17 @@ class rcube_ldap extends rcube_addressbook
$attrs = array();
foreach ((array)$entry['objectclass'] as $objectclass) {
- if (strtolower($objectclass) == 'groupofurls') {
- $members = $this->_list_group_memberurl($dn, $entry, $count);
- $group_members = array_merge($group_members, $members);
- }
- else if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
+ if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
&& ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
) {
$members = $this->_list_group_members($dn, $entry, $member_attr, $count);
$group_members = array_merge($group_members, $members);
$attrs[] = $member_attr;
}
+ else if (!empty($entry['memberurl'])) {
+ $members = $this->_list_group_memberurl($dn, $entry, $count);
+ $group_members = array_merge($group_members, $members);
+ }
if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
break 2;
@@ -608,6 +645,7 @@ class rcube_ldap extends rcube_addressbook
* @param string Group DN
* @param array Group entry
* @param string Member attribute to use
+ * @param boolean Count only
* @return array Accumulated group members
*/
private function _list_group_members($dn, $entry, $attr, $count)
@@ -621,8 +659,7 @@ class rcube_ldap extends rcube_addressbook
// read these attributes for all members
$attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
- $attrib[] = 'member';
- $attrib[] = 'uniqueMember';
+ $attrib = array_merge($attrib, array_values($this->group_types));
$attrib[] = 'memberURL';
$filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
@@ -669,7 +706,7 @@ class rcube_ldap extends rcube_addressbook
if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
$entries = $result->entries();
for ($j = 0; $j < $entries['count']; $j++) {
- if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
+ if ($this->is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
$group_members = array_merge($group_members, $nested_group_members);
else
$group_members[] = $entries[$j];
@@ -1287,8 +1324,10 @@ class rcube_ldap extends rcube_addressbook
/**
* Remove all contact records
+ *
+ * @param bool $with_groups Delete also groups if enabled
*/
- function delete_all()
+ function delete_all($with_groups = false)
{
// searching for contact entries
$dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
@@ -1299,6 +1338,16 @@ class rcube_ldap extends rcube_addressbook
}
$this->delete($dn_list);
}
+
+ if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) {
+ foreach ($groups as $group) {
+ $this->ldap->delete($group['dn']);
+ }
+
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
+ }
}
/**
@@ -1354,7 +1403,7 @@ class rcube_ldap extends rcube_addressbook
$out[$this->primary_key] = self::dn_encode($rec['dn']);
// determine record type
- if (self::is_group_entry($rec)) {
+ if ($this->is_group_entry($rec)) {
$out['_type'] = 'group';
$out['readonly'] = true;
$fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
@@ -1479,11 +1528,11 @@ class rcube_ldap extends rcube_addressbook
/**
* Determines whether the given LDAP entry is a group record
*/
- private static function is_group_entry($entry)
+ private function is_group_entry($entry)
{
$classes = array_map('strtolower', (array)$entry['objectclass']);
- return count(array_intersect(array_keys(self::$group_types), $classes)) > 0;
+ return count(array_intersect(array_keys($this->group_types), $classes)) > 0;
}
/**
@@ -1569,11 +1618,12 @@ class rcube_ldap extends rcube_addressbook
// special case: list groups from 'group_filters' config
if ($vlv_page === null && !empty($this->prop['group_filters'])) {
$groups = array();
+ $rcube = rcube::get_instance();
// list regular groups configuration as special filter
if (!empty($this->prop['groups']['filter'])) {
$id = '__groups__';
- $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups'];
+ $groups[$id] = array('ID' => $id, 'name' => $rcube->gettext('groups'), 'virtual' => true) + $this->prop['groups'];
}
foreach ($this->prop['group_filters'] as $id => $prop) {
@@ -1914,7 +1964,7 @@ class rcube_ldap extends rcube_addressbook
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
- if ($attr = self::$group_types[strtolower($oc)]) {
+ if ($attr = $this->group_types[strtolower($oc)]) {
return $attr;
}
}
diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php
index 923a12a41..b85afe4ce 100644
--- a/program/lib/Roundcube/rcube_ldap_generic.php
+++ b/program/lib/Roundcube/rcube_ldap_generic.php
@@ -240,7 +240,7 @@ class rcube_ldap_generic
$method = 'DIGEST-MD5';
}
- $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: $pass]");
+ $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: **** [" . strlen($pass) . "]");
if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
$this->_debug("S: OK");
@@ -271,7 +271,7 @@ class rcube_ldap_generic
return false;
}
- $this->_debug("C: Bind $dn [pass: $pass]");
+ $this->_debug("C: Bind $dn, pass: **** [" . strlen($pass) . "]");
if (@ldap_bind($this->conn, $dn, $pass)) {
$this->_debug("S: OK");
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index 9b662a286..f24ec3ed8 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -211,16 +211,19 @@ class rcube_message
}
$level = explode('.', $part->mime_id);
+ $depth = count($level);
// Check if the part belongs to higher-level's multipart part
- // this can be alternative/related/signed/encrypted, but not mixed
+ // this can be alternative/related/signed/encrypted or mixed
while (array_pop($level) !== null) {
- if (!count($level)) {
+ $parent_depth = count($level);
+ if (!$parent_depth) {
return true;
}
$parent = $this->mime_parts[join('.', $level)];
- if (!preg_match('/^multipart\/(alternative|related|signed|encrypted)$/', $parent->mimetype)) {
+ if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $parent->mimetype)
+ || ($parent->mimetype == 'multipart/mixed' && $parent_depth < $depth - 1)) {
continue 2;
}
}
@@ -529,8 +532,9 @@ class rcube_message
$part_mimetype = $mail_part->real_mimetype;
list($primary_type, $secondary_type) = explode('/', $part_mimetype);
}
- else
- $part_mimetype = $mail_part->mimetype;
+ else {
+ $part_mimetype = $part_orig_mimetype = $mail_part->mimetype;
+ }
// multipart/alternative
if ($primary_type == 'multipart') {
diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php
index 9c2220328..55b70f67c 100644
--- a/program/lib/Roundcube/rcube_mime.php
+++ b/program/lib/Roundcube/rcube_mime.php
@@ -378,6 +378,10 @@ class rcube_mime
}
if ($decode) {
$name = self::decode_header($name, $fallback);
+ // some clients encode addressee name with quotes around it
+ if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
+ $name = substr($name, 1, -1);
+ }
}
}
@@ -810,7 +814,7 @@ class rcube_mime
}
$mime_types = $mime_extensions = array();
- $regex = "/([\w\+\-\.\/]+)\t+([\w\s]+)/i";
+ $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
foreach((array)$lines as $line) {
// skip comments or mime types w/o any extensions
if ($line[0] == '#' || !preg_match($regex, $line, $matches))
diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php
index 3153a8410..f0af95332 100644
--- a/program/lib/Roundcube/rcube_plugin.php
+++ b/program/lib/Roundcube/rcube_plugin.php
@@ -109,7 +109,7 @@ abstract class rcube_plugin
*/
public function require_plugin($plugin_name)
{
- return $this->api->load_plugin($plugin_name);
+ return $this->api->load_plugin($plugin_name, true);
}
/**
@@ -125,13 +125,17 @@ abstract class rcube_plugin
$fpath = $this->home.'/'.$fname;
$rcube = rcube::get_instance();
- if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) {
+ if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) {
rcube::raise_error(array(
'code' => 527, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load config from $fpath"), true, false);
return false;
}
+ else if (!$is_local) {
+ // Search plugin_name.inc.php file in any configured path
+ return $rcube->config->load_from_file($this->ID . '.inc.php');
+ }
return true;
}
diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php
index 5a25ada02..461c3cc07 100644
--- a/program/lib/Roundcube/rcube_plugin_api.php
+++ b/program/lib/Roundcube/rcube_plugin_api.php
@@ -35,8 +35,9 @@ class rcube_plugin_api
public $url = 'plugins/';
public $task = '';
public $output;
- public $handlers = array();
- public $allowed_prefs = array();
+ public $handlers = array();
+ public $allowed_prefs = array();
+ public $allowed_session_prefs = array();
protected $plugins = array();
protected $tasks = array();
@@ -167,10 +168,11 @@ class rcube_plugin_api
* Load the specified plugin
*
* @param string Plugin name
+ * @param boolean Force loading of the plugin even if it doesn't match the filter
*
* @return boolean True on success, false if not loaded or failure
*/
- public function load_plugin($plugin_name)
+ public function load_plugin($plugin_name, $force = false)
{
static $plugins_dir;
@@ -196,7 +198,7 @@ class rcube_plugin_api
// check inheritance...
if (is_subclass_of($plugin, 'rcube_plugin')) {
// ... task, request type and framed mode
- if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
+ if (($force || !$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
&& (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html'))
&& (!$plugin->noframe || empty($_REQUEST['_framed']))
) {
@@ -282,6 +284,7 @@ class rcube_plugin_api
$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['version'] = $json['version'];
$info['license'] = $json['license'];
if ($license_uri = $license_uris[$info['license']])
$info['license_uri'] = $license_uri;
diff --git a/program/lib/Roundcube/rcube_result_index.php b/program/lib/Roundcube/rcube_result_index.php
index 5f592c54f..058f25c6f 100644
--- a/program/lib/Roundcube/rcube_result_index.php
+++ b/program/lib/Roundcube/rcube_result_index.php
@@ -231,29 +231,13 @@ class rcube_result_index
/**
- * Filters data set. Removes elements listed in $ids list.
+ * Filters data set. Removes elements not listed in $ids list.
*
* @param array $ids List of IDs to remove.
*/
public function filter($ids = array())
{
$data = $this->get();
- $data = array_diff($data, $ids);
-
- $this->meta = array();
- $this->meta['count'] = count($data);
- $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
- }
-
-
- /**
- * Filters data set. Removes elements not listed in $ids list.
- *
- * @param array $ids List of IDs to keep.
- */
- public function intersect($ids = array())
- {
- $data = $this->get();
$data = array_intersect($data, $ids);
$this->meta = array();
@@ -332,6 +316,7 @@ class rcube_result_index
if (empty($this->raw_data)) {
return array();
}
+
return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
}
diff --git a/program/lib/Roundcube/rcube_result_thread.php b/program/lib/Roundcube/rcube_result_thread.php
index 7657550be..ceaaf59a6 100644
--- a/program/lib/Roundcube/rcube_result_thread.php
+++ b/program/lib/Roundcube/rcube_result_thread.php
@@ -453,7 +453,7 @@ class rcube_result_thread
// when sorting search result it's good to make the index smaller
if ($index->count() != $this->count_messages()) {
- $index->intersect($this->get());
+ $index->filter($this->get());
}
$result = array_fill_keys($index->get(), null);
@@ -606,33 +606,39 @@ class rcube_result_thread
// arrays handling is much more expensive
// For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))
// -- 2
- //
// -- 3
// \-- 6
// |-- 4
// | \-- 23
// |
// \-- 44
- // \-- 7
- // \-- 96
+ // \-- 7
+ // \-- 96
//
// The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96
if ($str[$begin] != '(') {
- $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
- $msg = substr($str, $begin, $stop - $begin);
- if (!$msg) {
+ // find next bracket
+ $stop = $begin + strcspn($str, '()', $begin, $end - $begin);
+ $messages = explode(' ', trim(substr($str, $begin, $stop - $begin)));
+
+ if (empty($messages)) {
return $node;
}
- $this->meta['messages']++;
-
- $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
+ foreach ($messages as $msg) {
+ if ($msg) {
+ $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
+ $this->meta['messages']++;
+ $depth++;
+ }
+ }
- if ($stop + 1 < $end) {
- $node .= $this->parse_thread($str, $stop + 1, $end, $depth + 1);
+ if ($stop < $end) {
+ $node .= $this->parse_thread($str, $stop, $end, $depth);
}
- } else {
+ }
+ else {
$off = $begin;
while ($off < $end) {
$start = $off;
@@ -649,7 +655,8 @@ class rcube_result_thread
if ($p1 !== false && $p1 < $p) {
$off = $p1 + 1;
$n++;
- } else {
+ }
+ else {
$off = $p + 1;
$n--;
}
diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php
index 67072df41..caca262c6 100644
--- a/program/lib/Roundcube/rcube_session.php
+++ b/program/lib/Roundcube/rcube_session.php
@@ -34,6 +34,7 @@ class rcube_session
private $changed;
private $time_diff = 0;
private $reloaded = false;
+ private $appends = array();
private $unsets = array();
private $gc_handlers = array();
private $cookiename = 'roundcube_sessauth';
@@ -441,8 +442,19 @@ class rcube_session
$node = &$this->get_node(explode('.', $path), $_SESSION);
- if ($key !== null) $node[$key] = $value;
- else $node[] = $value;
+ if ($key !== null) {
+ $node[$key] = $value;
+ $path .= '.' . $key;
+ }
+ else {
+ $node[] = $value;
+ }
+
+ $this->appends[] = $path;
+
+ // when overwriting a previously unset variable
+ if ($this->unsets[$path])
+ unset($this->unsets[$path]);
}
@@ -491,13 +503,40 @@ class rcube_session
*/
public function reload()
{
+ // collect updated data from previous appends
+ $merge_data = array();
+ foreach ((array)$this->appends as $var) {
+ $path = explode('.', $var);
+ $value = $this->get_node($path, $_SESSION);
+ $k = array_pop($path);
+ $node = &$this->get_node($path, $merge_data);
+ $node[$k] = $value;
+ }
+
if ($this->key && $this->memcache)
$data = $this->mc_read($this->key);
else if ($this->key)
$data = $this->db_read($this->key);
- if ($data)
+ if ($data) {
session_decode($data);
+
+ // apply appends and unsets to reloaded data
+ $_SESSION = array_merge_recursive($_SESSION, $merge_data);
+
+ foreach ((array)$this->unsets as $var) {
+ if (isset($_SESSION[$var])) {
+ unset($_SESSION[$var]);
+ }
+ else {
+ $path = explode('.', $var);
+ $k = array_pop($path);
+ $node = &$this->get_node($path, $_SESSION);
+ unset($node[$k]);
+ }
+ }
+ }
+
}
/**
diff --git a/program/lib/Roundcube/rcube_smtp.php b/program/lib/Roundcube/rcube_smtp.php
index 60b1389ea..70f15dc7b 100644
--- a/program/lib/Roundcube/rcube_smtp.php
+++ b/program/lib/Roundcube/rcube_smtp.php
@@ -29,6 +29,7 @@ class rcube_smtp
private $conn = null;
private $response;
private $error;
+ private $anonymize_log = 0;
// define headers delimiter
const SMTP_MIME_CRLF = "\r\n";
@@ -67,6 +68,7 @@ class rcube_smtp
'smtp_auth_type' => $rcube->config->get('smtp_auth_type'),
'smtp_helo_host' => $rcube->config->get('smtp_helo_host'),
'smtp_timeout' => $rcube->config->get('smtp_timeout'),
+ 'smtp_conn_options' => $rcube->config->get('smtp_conn_options'),
'smtp_auth_callbacks' => array(),
));
@@ -106,10 +108,11 @@ class rcube_smtp
// IDNA Support
$smtp_host = rcube_utils::idn_to_ascii($smtp_host);
- $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host);
+ $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host, false, 0, $CONFIG['smtp_conn_options']);
if ($rcube->config->get('smtp_debug')) {
$this->conn->setDebug(true, array($this, 'debug_handler'));
+ $this->anonymize_log = 0;
}
// register authentication methods
@@ -329,6 +332,15 @@ class rcube_smtp
*/
public function debug_handler(&$smtp, $message)
{
+ // catch AUTH commands and set anonymization flag for subsequent sends
+ if (preg_match('/^Send: AUTH ([A-Z]+)/', $message, $m)) {
+ $this->anonymize_log = $m[1] == 'LOGIN' ? 2 : 1;
+ }
+ // anonymize this log entry
+ else if ($this->anonymize_log > 0 && strpos($message, 'Send:') === 0 && --$this->anonymize_log == 0) {
+ $message = sprintf('Send: ****** [%d]', strlen($message) - 8);
+ }
+
if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$message = substr($message, 0, self::DEBUG_LINE_LENGTH)
diff --git a/program/lib/Roundcube/rcube_spellcheck_atd.php b/program/lib/Roundcube/rcube_spellcheck_atd.php
index 68e8b7cb8..9f073f56f 100644
--- a/program/lib/Roundcube/rcube_spellcheck_atd.php
+++ b/program/lib/Roundcube/rcube_spellcheck_atd.php
@@ -39,6 +39,18 @@ class rcube_spellcheck_atd extends rcube_spellcheck_engine
);
/**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $langs = array_values($this->langhosts);
+ $langs[] = 'en';
+ return $langs;
+ }
+
+ /**
* Set content and check spelling
*
* @see rcube_spellcheck_engine::check()
diff --git a/program/lib/Roundcube/rcube_spellcheck_enchant.php b/program/lib/Roundcube/rcube_spellcheck_enchant.php
index a22251e00..14d6fff46 100644
--- a/program/lib/Roundcube/rcube_spellcheck_enchant.php
+++ b/program/lib/Roundcube/rcube_spellcheck_enchant.php
@@ -31,6 +31,24 @@ class rcube_spellcheck_enchant extends rcube_spellcheck_engine
private $matches = array();
/**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $this->init();
+
+ $langs = array();
+ $dicts = enchant_broker_list_dicts($this->enchant_broker);
+ foreach ($dicts as $dict) {
+ $langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
+ }
+
+ return array_unique($langs);
+ }
+
+ /**
* Initializes Enchant dictionary
*/
private function init()
diff --git a/program/lib/Roundcube/rcube_spellcheck_engine.php b/program/lib/Roundcube/rcube_spellcheck_engine.php
index 88e10ac05..3cb4ca3de 100644
--- a/program/lib/Roundcube/rcube_spellcheck_engine.php
+++ b/program/lib/Roundcube/rcube_spellcheck_engine.php
@@ -43,6 +43,13 @@ abstract class rcube_spellcheck_engine
}
/**
+ * Return a list of languages supported by this backend
+ *
+ * @return array Indexed list of language codes
+ */
+ abstract function languages();
+
+ /**
* Set content and check spelling
*
* @param string $text Text content for spellchecking
diff --git a/program/lib/Roundcube/rcube_spellcheck_googie.php b/program/lib/Roundcube/rcube_spellcheck_googie.php
index 70507dc23..3777942a6 100644
--- a/program/lib/Roundcube/rcube_spellcheck_googie.php
+++ b/program/lib/Roundcube/rcube_spellcheck_googie.php
@@ -26,13 +26,28 @@
*/
class rcube_spellcheck_googie extends rcube_spellcheck_engine
{
- const GOOGLE_HOST = 'ssl://www.google.com';
- const GOOGLE_PORT = 443;
+ const GOOGIE_HOST = 'ssl://spell.roundcube.net';
+ const GOOGIE_PORT = 443;
private $matches = array();
private $content;
/**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ return array('am','ar','ar','bg','br','ca','cs','cy','da',
+ 'de_CH','de_DE','el','en_GB','en_US',
+ 'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl',
+ 'he','hr','hu','hy','is','it','ku','lt','lv','nl',
+ 'pl','pt_BR','pt_PT','ro','ru',
+ 'sk','sl','sv','uk');
+ }
+
+ /**
* Set content and check spelling
*
* @see rcube_spellcheck_engine::check()
@@ -52,25 +67,25 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
$path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
}
else {
- $host = self::GOOGLE_HOST;
- $port = self::GOOGLE_PORT;
+ $host = self::GOOGIE_HOST;
+ $port = self::GOOGIE_PORT;
$path = '/tbproxy/spell?lang=' . $this->lang;
}
- // Google has some problem with spaces, use \n instead
- $gtext = str_replace(' ', "\n", $text);
+ $path .= sprintf('&key=%06d', $_SESSION['user_id']);
$gtext = '<?xml version="1.0" encoding="utf-8" ?>'
.'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
- .'<text>' . $gtext . '</text>'
+ .'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>'
.'</spellrequest>';
$store = '';
if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
$out = "POST $path HTTP/1.0\r\n";
$out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
+ $out .= "User-Agent: Roundcube Webmail/" . RCMAIL_VERSION . " (Googiespell Wrapper)\r\n";
$out .= "Content-Length: " . strlen($gtext) . "\r\n";
- $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
+ $out .= "Content-Type: text/xml\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= $gtext;
fwrite($fp, $out);
@@ -83,8 +98,10 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
// parse HTTP response
if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) {
$http_status = $m[1];
- if ($http_status != '200')
+ if ($http_status != '200') {
$this->error = 'HTTP ' . $m[1] . $m[2];
+ $this->error .= "\n" . $store;
+ }
}
if (!$store) {
@@ -92,6 +109,7 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
}
else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
$this->error = "Error code $m[1] returned";
+ $this->error .= preg_match('/<errortext>([^<]+)/', $store, $m) ? ": " . html_entity_decode($m[1]) : '';
}
preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
diff --git a/program/lib/Roundcube/rcube_spellcheck_pspell.php b/program/lib/Roundcube/rcube_spellcheck_pspell.php
index ce089ed8f..b12684e43 100644
--- a/program/lib/Roundcube/rcube_spellcheck_pspell.php
+++ b/program/lib/Roundcube/rcube_spellcheck_pspell.php
@@ -30,6 +30,35 @@ class rcube_spellcheck_pspell extends rcube_spellcheck_engine
private $matches = array();
/**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $defaults = array('en');
+ $langs = array();
+
+ // get aspell dictionaries
+ exec('aspell dump dicts', $dicts);
+ if (!empty($dicts)) {
+ $seen = array();
+ foreach ($dicts as $lang) {
+ $lang = preg_replace('/-.*$/', '', $lang);
+ $langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang;
+ if (!$seen[$langc]++)
+ $langs[] = $lang;
+ }
+ $langs = array_unique($langs);
+ }
+ else {
+ $langs = $defaults;
+ }
+
+ return $langs;
+ }
+
+ /**
* Initializes PSpell dictionary
*/
private function init()
diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php
index 31835dbb5..5b77bda02 100644
--- a/program/lib/Roundcube/rcube_spellchecker.php
+++ b/program/lib/Roundcube/rcube_spellchecker.php
@@ -65,6 +65,52 @@ class rcube_spellchecker
}
}
+ /**
+ * Return a list of supported languages
+ */
+ function languages()
+ {
+ // trust configuration
+ $configured = $this->rc->config->get('spellcheck_languages');
+ if (!empty($configured) && is_array($configured) && !$configured[0]) {
+ return $configured;
+ }
+ else if (!empty($configured)) {
+ $langs = (array)$configured;
+ }
+ else if ($this->backend) {
+ $langs = $this->backend->languages();
+ }
+
+ // load index
+ @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
+
+ // add correct labels
+ $languages = array();
+ foreach ($langs as $lang) {
+ $langc = strtolower(substr($lang, 0, 2));
+ $alias = $rcube_language_aliases[$langc];
+ if (!$alias) {
+ $alias = $langc.'_'.strtoupper($langc);
+ }
+ if ($rcube_languages[$lang]) {
+ $languages[$lang] = $rcube_languages[$lang];
+ }
+ else if ($rcube_languages[$alias]) {
+ $languages[$lang] = $rcube_languages[$alias];
+ }
+ else {
+ $languages[$lang] = ucfirst($lang);
+ }
+ }
+
+ // remove possible duplicates (#1489395)
+ $languages = array_unique($languages);
+
+ asort($languages);
+
+ return $languages;
+ }
/**
* Set content and check spelling
@@ -152,7 +198,7 @@ class rcube_spellchecker
// send output
$out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
- foreach ($this->matches as $item) {
+ foreach ((array)$this->matches as $item) {
$out .= '<c o="'.$item[1].'" l="'.$item[2].'">';
$out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
$out .= '</c>';
@@ -173,7 +219,7 @@ class rcube_spellchecker
{
$result = array();
- foreach ($this->matches as $item) {
+ foreach ((array)$this->matches as $item) {
if ($this->engine == 'pspell') {
$word = $item[0];
}
@@ -306,7 +352,7 @@ class rcube_spellchecker
"UPDATE ".$this->rc->db->table_name('dictionary')
." SET data = ?"
." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
- ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
+ ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
implode(' ', $plugin['dictionary']), $plugin['language']);
}
// don't store empty dict
@@ -314,14 +360,14 @@ class rcube_spellchecker
$this->rc->db->query(
"DELETE 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') . " = ?",
+ ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
$plugin['language']);
}
}
else if (!empty($this->dict)) {
$this->rc->db->query(
"INSERT INTO " .$this->rc->db->table_name('dictionary')
- ." (user_id, " . $this->rc->db->quoteIdentifier('language') . ", data) VALUES (?, ?, ?)",
+ ." (user_id, " . $this->rc->db->quote_identifier('language') . ", data) VALUES (?, ?, ?)",
$plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary']));
}
}
@@ -348,7 +394,7 @@ class rcube_spellchecker
$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') . " = ?",
+ ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
$plugin['language']);
if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) {
diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php
index e697b2c73..ca65af1cb 100644
--- a/program/lib/Roundcube/rcube_storage.php
+++ b/program/lib/Roundcube/rcube_storage.php
@@ -360,6 +360,18 @@ abstract class rcube_storage
/**
+ * Public method for listing message flags
+ *
+ * @param string $folder Folder name
+ * @param array $uids Message UIDs
+ * @param int $mod_seq Optional MODSEQ value
+ *
+ * @return array Indexed array with message flags
+ */
+ abstract function list_flags($folder, $uids, $mod_seq = null);
+
+
+ /**
* Public method for listing headers.
*
* @param string $folder Folder name
diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php
index 5e9c9af80..e232736c9 100644
--- a/program/lib/Roundcube/rcube_user.php
+++ b/program/lib/Roundcube/rcube_user.php
@@ -125,8 +125,10 @@ class rcube_user
*/
function get_prefs()
{
+ $prefs = array();
+
if (!empty($this->language))
- $prefs = array('language' => $this->language);
+ $prefs['language'] = $this->language;
if ($this->ID) {
// Preferences from session (write-master is unavailable)
@@ -163,8 +165,16 @@ class rcube_user
if (!$this->ID)
return false;
- $config = $this->rc->config;
- $old_prefs = (array)$this->get_prefs();
+ $plugin = $this->rc->plugins->exec_hook('preferences_update', array(
+ 'userid' => $this->ID, 'prefs' => $a_user_prefs, 'old' => (array)$this->get_prefs()));
+
+ if (!empty($plugin['abort'])) {
+ return;
+ }
+
+ $a_user_prefs = $plugin['prefs'];
+ $old_prefs = $plugin['old'];
+ $config = $this->rc->config;
// merge (partial) prefs array with existing settings
$save_prefs = $a_user_prefs + $old_prefs;
@@ -213,6 +223,14 @@ class rcube_user
return false;
}
+ /**
+ * Generate a unique hash to identify this user which
+ */
+ function get_hash()
+ {
+ $key = substr($this->rc->config->get('des_key'), 1, 4);
+ return md5($this->data['user_id'] . $key . $this->data['username'] . '@' . $this->data['mail_host']);
+ }
/**
* Get default identity of this user
@@ -249,7 +267,7 @@ class rcube_user
"SELECT * FROM ".$this->db->table_name('identities').
" WHERE del <> 1 AND user_id = ?".
($sql_add ? " ".$sql_add : "").
- " ORDER BY ".$this->db->quoteIdentifier('standard')." DESC, name ASC, identity_id ASC",
+ " ORDER BY ".$this->db->quote_identifier('standard')." DESC, name ASC, identity_id ASC",
$this->ID);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -284,7 +302,7 @@ class rcube_user
$query_cols = $query_params = array();
foreach ((array)$data as $col => $value) {
- $query_cols[] = $this->db->quoteIdentifier($col) . ' = ?';
+ $query_cols[] = $this->db->quote_identifier($col) . ' = ?';
$query_params[] = $value;
}
$query_params[] = $iid;
@@ -320,7 +338,7 @@ class rcube_user
$insert_cols = $insert_values = array();
foreach ((array)$data as $col => $value) {
- $insert_cols[] = $this->db->quoteIdentifier($col);
+ $insert_cols[] = $this->db->quote_identifier($col);
$insert_values[] = $value;
}
$insert_cols[] = 'user_id';
@@ -385,7 +403,7 @@ class rcube_user
if ($this->ID && $iid) {
$this->db->query(
"UPDATE ".$this->db->table_name('identities').
- " SET ".$this->db->quoteIdentifier('standard')." = '0'".
+ " SET ".$this->db->quote_identifier('standard')." = '0'".
" WHERE user_id = ?".
" AND identity_id <> ?".
" AND del <> 1",
@@ -625,11 +643,11 @@ class rcube_user
$result = array();
$sql_result = $this->db->query(
- "SELECT search_id AS id, ".$this->db->quoteIdentifier('name')
+ "SELECT search_id AS id, ".$this->db->quote_identifier('name')
." FROM ".$this->db->table_name('searches')
." WHERE user_id = ?"
- ." AND ".$this->db->quoteIdentifier('type')." = ?"
- ." ORDER BY ".$this->db->quoteIdentifier('name'),
+ ." AND ".$this->db->quote_identifier('type')." = ?"
+ ." ORDER BY ".$this->db->quote_identifier('name'),
(int) $this->ID, (int) $type);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -657,9 +675,9 @@ class rcube_user
}
$sql_result = $this->db->query(
- "SELECT ".$this->db->quoteIdentifier('name')
- .", ".$this->db->quoteIdentifier('data')
- .", ".$this->db->quoteIdentifier('type')
+ "SELECT ".$this->db->quote_identifier('name')
+ .", ".$this->db->quote_identifier('data')
+ .", ".$this->db->quote_identifier('type')
." FROM ".$this->db->table_name('searches')
." WHERE user_id = ?"
." AND search_id = ?",
@@ -714,11 +732,11 @@ class rcube_user
$insert_cols[] = 'user_id';
$insert_values[] = (int) $this->ID;
- $insert_cols[] = $this->db->quoteIdentifier('type');
+ $insert_cols[] = $this->db->quote_identifier('type');
$insert_values[] = (int) $data['type'];
- $insert_cols[] = $this->db->quoteIdentifier('name');
+ $insert_cols[] = $this->db->quote_identifier('name');
$insert_values[] = $data['name'];
- $insert_cols[] = $this->db->quoteIdentifier('data');
+ $insert_cols[] = $this->db->quote_identifier('data');
$insert_values[] = serialize($data['data']);
$sql = "INSERT INTO ".$this->db->table_name('searches')
diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php
index b73bc0812..46d53ac91 100644
--- a/program/lib/Roundcube/rcube_utils.php
+++ b/program/lib/Roundcube/rcube_utils.php
@@ -454,6 +454,9 @@ class rcube_utils
// cut out all contents between { and }
while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
+ $nested = strpos($source, '{', $pos+1);
+ if ($nested && $nested < $pos2) // when dealing with nested blocks (e.g. @media), take the inner one
+ $pos = $nested;
$length = $pos2 - $pos - 1;
$styles = substr($source, $pos+1, $length);
@@ -619,6 +622,10 @@ class rcube_utils
*/
public static function parse_host($name, $host = '')
{
+ if (!is_string($name)) {
+ return $name;
+ }
+
// %n - host
$n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
// %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
@@ -639,8 +646,7 @@ class rcube_utils
}
}
- $name = str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
- return $name;
+ return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
}
@@ -677,9 +683,17 @@ class rcube_utils
*/
public static function remote_addr()
{
- foreach (array('HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR') as $prop) {
- if (!empty($_SERVER[$prop]))
- return $_SERVER[$prop];
+ if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+ $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
+ return $hosts[0];
+ }
+
+ if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
+ return $_SERVER['HTTP_X_REAL_IP'];
+ }
+
+ if (!empty($_SERVER['REMOTE_ADDR'])) {
+ return $_SERVER['REMOTE_ADDR'];
}
return '';
@@ -744,40 +758,13 @@ class rcube_utils
*/
public static function strtotime($date)
{
- $date = trim($date);
-
- // check for MS Outlook vCard date format YYYYMMDD
- if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
- return mktime(0,0,0, intval($m[2]), intval($m[3]), intval($m[1]));
- }
-
- // common little-endian formats, e.g. dd/mm/yyyy (not all are supported by strtotime)
- if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)
- && $m[1] > 0 && $m[1] <= 31 && $m[2] > 0 && $m[2] <= 12 && $m[3] >= 1970
- ) {
- return mktime(0,0,0, intval($m[2]), intval($m[1]), intval($m[3]));
- }
+ $date = self::clean_datestr($date);
// unix timestamp
if (is_numeric($date)) {
return (int) $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
while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
@@ -805,8 +792,8 @@ class rcube_utils
return $date;
}
- $dt = false;
- $date = trim($date);
+ $dt = false;
+ $date = self::clean_datestr($date);
// try to parse string with DateTime first
if (!empty($date)) {
@@ -831,6 +818,52 @@ class rcube_utils
return $dt;
}
+ /**
+ * Clean up date string for strtotime() input
+ *
+ * @param string $date Date string
+ *
+ * @return string Date string
+ */
+ public static function clean_datestr($date)
+ {
+ $date = trim($date);
+
+ // check for MS Outlook vCard date format YYYYMMDD
+ if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
+ return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3]));
+ }
+
+ // 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);
+
+ // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here
+ if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)) {
+ $mdy = $m[2] > 12 && $m[1] <= 12;
+ $day = $mdy ? $m[2] : $m[1];
+ $month = $mdy ? $m[1] : $m[2];
+ $date = sprintf('%04d-%02d-%02d 00:00:00', intval($m[3]), $month, $day);
+ }
+ // I've found that YYYY.MM.DD is recognized wrong, so here's a fix
+ else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/', $date)) {
+ $date = str_replace('.', '-', $date) . ' 00:00:00';
+ }
+
+ return $date;
+ }
+
/*
* Idn_to_ascii wrapper.
* Intl/Idn modules version of this function doesn't work with e-mail address
@@ -890,10 +923,20 @@ class rcube_utils
*
* @param string Input string (UTF-8)
* @param boolean True to return list of words as array
+ *
* @return mixed Normalized string or a list of normalized tokens
*/
public static function normalize_string($str, $as_array = false)
{
+ // replace 4-byte unicode characters with '?' character,
+ // these are not supported in default utf-8 charset on mysql,
+ // the chance we'd need them in searching is very low
+ $str = preg_replace('/('
+ . '\xF0[\x90-\xBF][\x80-\xBF]{2}'
+ . '|[\xF1-\xF3][\x80-\xBF]{3}'
+ . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'
+ . ')/', '?', $str);
+
// split by words
$arr = self::tokenize_string($str);
@@ -1002,4 +1045,16 @@ class rcube_utils
return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true);
}
+ /**
+ * OS-dependent absolute path detection
+ */
+ public static function is_absolute_path($path)
+ {
+ if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
+ return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
+ }
+ else {
+ return $path[0] == DIRECTORY_SEPARATOR;
+ }
+ }
}
diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index d54dc56ad..a54ee7e11 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/Roundcube/rcube_vcard.php
@@ -378,7 +378,7 @@ class rcube_vcard
default:
if ($field == 'phone' && $this->phonetypemap[$type_uc]) {
$type = $this->phonetypemap[$type_uc];
- }
+ }
if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) {
$index = count($this->raw[$tag]);
@@ -518,20 +518,28 @@ class rcube_vcard
*/
public static function cleanup($vcard)
{
- // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
- $vcard = preg_replace(
- '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
- '\2;type=\5\3:\4',
- $vcard);
-
// convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
$vcard = preg_replace_callback(
'/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
array('self', 'x_abrelatednames_callback'),
$vcard);
- // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
- $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
+ // Cleanup
+ $vcard = preg_replace(array(
+ // convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
+ '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
+ '/^item\d*\.X-AB.*$/m', // remove cruft like item1.X-AB*
+ '/^item\d*\./m', // remove item1.ADR instead of ADR
+ '/\n+/', // remove empty lines
+ '/^(N:[^;\R]*)$/m', // if N doesn't have any semicolons, add some
+ ),
+ array(
+ '\2;type=\5\3:\4',
+ '',
+ '',
+ "\n",
+ '\1;;;;',
+ ), $vcard);
// convert X-WAB-GENDER to X-GENDER
if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
@@ -539,9 +547,6 @@ class rcube_vcard
$vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
}
- // if N doesn't have any semicolons, add some
- $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
-
return $vcard;
}
@@ -612,8 +617,8 @@ class rcube_vcard
$enc = null;
foreach($regs2[1] as $attrid => $attr) {
+ $attr = preg_replace('/[\s\t\n\r\0\x0B]/', '', $attr);
if ((list($key, $value) = explode('=', $attr)) && $value) {
- $value = trim($value);
if ($key == 'ENCODING') {
$value = strtoupper($value);
// add next line(s) to value string if QP line end detected
@@ -792,7 +797,7 @@ class rcube_vcard
return $result;
}
- $s = strtr($s, $rep2);
+ $s = trim(strtr($s, $rep2));
}
// some implementations (GMail) use non-standard backslash before colon (#1489085)
diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
index e7467545f..51f7930aa 100644
--- a/program/lib/Roundcube/rcube_washtml.php
+++ b/program/lib/Roundcube/rcube_washtml.php
@@ -184,7 +184,7 @@ class rcube_washtml
'|rgb\(\s*[0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*\)'.
'|-?[0-9.]+\s*(em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)?'.
'|#[0-9a-f]{3,6}'.
- '|[a-z0-9", -]+'.
+ '|[a-z0-9"\', -]+'.
')\s*/i', $str, $match)
) {
if ($match[2]) {
@@ -418,7 +418,7 @@ class rcube_washtml
$html = preg_replace($html_search, $html_replace, trim($html));
//-> Replace all of those weird MS Word quotes and other high characters
- $badwordchars=array(
+ $badwordchars = array(
"\xe2\x80\x98", // left single quote
"\xe2\x80\x99", // right single quote
"\xe2\x80\x9c", // left double quote
@@ -426,7 +426,7 @@ class rcube_washtml
"\xe2\x80\x94", // em dash
"\xe2\x80\xa6" // elipses
);
- $fixedwordchars=array(
+ $fixedwordchars = array(
"'",
"'",
'"',
@@ -434,7 +434,7 @@ class rcube_washtml
'&mdash;',
'...'
);
- $html = str_replace($badwordchars,$fixedwordchars, $html);
+ $html = str_replace($badwordchars, $fixedwordchars, $html);
// PCRE errors handling (#1486856), should we use something like for every preg_* use?
if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
@@ -455,13 +455,16 @@ class rcube_washtml
}
// fix (unknown/malformed) HTML tags before "wash"
- $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html);
+ $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
// Remove invalid HTML comments (#1487759)
// Don't remove valid conditional comments
// Don't remove MSOutlook (<!-->) conditional comments (#1489004)
$html = preg_replace('/<!--[^->\[\n]+>/', '', $html);
+ // fix broken nested lists
+ self::fix_broken_lists($html);
+
// turn relative into absolute urls
$html = self::resolve_base($html);
@@ -479,7 +482,12 @@ class rcube_washtml
'/[^a-z0-9_\[\]\!-]/i', // forbidden characters
), '', $tagname);
- return $matches[1] . $tagname;
+ // fix invalid closing tags - remove any attributes (#1489446)
+ if ($matches[1] == '</') {
+ $matches[3] = '';
+ }
+
+ return $matches[1] . $tagname . $matches[3];
}
/**
@@ -495,5 +503,77 @@ class rcube_washtml
return $body;
}
-}
+ /**
+ * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
+ */
+ public static function fix_broken_lists(&$html)
+ {
+ // do two rounds, one for <ol>, one for <ul>
+ foreach (array('ol', 'ul') as $tag) {
+ $pos = 0;
+ while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
+ $pos++;
+
+ // make sure this is an ol/ul tag
+ if (!in_array($html[$pos+2], array(' ', '>'))) {
+ continue;
+ }
+
+ $p = $pos;
+ $in_li = false;
+ $li_pos = 0;
+
+ while (($p = strpos($html, '<', $p)) !== false) {
+ $tt = strtolower(substr($html, $p, 4));
+
+ // li open tag
+ if ($tt == '<li>' || $tt == '<li ') {
+ $in_li = true;
+ $p += 4;
+ }
+ // li close tag
+ else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
+ $li_pos = $p;
+ $p += 4;
+ $in_li = false;
+ }
+ // ul/ol closing tag
+ else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
+ break;
+ }
+ // nested ol/ul element out of li
+ else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
+ // find closing tag of this ul/ol element
+ $element = substr($tt, 1, 2);
+ $cpos = $p;
+ do {
+ $tpos = stripos($html, '<' . $element, $cpos+1);
+ $cpos = stripos($html, '</' . $element, $cpos+1);
+ }
+ while ($tpos !== false && $cpos !== false && $cpos > $tpos);
+
+ // not found, this is invalid HTML, skip it
+ if ($cpos === false) {
+ break;
+ }
+
+ // get element content
+ $end = strpos($html, '>', $cpos);
+ $len = $end - $p + 1;
+ $element = substr($html, $p, $len);
+
+ // move element to the end of the last li
+ $html = substr_replace($html, '', $p, $len);
+ $html = substr_replace($html, $element, $li_pos, 0);
+
+ $p = $end;
+ }
+ else {
+ $p++;
+ }
+ }
+ }
+ }
+ }
+}