Branch 'kolab-syncroton-2.2' - 5 commits - lib/ext lib/kolab_sync.php
Jeroen van Meeuwen
vanmeeuwen at kolabsys.com
Tue Nov 12 14:06:17 CET 2013
lib/ext/Roundcube/html.php | 9 +++++----
lib/kolab_sync.php | 2 +-
2 files changed, 6 insertions(+), 5 deletions(-)
New commits:
commit b2bf9c32dc68ad427f083fd592011df86d21d517
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date: Tue Nov 12 14:05:45 2013 +0100
Bump version to 2.2.3
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index d768ed0..4f19cb0 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -43,7 +43,7 @@ class kolab_sync extends rcube
public $user;
const CHARSET = 'UTF-8';
- const VERSION = "2.2.2";
+ const VERSION = "2.2.3";
/**
commit 4a58eb4c5d8041fea1bd6b5c1ed8c584640fd6a3
Merge: 7601b25 24f6cf4
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date: Tue Nov 12 14:05:23 2013 +0100
Merge branch 'kolab-syncroton-2.2' of ssh://git.kolab.org/git/kolab-syncroton into kolab-syncroton-2.2
Conflicts:
lib/plugins/libkolab/SQL/mysql.initial.sql
lib/plugins/libkolab/lib/kolab_format.php
lib/plugins/libkolab/lib/kolab_storage_cache.php
commit 7601b25834eab797d2b29a32f5b4b03f103ddd7b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date: Thu Oct 17 18:20:27 2013 +0200
Set version to 2.2.2
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 52c8b8a..d768ed0 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -43,7 +43,7 @@ class kolab_sync extends rcube
public $user;
const CHARSET = 'UTF-8';
- const VERSION = "2.2.1";
+ const VERSION = "2.2.2";
/**
commit ba11518280a7d6c667f72bd5a28fcde767e5b143
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date: Thu Oct 17 18:19:46 2013 +0200
Rebase lib/ext/Roundcube with cache refactoring
diff --git a/lib/ext/Roundcube/bootstrap.php b/lib/ext/Roundcube/bootstrap.php
index 182ea12..6e51433 100644
--- a/lib/ext/Roundcube/bootstrap.php
+++ b/lib/ext/Roundcube/bootstrap.php
@@ -26,25 +26,25 @@
*/
$config = array(
- 'error_reporting' => E_ALL &~ (E_NOTICE | E_STRICT),
+ 'error_reporting' => E_ALL & ~E_NOTICE & ~E_STRICT,
// Some users are not using Installer, so we'll check some
// critical PHP settings here. Only these, which doesn't provide
// an error/warning in the logs later. See (#1486307).
'mbstring.func_overload' => 0,
- 'magic_quotes_runtime' => 0,
- 'magic_quotes_sybase' => 0, // #1488506
+ 'magic_quotes_runtime' => false,
+ 'magic_quotes_sybase' => false, // #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,
+ 'suhosin.session.encrypt' => false,
+ 'file_uploads' => true,
);
}
foreach ($config as $optname => $optval) {
- $ini_optval = filter_var(ini_get($optname), FILTER_VALIDATE_BOOLEAN);
+ $ini_optval = filter_var(ini_get($optname), is_bool($optval) ? FILTER_VALIDATE_BOOLEAN : FILTER_VALIDATE_INT);
if ($optval != $ini_optval && @ini_set($optname, $optval) === false) {
$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).";
@@ -58,7 +58,7 @@ define('RCUBE_VERSION', '1.0-git');
define('RCUBE_CHARSET', 'UTF-8');
if (!defined('RCUBE_LIB_DIR')) {
- define('RCUBE_LIB_DIR', dirname(__FILE__).'/');
+ define('RCUBE_LIB_DIR', dirname(__FILE__).DIRECTORY_SEPARATOR);
}
if (!defined('RCUBE_INSTALL_PATH')) {
@@ -83,6 +83,16 @@ if (extension_loaded('mbstring')) {
@mb_regex_encoding(RCUBE_CHARSET);
}
+// make sure the Roundcube lib directory is in the include_path
+$rcube_path = realpath(RCUBE_LIB_DIR . '..');
+$sep = PATH_SEPARATOR;
+$regexp = "!(^|$sep)" . preg_quote($rcube_path, '!') . "($sep|\$)!";
+$path = ini_get('include_path');
+
+if (!preg_match($regexp, $path)) {
+ set_include_path($path . PATH_SEPARATOR . $rcube_path);
+}
+
// Register autoloader
spl_autoload_register('rcube_autoload');
diff --git a/lib/ext/Roundcube/html.php b/lib/ext/Roundcube/html.php
index a367112..4f87d25 100644
--- a/lib/ext/Roundcube/html.php
+++ b/lib/ext/Roundcube/html.php
@@ -604,16 +604,17 @@ class html_select extends html
*
* @param mixed $names Option name or array with option names
* @param mixed $values Option value or array with option values
+ * @param array $attrib Additional attributes for the option entry
*/
- public function add($names, $values = null)
+ public function add($names, $values = null, $attrib = array())
{
if (is_array($names)) {
foreach ($names as $i => $text) {
- $this->options[] = array('text' => $text, 'value' => $values[$i]);
+ $this->options[] = array('text' => $text, 'value' => $values[$i]) + $attrib;
}
}
else {
- $this->options[] = array('text' => $names, 'value' => $values);
+ $this->options[] = array('text' => $names, 'value' => $values) + $attrib;
}
}
@@ -644,7 +645,7 @@ class html_select extends html
$option_content = self::quote($option_content);
}
- $this->content .= self::tag('option', $attr, $option_content);
+ $this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected'));
}
return parent::show();
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index e0f889a..399f84f 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -467,6 +467,10 @@ class rcube
$this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->config->get('ip_check'));
+ if ($this->config->get('session_auth_name')) {
+ $this->session->set_cookiename($this->config->get('session_auth_name'));
+ }
+
// start PHP session (if not in CLI mode)
if ($_SERVER['REMOTE_ADDR']) {
$this->session->start();
@@ -494,7 +498,14 @@ class rcube
public function gc_temp()
{
$tmp = unslashify($this->config->get('temp_dir'));
- $expire = time() - 172800; // expire in 48 hours
+
+ // expire in 48 hours by default
+ $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
+ $temp_dir_ttl = get_offset_sec($temp_dir_ttl);
+ if ($temp_dir_ttl < 6*3600)
+ $temp_dir_ttl = 6*3600; // 6 hours sensible lower bound.
+
+ $expire = time() - $temp_dir_ttl;
if ($tmp && ($dir = opendir($tmp))) {
while (($fname = readdir($dir)) !== false) {
@@ -691,7 +702,11 @@ class rcube
// user HTTP_ACCEPT_LANGUAGE if no language is specified
if (empty($lang) || $lang == 'auto') {
$accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
- $lang = str_replace('-', '_', $accept_langs[0]);
+ $lang = $accept_langs[0];
+
+ if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
+ $lang = $m[1] . '_' . strtoupper($m[2]);
+ }
}
if (empty($rcube_languages)) {
@@ -1121,8 +1136,8 @@ class rcube
* - code: Error code (required)
* - type: Error type [php|db|imap|javascript] (required)
* - message: Error message
- * - file: File where error occured
- * - line: Line where error occured
+ * - file: File where error occurred
+ * - line: Line where error occurred
* @param boolean True to log the error
* @param boolean Terminate script execution
*/
@@ -1393,6 +1408,10 @@ class rcube
'options' => $options,
));
+ if ($plugin['abort']) {
+ return isset($plugin['result']) ? $plugin['result'] : false;
+ }
+
$from = $plugin['from'];
$mailto = $plugin['mailto'];
$options = $plugin['options'];
diff --git a/lib/ext/Roundcube/rcube_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php
index 4ed139c..6e2b439 100644
--- a/lib/ext/Roundcube/rcube_addressbook.php
+++ b/lib/ext/Roundcube/rcube_addressbook.php
@@ -3,7 +3,7 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2012, The Roundcube Dev Team |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -35,6 +35,7 @@ abstract class rcube_addressbook
/** public properties (mandatory) */
public $primary_key;
public $groups = false;
+ public $export_groups = true;
public $readonly = true;
public $searchonly = false;
public $undelete = false;
@@ -133,7 +134,7 @@ abstract class rcube_addressbook
abstract function get_record($id, $assoc=false);
/**
- * Returns the last error occured (e.g. when updating/inserting failed)
+ * Returns the last error occurred (e.g. when updating/inserting failed)
*
* @return array Hash array with the following fields: type, message
*/
@@ -423,7 +424,7 @@ abstract class rcube_addressbook
* @param boolean True to return one array with all values, False for hash array with values grouped by type
* @return array List of column values
*/
- function get_col_values($col, $data, $flat = false)
+ public static function get_col_values($col, $data, $flat = false)
{
$out = array();
foreach ((array)$data as $c => $values) {
@@ -476,7 +477,8 @@ abstract class rcube_addressbook
$fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
// use email address part for name
- $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
+ $email = self::get_col_values('email', $contact, true);
+ $email = $email[0];
if ($email && (empty($fn) || $fn == $email)) {
// return full email
@@ -523,9 +525,9 @@ abstract class rcube_addressbook
$fn = $contact['name'];
// fallback to email address
- $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
- if (empty($fn) && $email)
- return $email;
+ if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
+ return $email[0];
+ }
return $fn;
}
@@ -538,8 +540,8 @@ abstract class rcube_addressbook
$key = $contact[$sort_col] . ':' . $contact['sourceid'];
// add email to a key to not skip contacts with the same name (#1488375)
- if (!empty($contact['email'])) {
- $key .= ':' . implode(':', (array)$contact['email']);
+ if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
+ $key .= ':' . implode(':', (array)$email);
}
return $key;
@@ -561,9 +563,9 @@ abstract class rcube_addressbook
// use only strict comparison (mode = 1)
// @TODO: partial search, e.g. match only day and month
if (in_array($colname, $this->date_cols)) {
- return (($value = rcube_utils::strtotime($value))
- && ($search = rcube_utils::strtotime($search))
- && date('Ymd', $value) == date('Ymd', $search));
+ return (($value = rcube_utils::anytodatetime($value))
+ && ($search = rcube_utils::anytodatetime($search))
+ && $value->format('Ymd') == $search->format('Ymd'));
}
// composite field, e.g. address
diff --git a/lib/ext/Roundcube/rcube_base_replacer.php b/lib/ext/Roundcube/rcube_base_replacer.php
index a59bba9..fa67647 100644
--- a/lib/ext/Roundcube/rcube_base_replacer.php
+++ b/lib/ext/Roundcube/rcube_base_replacer.php
@@ -90,8 +90,8 @@ class rcube_base_replacer
if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
foreach ($matches as $a_match) {
- if (strrpos($base_url, '/')) {
- $base_url = substr($base_url, 0, strrpos($base_url, '/'));
+ if ($pos = strrpos($base_url, '/')) {
+ $base_url = substr($base_url, 0, $pos);
}
$path = substr($path, 3);
}
diff --git a/lib/ext/Roundcube/rcube_config.php b/lib/ext/Roundcube/rcube_config.php
index 3cf4da8..04b914c 100644
--- a/lib/ext/Roundcube/rcube_config.php
+++ b/lib/ext/Roundcube/rcube_config.php
@@ -3,7 +3,7 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -27,7 +27,7 @@ class rcube_config
const DEFAULT_SKIN = 'larry';
private $env = '';
- private $basedir = 'config/';
+ private $paths = array();
private $prop = array();
private $errors = array();
private $userprefs = array();
@@ -58,7 +58,32 @@ class rcube_config
public function __construct($env = '')
{
$this->env = $env;
- $this->basedir = RCUBE_CONFIG_DIR;
+
+ if ($paths = getenv('RCUBE_CONFIG_PATH')) {
+ $this->paths = explode(PATH_SEPARATOR, $paths);
+ // make all paths absolute
+ foreach ($this->paths as $i => $path) {
+ if (!$this->_is_absolute($path)) {
+ if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
+ $this->paths[$i] = unslashify($realpath) . '/';
+ }
+ else {
+ unset($this->paths[$i]);
+ }
+ }
+ else {
+ $this->paths[$i] = unslashify($path) . '/';
+ }
+ }
+ }
+
+ if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) {
+ $this->paths[] = RCUBE_CONFIG_DIR;
+ }
+
+ if (empty($this->paths)) {
+ $this->paths[] = RCUBE_INSTALL_PATH . 'config/';
+ }
$this->load();
@@ -94,8 +119,7 @@ class rcube_config
}
// load host-specific configuration
- if (!empty($_SERVER['HTTP_HOST']))
- $this->load_host_config();
+ $this->load_host_config();
// set skin (with fallback to old 'skin_path' property)
if (empty($this->prop['skin'])) {
@@ -138,17 +162,6 @@ class rcube_config
// enable display_errors in 'show' level, but not for ajax requests
ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4)));
- // set timezone auto settings values
- if ($this->prop['timezone'] == 'auto') {
- $this->prop['_timezone_value'] = $this->client_timezone();
- }
- else if (is_numeric($this->prop['timezone']) && ($tz = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0))) {
- $this->prop['timezone'] = $tz;
- }
- else if (empty($this->prop['timezone'])) {
- $this->prop['timezone'] = 'UTC';
- }
-
// remove deprecated properties
unset($this->prop['dst_active']);
@@ -162,21 +175,31 @@ class rcube_config
*/
private function load_host_config()
{
- $fname = null;
-
- if (is_array($this->prop['include_host_config'])) {
- $fname = $this->prop['include_host_config'][$_SERVER['HTTP_HOST']];
- }
- else if (!empty($this->prop['include_host_config'])) {
- $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $_SERVER['HTTP_HOST']) . '.inc.php';
+ if (empty($this->prop['include_host_config'])) {
+ return;
}
- if ($fname) {
- $this->load_from_file($fname);
+ foreach (array('HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR') as $key) {
+ $fname = null;
+ $name = $_SERVER[$key];
+
+ if (!$name) {
+ continue;
+ }
+
+ if (is_array($this->prop['include_host_config'])) {
+ $fname = $this->prop['include_host_config'][$name];
+ }
+ else {
+ $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $name) . '.inc.php';
+ }
+
+ if ($fname && $this->load_from_file($fname)) {
+ return;
+ }
}
}
-
/**
* Read configuration from a file
* and merge with the already stored config values
@@ -186,51 +209,73 @@ class rcube_config
*/
public function load_from_file($file)
{
- $fpath = $this->resolve_path($file);
- if ($fpath && (is_file($fpath) || file_exists($fpath)) && is_readable($fpath)) {
- // use output buffering, we don't need any output here
- ob_start();
- include($fpath);
- ob_end_clean();
-
- if (is_array($config)) {
- $this->merge($config);
- return true;
- }
- // deprecated name of config variable
- else if (is_array($rcmail_config)) {
- $this->merge($rcmail_config);
- return true;
+ $success = false;
+
+ foreach ($this->resolve_paths($file) as $fpath) {
+ if ($fpath && is_file($fpath) && is_readable($fpath)) {
+ // use output buffering, we don't need any output here
+ ob_start();
+ include($fpath);
+ ob_end_clean();
+
+ if (is_array($config)) {
+ $this->merge($config);
+ $success = true;
+ }
+ // deprecated name of config variable
+ if (is_array($rcmail_config)) {
+ $this->merge($rcmail_config);
+ $success = true;
+ }
}
}
- return false;
+ return $success;
}
/**
- * Helper method to resolve the absolute path to the given config file.
+ * Helper method to resolve absolute paths to the given config file.
* This also takes the 'env' property into account.
+ *
+ * @param string Filename or absolute file path
+ * @param boolean Return -$env file path if exists
+ * @return array List of candidates in config dir path(s)
*/
- public function resolve_path($file, $use_env = true)
+ public function resolve_paths($file, $use_env = true)
{
- if (strpos($file, '/') === false) {
- $file = rtrim($this->basedir, '/') . '/' . $file;
+ $files = array();
+ $abs_path = $this->_is_absolute($file);
- if (!realpath($file) === false) {
- $file = realpath($file);
+ foreach ($this->paths as $basepath) {
+ $realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
+
+ // check if <file>-env.ini exists
+ if ($realpath && $use_env && !empty($this->env)) {
+ $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $realpath);
+ if (is_file($envfile))
+ $realpath = $envfile;
}
- }
- // check if <file>-env.ini exists
- if ($file && $use_env && !empty($this->env)) {
- $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $file);
- if (is_file($envfile))
- return $envfile;
+ if ($realpath) {
+ $files[] = $realpath;
+
+ // no need to continue the loop if an absolute file path is given
+ if ($abs_path) {
+ break;
+ }
+ }
}
- return $file;
+ return $files;
}
+ /**
+ * 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
@@ -250,8 +295,10 @@ class rcube_config
$rcube = rcube::get_instance();
- if ($name == 'timezone' && isset($this->prop['_timezone_value'])) {
- $result = $this->prop['_timezone_value'];
+ if ($name == 'timezone') {
+ if (empty($result) || $result == 'auto') {
+ $result = $this->client_timezone();
+ }
}
else if ($name == 'client_mimetypes') {
if ($result == null && $def == null)
@@ -286,8 +333,8 @@ class rcube_config
*/
public function merge($prefs)
{
+ $prefs = $this->fix_legacy_props($prefs);
$this->prop = array_merge($this->prop, $prefs, $this->userprefs);
- $this->fix_legacy_props();
}
@@ -299,6 +346,8 @@ class rcube_config
*/
public function set_user_prefs($prefs)
{
+ $prefs = $this->fix_legacy_props($prefs);
+
// Honor the dont_override setting for any existing user preferences
$dont_override = $this->get('dont_override');
if (is_array($dont_override) && !empty($dont_override)) {
@@ -307,11 +356,6 @@ class rcube_config
}
}
- // convert user's timezone into the new format
- if (is_numeric($prefs['timezone']) && ($tz = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0))) {
- $prefs['timezone'] = $tz;
- }
-
// larry is the new default skin :-)
if ($prefs['skin'] == 'default') {
$prefs['skin'] = self::DEFAULT_SKIN;
@@ -319,22 +363,13 @@ 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'];
- }
- else if (isset($this->prop['_timezone_value']))
- unset($this->prop['_timezone_value']);
}
/**
* Getter for all config options
*
- * @return array Hash array containg all config properties
+ * @return array Hash array containing all config properties
*/
public function all()
{
@@ -468,13 +503,12 @@ class rcube_config
*/
private function client_timezone()
{
- if (isset($_SESSION['timezone']) && is_numeric($_SESSION['timezone'])
- && ($ctz = timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0))) {
- return $ctz;
- }
- else if (!empty($_SESSION['timezone'])) {
+ // @TODO: remove this legacy timezone handling in the future
+ $props = $this->fix_legacy_props(array('timezone' => $_SESSION['timezone']));
+
+ if (!empty($props['timezone'])) {
try {
- $tz = timezone_open($_SESSION['timezone']);
+ $tz = new DateTimeZone($props['timezone']);
return $tz->getName();
}
catch (Exception $e) { /* gracefully ignore */ }
@@ -486,16 +520,93 @@ class rcube_config
/**
* Convert legacy options into new ones
+ *
+ * @param array $props Hash array with config props
+ *
+ * @return array Converted config props
*/
- private function fix_legacy_props()
+ private function fix_legacy_props($props)
{
foreach ($this->legacy_props as $new => $old) {
- if (isset($this->prop[$old])) {
- if (!isset($this->prop[$new])) {
- $this->prop[$new] = $this->prop[$old];
+ if (isset($props[$old])) {
+ if (!isset($props[$new])) {
+ $props[$new] = $props[$old];
}
- unset($this->prop[$old]);
+ unset($props[$old]);
+ }
+ }
+
+ // convert deprecated numeric timezone value
+ if (isset($props['timezone']) && is_numeric($props['timezone'])) {
+ if ($tz = self::timezone_name_from_abbr($props['timezone'])) {
+ $props['timezone'] = $tz;
+ }
+ else {
+ unset($props['timezone']);
}
}
+
+ return $props;
+ }
+
+ /**
+ * timezone_name_from_abbr() replacement. Converts timezone offset
+ * into timezone name abbreviation.
+ *
+ * @param float $offset Timezone offset (in hours)
+ *
+ * @return string Timezone abbreviation
+ */
+ static public function timezone_name_from_abbr($offset)
+ {
+ // List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780
+ if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) {
+ return $tz;
+ }
+
+ // try with more complete list (#1489261)
+ $timezones = array(
+ '-660' => "Pacific/Apia",
+ '-600' => "Pacific/Honolulu",
+ '-570' => "Pacific/Marquesas",
+ '-540' => "America/Anchorage",
+ '-480' => "America/Los_Angeles",
+ '-420' => "America/Denver",
+ '-360' => "America/Chicago",
+ '-300' => "America/New_York",
+ '-270' => "America/Caracas",
+ '-240' => "America/Halifax",
+ '-210' => "Canada/Newfoundland",
+ '-180' => "America/Sao_Paulo",
+ '-60' => "Atlantic/Azores",
+ '0' => "Europe/London",
+ '60' => "Europe/Paris",
+ '120' => "Europe/Helsinki",
+ '180' => "Europe/Moscow",
+ '210' => "Asia/Tehran",
+ '240' => "Asia/Dubai",
+ '300' => "Asia/Karachi",
+ '270' => "Asia/Kabul",
+ '300' => "Asia/Karachi",
+ '330' => "Asia/Kolkata",
+ '345' => "Asia/Katmandu",
+ '360' => "Asia/Yekaterinburg",
+ '390' => "Asia/Rangoon",
+ '420' => "Asia/Krasnoyarsk",
+ '480' => "Asia/Shanghai",
+ '525' => "Australia/Eucla",
+ '540' => "Asia/Tokyo",
+ '570' => "Australia/Adelaide",
+ '600' => "Australia/Melbourne",
+ '630' => "Australia/Lord_Howe",
+ '660' => "Asia/Vladivostok",
+ '690' => "Pacific/Norfolk",
+ '720' => "Pacific/Auckland",
+ '765' => "Pacific/Chatham",
+ '780' => "Pacific/Enderbury",
+ '840' => "Pacific/Kiritimati",
+ );
+
+ return $timezones[(string) intval($offset * 60)];
}
}
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index 3919cdc..6d01368 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -718,6 +718,10 @@ class rcube_contacts extends rcube_addressbook
foreach ($save_data as $key => $values) {
list($field, $section) = explode(':', $key);
$fulltext = in_array($field, $this->fulltext_cols);
+ // avoid casting DateTime objects to array
+ if (is_object($values) && is_a($values, 'DateTime')) {
+ $values = array(0 => $values);
+ }
foreach ((array)$values as $value) {
if (isset($value))
$vcard->set($field, $value, $section);
diff --git a/lib/ext/Roundcube/rcube_csv2vcard.php b/lib/ext/Roundcube/rcube_csv2vcard.php
index fb8d8f1..00e6d4e 100644
--- a/lib/ext/Roundcube/rcube_csv2vcard.php
+++ b/lib/ext/Roundcube/rcube_csv2vcard.php
@@ -145,6 +145,7 @@ class rcube_csv2vcard
'work_mobile' => 'phone:work,cell',
'work_title' => 'jobtitle',
'work_zip' => 'zipcode:work',
+ 'group' => 'groups',
);
/**
@@ -268,6 +269,7 @@ class rcube_csv2vcard
'work_mobile' => "Work Mobile",
'work_title' => "Work Title",
'work_zip' => "Work Zip",
+ 'groups' => "Group",
);
protected $local_label_map = array();
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
index 8520700..aaba281 100644
--- a/lib/ext/Roundcube/rcube_db.php
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -31,7 +31,10 @@ class rcube_db
protected $db_dsnr; // DSN for read operations
protected $db_connected = false; // Already connected ?
protected $db_mode; // Connection mode
+ protected $db_table_dsn_map = array();
protected $dbh; // Connection handle
+ protected $dbhs = array();
+ protected $table_connections = array();
protected $db_error = false;
protected $db_error_msg = '';
@@ -97,9 +100,12 @@ class rcube_db
$this->db_dsnw = $db_dsnw;
$this->db_dsnr = $db_dsnr;
$this->db_pconn = $pconn;
+ $this->db_dsnw_noread = rcube::get_instance()->config->get('db_dsnw_noread', false);
$this->db_dsnw_array = self::parse_dsn($db_dsnw);
$this->db_dsnr_array = self::parse_dsn($db_dsnr);
+
+ $this->db_table_dsn_map = array_map(array($this, 'table_name'), rcube::get_instance()->config->get('db_table_dsn', array()));
}
/**
@@ -113,6 +119,13 @@ class rcube_db
$this->db_error = false;
$this->db_error_msg = null;
+ // return existing handle
+ if ($this->dbhs[$mode]) {
+ $this->dbh = $this->dbhs[$mode];
+ $this->db_mode = $mode;
+ return $this->dbh;
+ }
+
// Get database specific connection options
$dsn_string = $this->dsn_string($dsn);
$dsn_options = $this->dsn_options($dsn);
@@ -147,6 +160,7 @@ class rcube_db
}
$this->dbh = $dbh;
+ $this->dbhs[$mode] = $dbh;
$this->db_mode = $mode;
$this->db_connected = true;
$this->conn_configure($dsn, $dbh);
@@ -175,8 +189,9 @@ class rcube_db
* Connect to appropriate database depending on the operation
*
* @param string $mode Connection mode (r|w)
+ * @param boolean $force Enforce using the given mode
*/
- public function db_connect($mode)
+ public function db_connect($mode, $force = false)
{
// previous connection failed, don't attempt to connect again
if ($this->conn_failure) {
@@ -190,14 +205,13 @@ class rcube_db
// Already connected
if ($this->db_connected) {
- // connected to db with the same or "higher" mode
- if ($this->db_mode == 'w' || $this->db_mode == $mode) {
+ // connected to db with the same or "higher" mode (if allowed)
+ if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->db_dsnw_noread) {
return;
}
}
$dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
-
$this->dsn_connect($dsn, $mode);
// use write-master when read-only fails
@@ -209,6 +223,46 @@ class rcube_db
}
/**
+ * Analyze the given SQL statement and select the appropriate connection to use
+ */
+ protected function dsn_select($query)
+ {
+ // no replication
+ if ($this->db_dsnw == $this->db_dsnr) {
+ return 'w';
+ }
+
+ // Read or write ?
+ $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
+
+ // find tables involved in this query
+ if (preg_match_all('/(?:^|\s)(from|update|into|join)\s+'.$this->options['identifier_start'].'?([a-z0-9._]+)'.$this->options['identifier_end'].'?\s+/i', $query, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $m) {
+ $table = $m[2];
+
+ // always use direct mapping
+ if ($this->db_table_dsn_map[$table]) {
+ $mode = $this->db_table_dsn_map[$table];
+ break; // primary table rules
+ }
+ else if ($mode == 'r') {
+ // connected to db with the same or "higher" mode for this table
+ $db_mode = $this->table_connections[$table];
+ if ($db_mode == 'w' && !$this->db_dsnw_noread) {
+ $mode = $db_mode;
+ }
+ }
+ }
+
+ // remember mode chosen (for primary table)
+ $table = $matches[0][2];
+ $this->table_connections[$table] = $mode;
+ }
+
+ return $mode;
+ }
+
+ /**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if SQL queries should be logged
@@ -340,10 +394,7 @@ class rcube_db
{
$query = trim($query);
- // Read or write ?
- $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
-
- $this->db_connect($mode);
+ $this->db_connect($this->dsn_select($query), true);
// check connection before proceeding
if (!$this->is_connected()) {
@@ -386,17 +437,7 @@ class rcube_db
$result = $this->dbh->query($query);
if ($result === false) {
- $error = $this->dbh->errorInfo();
-
- 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);
- }
+ $result = $this->handle_error($query);
}
$this->last_result = $result;
@@ -405,6 +446,30 @@ class rcube_db
}
/**
+ * Helper method to handle DB errors.
+ * This by default logs the error but could be overriden by a driver implementation
+ *
+ * @param string Query that triggered the error
+ * @return mixed Result to be stored and returned
+ */
+ protected function handle_error($query)
+ {
+ $error = $this->dbh->errorInfo();
+
+ if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) {
+ $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);
+ }
+
+ return false;
+ }
+
+ /**
* Get number of affected rows for the last query
*
* @param mixed $result Optional query handle
@@ -854,10 +919,14 @@ class rcube_db
*/
public function table_name($table)
{
- $rcube = rcube::get_instance();
+ static $rcube;
+
+ if (!$rcube) {
+ $rcube = rcube::get_instance();
+ }
// add prefix to the table name if configured
- if ($prefix = $rcube->config->get('db_prefix')) {
+ if (($prefix = $rcube->config->get('db_prefix')) && strpos($table, $prefix) !== 0) {
return $prefix . $table;
}
@@ -876,6 +945,17 @@ class rcube_db
}
/**
+ * Set DSN connection to be used for the given table
+ *
+ * @param string Table name
+ * @param string DSN connection ('r' or 'w') to be used
+ */
+ public function set_table_dsn($table, $mode)
+ {
+ $this->db_table_dsn_map[$this->table_name($table)] = $mode;
+ }
+
+ /**
* MDB2 DSN string parser
*
* @param string $sequence Secuence name
diff --git a/lib/ext/Roundcube/rcube_db_mssql.php b/lib/ext/Roundcube/rcube_db_mssql.php
index 3c1b9d7..726e4b4 100644
--- a/lib/ext/Roundcube/rcube_db_mssql.php
+++ b/lib/ext/Roundcube/rcube_db_mssql.php
@@ -52,7 +52,7 @@ class rcube_db_mssql extends rcube_db
protected function conn_configure($dsn, $dbh)
{
// Set date format in case of non-default language (#1488918)
- $this->query("SET DATEFORMAT ymd");
+ $dbh->query("SET DATEFORMAT ymd");
}
/**
diff --git a/lib/ext/Roundcube/rcube_db_mysql.php b/lib/ext/Roundcube/rcube_db_mysql.php
index 6fa5ad7..d3d0ac5 100644
--- a/lib/ext/Roundcube/rcube_db_mysql.php
+++ b/lib/ext/Roundcube/rcube_db_mysql.php
@@ -60,7 +60,7 @@ class rcube_db_mysql extends rcube_db
*/
protected function conn_configure($dsn, $dbh)
{
- $this->query("SET NAMES 'utf8'");
+ $dbh->query("SET NAMES 'utf8'");
}
/**
@@ -179,4 +179,29 @@ class rcube_db_mysql extends rcube_db
return isset($this->variables[$varname]) ? $this->variables[$varname] : $default;
}
+ /**
+ * Handle DB errors, re-issue the query on deadlock errors from InnoDB row-level locking
+ *
+ * @param string Query that triggered the error
+ * @return mixed Result to be stored and returned
+ */
+ protected function handle_error($query)
+ {
+ $error = $this->dbh->errorInfo();
+
+ // retry after "Deadlock found when trying to get lock" errors
+ $retries = 2;
+ while ($error[1] == 1213 && $retries >= 0) {
+ usleep(50000); // wait 50 ms
+ $result = $this->dbh->query($query);
+ if ($result !== false) {
+ return $result;
+ }
+ $error = $this->dbh->errorInfo();
+ $retries--;
+ }
+
+ return parent::handle_error($query);
+ }
+
}
diff --git a/lib/ext/Roundcube/rcube_db_pgsql.php b/lib/ext/Roundcube/rcube_db_pgsql.php
index d72c9d6..68bf6d8 100644
--- a/lib/ext/Roundcube/rcube_db_pgsql.php
+++ b/lib/ext/Roundcube/rcube_db_pgsql.php
@@ -36,7 +36,7 @@ class rcube_db_pgsql extends rcube_db
*/
protected function conn_configure($dsn, $dbh)
{
- $this->query("SET NAMES 'utf8'");
+ $dbh->query("SET NAMES 'utf8'");
}
/**
diff --git a/lib/ext/Roundcube/rcube_db_sqlsrv.php b/lib/ext/Roundcube/rcube_db_sqlsrv.php
index 45c41cd..4339f3d 100644
--- a/lib/ext/Roundcube/rcube_db_sqlsrv.php
+++ b/lib/ext/Roundcube/rcube_db_sqlsrv.php
@@ -52,7 +52,7 @@ class rcube_db_sqlsrv extends rcube_db
protected function conn_configure($dsn, $dbh)
{
// Set date format in case of non-default language (#1488918)
- $this->query("SET DATEFORMAT ymd");
+ $dbh->query("SET DATEFORMAT ymd");
}
/**
diff --git a/lib/ext/Roundcube/rcube_html2text.php b/lib/ext/Roundcube/rcube_html2text.php
index 9b248a3..6f79e2f 100644
--- a/lib/ext/Roundcube/rcube_html2text.php
+++ b/lib/ext/Roundcube/rcube_html2text.php
@@ -611,11 +611,13 @@ class rcube_html2text
$body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body));
$body = '<pre>' . htmlspecialchars($body) . '</pre>';
- $text = substr($text, 0, $start) . $body . "\n" . substr($text, $end + 13);
+ $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
$offset = 0;
+
break;
}
- } while ($end || $next);
+ }
+ while ($end || $next);
}
}
@@ -624,8 +626,9 @@ class rcube_html2text
*/
public function blockquote_citation_ballback($m)
{
- $line = ltrim($m[2]);
+ $line = ltrim($m[2]);
$space = $line[0] == '>' ? '' : ' ';
+
return $m[1] . '>' . $space . $line;
}
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index c5346c8..9faf1bb 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -70,7 +70,7 @@ class rcube_imap extends rcube_storage
protected $search_sort_field = '';
protected $search_threads = false;
protected $search_sorted = false;
- protected $options = array('auth_method' => 'check');
+ protected $options = array('auth_type' => 'check');
protected $caching = false;
protected $messages_caching = false;
protected $threading = false;
@@ -391,10 +391,10 @@ class rcube_imap extends rcube_storage
public function check_permflag($flag)
{
$flag = strtoupper($flag);
- $imap_flag = $this->conn->flags[$flag];
$perm_flags = $this->get_permflags($this->folder);
+ $imap_flag = $this->conn->flags[$flag];
- return in_array_nocase($imap_flag, $perm_flags);
+ return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
}
@@ -410,17 +410,7 @@ class rcube_imap extends rcube_storage
if (!strlen($folder)) {
return array();
}
-/*
- Checking PERMANENTFLAGS is rather rare, so we disable caching of it
- Re-think when we'll use it for more than only MDNSENT flag
- $cache_key = 'mailboxes.permanentflags.' . $folder;
- $permflags = $this->get_cache($cache_key);
-
- if ($permflags !== null) {
- return explode(' ', $permflags);
- }
-*/
if (!$this->check_connection()) {
return array();
}
@@ -435,10 +425,7 @@ class rcube_imap extends rcube_storage
if (!is_array($permflags)) {
$permflags = array();
}
-/*
- // Store permflags as string to limit cached object size
- $this->update_cache($cache_key, implode(' ', $permflags));
-*/
+
return $permflags;
}
@@ -3773,12 +3760,17 @@ class rcube_imap extends rcube_storage
/**
* Enable or disable messages caching
*
- * @param boolean $set Flag
+ * @param boolean $set Flag
+ * @param int $mode Cache mode
*/
- public function set_messages_caching($set)
+ public function set_messages_caching($set, $mode = null)
{
if ($set) {
$this->messages_caching = true;
+
+ if ($mode && ($cache = $this->get_mcache_engine())) {
+ $cache->set_mode($mode);
+ }
}
else {
if ($this->mcache) {
@@ -3798,9 +3790,10 @@ 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');
+ $ttl = $rcube->config->get('messages_cache_ttl', '10d');
+ $threshold = $rcube->config->get('messages_cache_threshold', 50);
$this->mcache = new rcube_imap_cache(
- $dbh, $this, $userid, $this->options['skip_deleted'], $ttl);
+ $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
}
}
@@ -3812,7 +3805,7 @@ class rcube_imap extends rcube_storage
* Clears the messages cache.
*
* @param string $folder Folder name
- * @param array $uids Optional message UIDs to remove from cache
+ * @param array $uids Optional message UIDs to remove from cache
*/
protected function clear_message_cache($folder = null, $uids = null)
{
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index 061ac54..a816654 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -27,6 +27,9 @@
*/
class rcube_imap_cache
{
+ const MODE_INDEX = 1;
+ const MODE_MESSAGE = 2;
+
/**
* Instance of rcube_imap
*
@@ -56,6 +59,13 @@ class rcube_imap_cache
private $ttl;
/**
+ * Maximum cached message size
+ *
+ * @var int
+ */
+ private $threshold;
+
+ /**
* Internal (in-memory) cache
*
* @var array
@@ -63,6 +73,7 @@ class rcube_imap_cache
private $icache = array();
private $skip_deleted = false;
+ private $mode;
/**
* List of known flags. Thanks to this we can handle flag changes
@@ -88,6 +99,7 @@ class rcube_imap_cache
);
+
/**
* Object constructor.
*
@@ -96,9 +108,9 @@ class rcube_imap_cache
* @param int $userid User identifier
* @param bool $skip_deleted skip_deleted flag
* @param string $ttl Expiration time of memcache/apc items
- *
+ * @param int $threshold Maximum cached message size
*/
- function __construct($db, $imap, $userid, $skip_deleted, $ttl=0)
+ function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
{
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
@@ -109,6 +121,10 @@ class rcube_imap_cache
$this->userid = $userid;
$this->skip_deleted = $skip_deleted;
$this->ttl = $ttl;
+ $this->threshold = $threshold;
+
+ // cache all possible information by default
+ $this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
}
@@ -123,6 +139,17 @@ class rcube_imap_cache
/**
+ * Set cache mode
+ *
+ * @param int $mode Cache mode
+ */
+ public function set_mode($mode)
+ {
+ $this->mode = $mode;
+ }
+
+
+ /**
* Return (sorted) messages index (UIDs).
* If index doesn't exist or is invalid, will be updated.
*
@@ -300,38 +327,46 @@ class rcube_imap_cache
return array();
}
- // Fetch messages from cache
- $sql_result = $this->db->query(
- "SELECT uid, data, flags"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
- $this->userid, $mailbox);
-
- $msgs = array_flip($msgs);
$result = array();
- while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $uid = intval($sql_arr['uid']);
- $result[$uid] = $this->build_message($sql_arr);
+ if ($this->mode & self::MODE_MESSAGE) {
+ // Fetch messages from cache
+ $sql_result = $this->db->query(
+ "SELECT uid, data, flags"
+ ." FROM ".$this->db->table_name('cache_messages')
+ ." WHERE user_id = ?"
+ ." AND mailbox = ?"
+ ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
+ $this->userid, $mailbox);
+
+ $msgs = array_flip($msgs);
+
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $uid = intval($sql_arr['uid']);
+ $result[$uid] = $this->build_message($sql_arr);
- if (!empty($result[$uid])) {
- // save memory, we don't need message body here (?)
- $result[$uid]->body = null;
+ if (!empty($result[$uid])) {
+ // save memory, we don't need message body here (?)
+ $result[$uid]->body = null;
- unset($msgs[$uid]);
+ unset($msgs[$uid]);
+ }
}
+
+ $msgs = array_flip($msgs);
}
// Fetch not found messages from IMAP server
if (!empty($msgs)) {
- $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true);
+ $messages = $this->imap->fetch_headers($mailbox, $msgs, false, true);
// Insert to DB and add to result list
if (!empty($messages)) {
foreach ($messages as $msg) {
- $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
+ if ($this->mode & self::MODE_MESSAGE) {
+ $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
+ }
+
$result[$msg->uid] = $msg;
}
}
@@ -362,17 +397,19 @@ class rcube_imap_cache
return $this->icache['__message']['object'];
}
- $sql_result = $this->db->query(
- "SELECT flags, data"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
- $this->userid, $mailbox, (int)$uid);
+ if ($this->mode & self::MODE_MESSAGE) {
+ $sql_result = $this->db->query(
+ "SELECT flags, data"
+ ." FROM ".$this->db->table_name('cache_messages')
+ ." WHERE user_id = ?"
+ ." AND mailbox = ?"
+ ." AND uid = ?",
+ $this->userid, $mailbox, (int)$uid);
- if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $message = $this->build_message($sql_arr);
- $found = true;
+ if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $message = $this->build_message($sql_arr);
+ $found = true;
+ }
}
// Get the message from IMAP server
@@ -381,6 +418,10 @@ class rcube_imap_cache
// cache will be updated in close(), see below
}
+ if (!($this->mode & self::MODE_MESSAGE)) {
+ return $message;
+ }
+
// Save the message in internal cache, will be written to DB in close()
// Common scenario: user opens unseen message
// - get message (SELECT)
@@ -416,6 +457,10 @@ class rcube_imap_cache
return;
}
+ if (!($this->mode & self::MODE_MESSAGE)) {
+ return;
+ }
+
$flags = 0;
$msg = clone $message;
@@ -487,6 +532,10 @@ class rcube_imap_cache
return;
}
+ if (!($this->mode & self::MODE_MESSAGE)) {
+ return;
+ }
+
$flag = strtoupper($flag);
$idx = (int) array_search($flag, $this->flags);
$uids = (array) $uids;
@@ -527,6 +576,10 @@ class rcube_imap_cache
*/
function remove_message($mailbox = null, $uids = null)
{
+ if (!($this->mode & self::MODE_MESSAGE)) {
+ return;
+ }
+
if (!strlen($mailbox)) {
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache_messages')
@@ -1028,15 +1081,17 @@ class rcube_imap_cache
$removed = array();
// Get known UIDs
- $sql_result = $this->db->query(
- "SELECT uid"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
- $this->userid, $mailbox);
+ if ($this->mode & self::MODE_MESSAGE) {
+ $sql_result = $this->db->query(
+ "SELECT uid"
+ ." FROM ".$this->db->table_name('cache_messages')
+ ." WHERE user_id = ?"
+ ." AND mailbox = ?",
+ $this->userid, $mailbox);
- while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $uids[] = $sql_arr['uid'];
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $uids[] = $sql_arr['uid'];
+ }
}
// Synchronize messages data
@@ -1155,13 +1210,13 @@ class rcube_imap_cache
// Save current message from internal cache
if ($message = $this->icache['__message']) {
// clean up some object's data
- $object = $this->message_object_prepare($message['object']);
+ $this->message_object_prepare($message['object']);
// calculate current md5 sum
- $md5sum = md5(serialize($object));
+ $md5sum = md5(serialize($message['object']));
if ($message['md5sum'] != $md5sum) {
- $this->add_message($message['mailbox'], $object, !$message['exists']);
+ $this->add_message($message['mailbox'], $message['object'], !$message['exists']);
}
$this->icache['__message']['md5sum'] = $md5sum;
@@ -1171,12 +1226,19 @@ class rcube_imap_cache
/**
* Prepares message object to be stored in database.
+ *
+ * @param rcube_message_header|rcube_message_part
*/
- private function message_object_prepare($msg)
+ private function message_object_prepare(&$msg, &$size = 0)
{
- // Remove body too big (>25kB)
- if ($msg->body && strlen($msg->body) > 25 * 1024) {
- unset($msg->body);
+ // Remove body too big
+ if ($msg->body && ($length = strlen($msg->body))) {
+ $size += $length;
+
+ if ($size > $this->threshold * 1024) {
+ $size -= $length;
+ unset($msg->body);
+ }
}
// Fix mimetype which might be broken by some code when message is displayed
@@ -1186,13 +1248,19 @@ class rcube_imap_cache
list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
}
+ unset($msg->replaces);
+
if (is_array($msg->structure->parts)) {
- foreach ($msg->structure->parts as $idx => $part) {
- $msg->structure->parts[$idx] = $this->message_object_prepare($part);
+ foreach ($msg->structure->parts as $part) {
+ $this->message_object_prepare($part, $size);
}
}
- return $msg;
+ if (is_array($msg->parts)) {
+ foreach ($msg->parts as $part) {
+ $this->message_object_prepare($part, $size);
+ }
+ }
}
diff --git a/lib/ext/Roundcube/rcube_imap_generic.php b/lib/ext/Roundcube/rcube_imap_generic.php
index 3138465..f9a62f0 100644
--- a/lib/ext/Roundcube/rcube_imap_generic.php
+++ b/lib/ext/Roundcube/rcube_imap_generic.php
@@ -706,22 +706,11 @@ class rcube_imap_generic
*/
function connect($host, $user, $password, $options=null)
{
- // set options
- if (is_array($options)) {
- $this->prefs = $options;
- }
- // set auth method
- if (!empty($this->prefs['auth_type'])) {
- $auth_method = strtoupper($this->prefs['auth_type']);
- } else {
- $auth_method = 'CHECK';
- }
+ // configure
+ $this->set_prefs($options);
- if (!empty($this->prefs['disabled_caps'])) {
- $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
- }
-
- $result = false;
+ $auth_method = $this->prefs['auth_type'];
+ $result = false;
// initialize connection
$this->error = '';
@@ -898,6 +887,36 @@ class rcube_imap_generic
}
/**
+ * Initializes environment
+ */
+ protected function set_prefs($prefs)
+ {
+ // set preferences
+ if (is_array($prefs)) {
+ $this->prefs = $prefs;
+ }
+
+ // set auth method
+ if (!empty($this->prefs['auth_type'])) {
+ $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
+ }
+ else {
+ $this->prefs['auth_type'] = 'CHECK';
+ }
+
+ // disabled capabilities
+ if (!empty($this->prefs['disabled_caps'])) {
+ $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
+ }
+
+ // additional message flags
+ if (!empty($this->prefs['message_flags'])) {
+ $this->flags = array_merge($this->flags, $this->prefs['message_flags']);
+ unset($this->prefs['message_flags']);
+ }
+ }
+
+ /**
* Checks connection status
*
* @return bool True if connection is active and user is logged in, False otherwise.
@@ -3139,8 +3158,7 @@ class rcube_imap_generic
}
foreach ($data as $entry) {
- // If we are running in a murder topology, the entry[2] string needs
- // to be escaped.
+ // Workaround cyrus-murder bug, the entry[2] string needs to be escaped
if (self::$mupdate) {
$entry[2] = addcslashes($entry[2], '\\"');
}
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index cb7fa84..64288f9 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -34,6 +34,7 @@ class rcube_ldap extends rcube_addressbook
public $ready = false;
public $group_id = 0;
public $coltypes = array();
+ public $export_groups = false;
// private properties
protected $ldap;
@@ -288,7 +289,9 @@ class rcube_ldap extends rcube_addressbook
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
// Search for the dn to use to authenticate
- if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
+ 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_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);
diff --git a/lib/ext/Roundcube/rcube_ldap_generic.php b/lib/ext/Roundcube/rcube_ldap_generic.php
index 88378dc..923a12a 100644
--- a/lib/ext/Roundcube/rcube_ldap_generic.php
+++ b/lib/ext/Roundcube/rcube_ldap_generic.php
@@ -696,11 +696,17 @@ class rcube_ldap_generic
* Turn an LDAP entry into a regular PHP array with attributes as keys.
*
* @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
+ *
* @return array Hash array with attributes as keys
*/
public static function normalize_entry($entry)
{
+ if (!isset($entry['count'])) {
+ return $entry;
+ }
+
$rec = array();
+
for ($i=0; $i < $entry['count']; $i++) {
$attr = $entry[$i];
if ($entry[$attr]['count'] == 1) {
diff --git a/lib/ext/Roundcube/rcube_message.php b/lib/ext/Roundcube/rcube_message.php
index 0d33ea4..9b662a2 100644
--- a/lib/ext/Roundcube/rcube_message.php
+++ b/lib/ext/Roundcube/rcube_message.php
@@ -195,8 +195,6 @@ class rcube_message
/**
* 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 $enriched Enables checking for text/enriched parts too
*
@@ -214,14 +212,15 @@ class rcube_message
$level = explode('.', $part->mime_id);
- // Check if the part belongs to higher-level's alternative/related
+ // Check if the part belongs to higher-level's multipart part
+ // this can be alternative/related/signed/encrypted, but not mixed
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') {
+ if (!preg_match('/^multipart\/(alternative|related|signed|encrypted)$/', $parent->mimetype)) {
continue 2;
}
}
@@ -435,17 +434,24 @@ class rcube_message
continue;
}
+ // We've encountered (malformed) messages with more than
+ // one text/plain or text/html part here. There's no way to choose
+ // which one is better, so we'll display first of them and add
+ // others as attachments (#1489358)
+
// check if sub part is
if ($is_multipart)
$related_part = $p;
- else if ($sub_mimetype == 'text/plain')
+ else if ($sub_mimetype == 'text/plain' && !$plain_part)
$plain_part = $p;
- else if ($sub_mimetype == 'text/html')
+ else if ($sub_mimetype == 'text/html' && !$html_part)
$html_part = $p;
- else if ($sub_mimetype == 'text/enriched')
+ else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
$enriched_part = $p;
- else
- $attach_part = $p;
+ else {
+ // add unsupported/unrecognized parts to attachments list
+ $this->attachments[] = $sub_part;
+ }
}
// parse related part (alternative part could be in here)
@@ -486,11 +492,6 @@ class rcube_message
$this->parts[] = $c;
}
-
- // add unsupported/unrecognized parts to attachments list
- if ($attach_part) {
- $this->attachments[] = $structure->parts[$attach_part];
- }
}
// this is an ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'multipart/encrypted') {
diff --git a/lib/ext/Roundcube/rcube_mime.php b/lib/ext/Roundcube/rcube_mime.php
index 572540f..9c22203 100644
--- a/lib/ext/Roundcube/rcube_mime.php
+++ b/lib/ext/Roundcube/rcube_mime.php
@@ -637,7 +637,8 @@ class rcube_mime
if ($nextChar === ' ' || $nextChar === $separator) {
$afterNextChar = mb_substr($string, $width + 1, 1);
- if ($afterNextChar === false) {
+ // Note: mb_substr() does never return False
+ if ($afterNextChar === false || $afterNextChar === '') {
$subString .= $nextChar;
}
@@ -650,24 +651,23 @@ class rcube_mime
$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) {
+ if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
$subString = mb_substr($string, 0, $spacePos);
$cutLength = $spacePos + 1;
}
+ else if ($breakPos === false) {
+ $subString = $string;
+ $cutLength = null;
+ }
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
- $subString = mb_substr($subString, 0, $width);
$cutLength = $width;
}
}
@@ -708,12 +708,20 @@ class rcube_mime
*/
public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
{
+ static $mime_ext = array();
+
$mime_type = null;
- $mime_magic = rcube::get_instance()->config->get('mime_magic');
- $mime_ext = $skip_suffix ? null : @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
+ $config = rcube::get_instance()->config;
+ $mime_magic = $config->get('mime_magic');
+
+ if (!$skip_suffix && empty($mime_ext)) {
+ foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
+ $mime_ext = array_merge($mime_ext, (array) @include($fpath));
+ }
+ }
// use file name suffix with hard-coded mime-type map
- if (is_array($mime_ext) && $name) {
+ if (!$skip_suffix && is_array($mime_ext) && $name) {
if ($suffix = substr($name, strrpos($name, '.')+1)) {
$mime_type = $mime_ext[strtolower($suffix)];
}
@@ -818,7 +826,9 @@ class rcube_mime
// fallback to some well-known types most important for daily emails
if (empty($mime_types)) {
- $mime_extensions = (array) @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
+ foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
+ $mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
+ }
foreach ($mime_extensions as $ext => $mime) {
$mime_types[$mime][] = $ext;
diff --git a/lib/ext/Roundcube/rcube_plugin_api.php b/lib/ext/Roundcube/rcube_plugin_api.php
index 33f04ea..5a25ada 100644
--- a/lib/ext/Roundcube/rcube_plugin_api.php
+++ b/lib/ext/Roundcube/rcube_plugin_api.php
@@ -403,7 +403,7 @@ class rcube_plugin_api
$args = $ret + $args;
}
- if ($args['abort']) {
+ if ($args['break']) {
break;
}
}
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index df43652..31835db 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -3,8 +3,8 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011, Kolab Systems AG |
- | Copyright (C) 2008-2011, The Roundcube Dev Team |
+ | Copyright (C) 2011-2013, Kolab Systems AG |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -28,21 +28,15 @@ class rcube_spellchecker
{
private $matches = array();
private $engine;
+ private $backend;
private $lang;
private $rc;
private $error;
- private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
private $options = array();
private $dict;
private $have_dict;
- // default settings
- const GOOGLE_HOST = 'ssl://www.google.com';
- const GOOGLE_PORT = 443;
- const MAX_SUGGESTIONS = 10;
-
-
/**
* Constructor
*
@@ -60,6 +54,15 @@ class rcube_spellchecker
'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'),
'dictionary' => $this->rc->config->get('spellcheck_dictionary'),
);
+
+ $cls = 'rcube_spellcheck_' . $this->engine;
+ if (class_exists($cls)) {
+ $this->backend = new $cls($this, $this->lang);
+ $this->backend->options = $this->options;
+ }
+ else {
+ $this->error = "Unknown spellcheck engine '$this->engine'";
+ }
}
@@ -81,14 +84,8 @@ class rcube_spellchecker
$this->content = $text;
}
- if ($this->engine == 'pspell') {
- $this->matches = $this->_pspell_check($this->content);
- }
- else if ($this->engine == 'enchant') {
- $this->matches = $this->_enchant_check($this->content);
- }
- else {
- $this->matches = $this->_googie_check($this->content);
+ if ($this->backend) {
+ $this->matches = $this->backend->check($this->content);
}
return $this->found() == 0;
@@ -115,14 +112,11 @@ class rcube_spellchecker
*/
function get_suggestions($word)
{
- if ($this->engine == 'pspell') {
- return $this->_pspell_suggestions($word);
- }
- else if ($this->engine == 'enchant') {
- return $this->_enchant_suggestions($word);
+ if ($this->backend) {
+ return $this->backend->get_suggestions($word);
}
- return $this->_googie_suggestions($word);
+ return array();
}
@@ -136,14 +130,15 @@ class rcube_spellchecker
*/
function get_words($text = null, $is_html=false)
{
- if ($this->engine == 'pspell') {
- return $this->_pspell_words($text, $is_html);
+ if ($is_html) {
+ $text = $this->html2text($text);
}
- else if ($this->engine == 'enchant') {
- return $this->_enchant_words($text, $is_html);
+
+ if ($this->backend) {
+ return $this->backend->get_words($text);
}
- return $this->_googie_words($text, $is_html);
+ return array();
}
@@ -199,394 +194,7 @@ class rcube_spellchecker
*/
function error()
{
- return $this->error;
- }
-
-
- /**
- * Checks the text using pspell
- *
- * @param string $text Text content for spellchecking
- */
- private function _pspell_check($text)
- {
- // init spellchecker
- $this->_pspell_init();
-
- if (!$this->plink) {
- return array();
- }
-
- // tokenize
- $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
- $diff = 0;
- $matches = array();
-
- foreach ($text as $w) {
- $word = trim($w[0]);
- $pos = $w[1] - $diff;
- $len = mb_strlen($word);
-
- // skip exceptions
- if ($this->is_exception($word)) {
- }
- else if (!pspell_check($this->plink, $word)) {
- $suggestions = pspell_suggest($this->plink, $word);
-
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
- $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
- }
-
- $matches[] = array($word, $pos, $len, null, $suggestions);
- }
-
- $diff += (strlen($word) - $len);
- }
-
- return $matches;
- }
-
-
- /**
- * Returns the misspelled words
- */
- private function _pspell_words($text = null, $is_html=false)
- {
- $result = array();
-
- if ($text) {
- // init spellchecker
- $this->_pspell_init();
-
- if (!$this->plink) {
- return array();
- }
-
- // With PSpell we don't need to get suggestions to return misspelled words
- if ($is_html) {
- $text = $this->html2text($text);
- }
-
- $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
- foreach ($text as $w) {
- $word = trim($w[0]);
-
- // skip exceptions
- if ($this->is_exception($word)) {
- continue;
- }
-
- if (!pspell_check($this->plink, $word)) {
- $result[] = $word;
- }
- }
-
- return $result;
- }
-
- foreach ($this->matches as $m) {
- $result[] = $m[0];
- }
-
- return $result;
- }
-
-
- /**
- * Returns suggestions for misspelled word
- */
- private function _pspell_suggestions($word)
- {
- // init spellchecker
- $this->_pspell_init();
-
- if (!$this->plink) {
- return array();
- }
-
- $suggestions = pspell_suggest($this->plink, $word);
-
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
- $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-
- return is_array($suggestions) ? $suggestions : array();
- }
-
-
- /**
- * Initializes PSpell dictionary
- */
- private function _pspell_init()
- {
- if (!$this->plink) {
- if (!extension_loaded('pspell')) {
- $this->error = "Pspell extension not available";
- return;
- }
-
- $this->plink = pspell_new($this->lang, null, null, RCUBE_CHARSET, PSPELL_FAST);
- }
-
- if (!$this->plink) {
- $this->error = "Unable to load Pspell engine for selected language";
- }
- }
-
-
- /**
- * Checks the text using enchant
- *
- * @param string $text Text content for spellchecking
- */
- private function _enchant_check($text)
- {
- // init spellchecker
- $this->_enchant_init();
-
- if (!$this->enchant_dictionary) {
- return array();
- }
-
- // tokenize
- $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
- $diff = 0;
- $matches = array();
-
- foreach ($text as $w) {
- $word = trim($w[0]);
- $pos = $w[1] - $diff;
- $len = mb_strlen($word);
-
- // skip exceptions
- if ($this->is_exception($word)) {
- }
- else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
- $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
-
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
- $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
- }
-
- $matches[] = array($word, $pos, $len, null, $suggestions);
- }
-
- $diff += (strlen($word) - $len);
- }
-
- return $matches;
- }
-
-
- /**
- * Returns the misspelled words
- */
- private function _enchant_words($text = null, $is_html=false)
- {
- $result = array();
-
- if ($text) {
- // init spellchecker
- $this->_enchant_init();
-
- if (!$this->enchant_dictionary) {
- return array();
- }
-
- // With Enchant we don't need to get suggestions to return misspelled words
- if ($is_html) {
- $text = $this->html2text($text);
- }
-
- $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
- foreach ($text as $w) {
- $word = trim($w[0]);
-
- // skip exceptions
- if ($this->is_exception($word)) {
- continue;
- }
-
- if (!enchant_dict_check($this->enchant_dictionary, $word)) {
- $result[] = $word;
- }
- }
-
- return $result;
- }
-
- foreach ($this->matches as $m) {
- $result[] = $m[0];
- }
-
- return $result;
- }
-
-
- /**
- * Returns suggestions for misspelled word
- */
- private function _enchant_suggestions($word)
- {
- // init spellchecker
- $this->_enchant_init();
-
- if (!$this->enchant_dictionary) {
- return array();
- }
-
- $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
-
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
- $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-
- return is_array($suggestions) ? $suggestions : array();
- }
-
-
- /**
- * Initializes PSpell dictionary
- */
- private function _enchant_init()
- {
- if (!$this->enchant_broker) {
- if (!extension_loaded('enchant')) {
- $this->error = "Enchant extension not available";
- return;
- }
-
- $this->enchant_broker = enchant_broker_init();
- }
-
- if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
- $this->error = "Unable to load dictionary for selected language using Enchant";
- return;
- }
-
- $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
- }
-
-
- private function _googie_check($text)
- {
- // spell check uri is configured
- $url = $this->rc->config->get('spellcheck_uri');
-
- if ($url) {
- $a_uri = parse_url($url);
- $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
- $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80);
- $host = ($ssl ? 'ssl://' : '') . $a_uri['host'];
- $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
- }
- else {
- $host = self::GOOGLE_HOST;
- $port = self::GOOGLE_PORT;
- $path = '/tbproxy/spell?lang=' . $this->lang;
- }
-
- // Google has some problem with spaces, use \n instead
- $gtext = str_replace(' ', "\n", $text);
-
- $gtext = '<?xml version="1.0" encoding="utf-8" ?>'
- .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
- .'<text>' . $gtext . '</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 .= "Content-Length: " . strlen($gtext) . "\r\n";
- $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
- $out .= "Connection: Close\r\n\r\n";
- $out .= $gtext;
- fwrite($fp, $out);
-
- while (!feof($fp))
- $store .= fgets($fp, 128);
- 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) && $m[1]) {
- $this->error = "Error code $m[1] returned";
- }
-
- preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
-
- // skip exceptions (if appropriate options are enabled)
- if (!empty($this->options['ignore_syms']) || !empty($this->options['ignore_nums'])
- || !empty($this->options['ignore_caps']) || !empty($this->options['dictionary'])
- ) {
- foreach ($matches as $idx => $m) {
- $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
- // skip exceptions
- if ($this->is_exception($word)) {
- unset($matches[$idx]);
- }
- }
- }
-
- return $matches;
- }
-
-
- private function _googie_words($text = null, $is_html=false)
- {
- if ($text) {
- if ($is_html) {
- $text = $this->html2text($text);
- }
-
- $matches = $this->_googie_check($text);
- }
- else {
- $matches = $this->matches;
- $text = $this->content;
- }
-
- $result = array();
-
- foreach ($matches as $m) {
- $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
- }
-
- return $result;
- }
-
-
- private function _googie_suggestions($word)
- {
- if ($word) {
- $matches = $this->_googie_check($word);
- }
- else {
- $matches = $this->matches;
- }
-
- if ($matches[0][4]) {
- $suggestions = explode("\t", $matches[0][4]);
- if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
- $suggestions = array_slice($suggestions, 0, MAX_SUGGESTIONS);
- }
-
- return $suggestions;
- }
-
- return array();
+ return $this->error ? $this->error : ($this->backend ? $this->backend->error() : false);
}
diff --git a/lib/ext/Roundcube/rcube_storage.php b/lib/ext/Roundcube/rcube_storage.php
index de83345..e697b2c 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -39,7 +39,7 @@ abstract class rcube_storage
protected $default_charset = 'ISO-8859-1';
protected $default_folders = array('INBOX');
protected $search_set;
- protected $options = array('auth_method' => 'check');
+ protected $options = array('auth_type' => 'check');
protected $page_size = 10;
protected $threading = false;
diff --git a/lib/ext/Roundcube/rcube_string_replacer.php b/lib/ext/Roundcube/rcube_string_replacer.php
index 354b459..77b91d1 100644
--- a/lib/ext/Roundcube/rcube_string_replacer.php
+++ b/lib/ext/Roundcube/rcube_string_replacer.php
@@ -24,11 +24,16 @@
*/
class rcube_string_replacer
{
- public static $pattern = '/##str_replacement\[([0-9]+)\]##/';
+ public static $pattern = '/##str_replacement_(\d+)##/';
public $mailto_pattern;
public $link_pattern;
+ public $linkref_index;
+ public $linkref_pattern;
+
private $values = array();
private $options = array();
+ private $linkrefs = array();
+ private $urls = array();
function __construct($options = array())
@@ -45,6 +50,8 @@ class rcube_string_replacer
."@$utf_domain" // domain-part
."(\?[$url1$url2]+)?" // e.g. ?subject=test...
.")/";
+ $this->linkref_index = '/\[([^\]#]+)\](:?\s*##str_replacement_(\d+)##)/';
+ $this->linkref_pattern = '/\[([^\]#]+)\]/';
$this->options = $options;
}
@@ -67,7 +74,7 @@ class rcube_string_replacer
*/
public function get_replacement($i)
{
- return '##str_replacement['.$i.']##';
+ return '##str_replacement_' . $i . '##';
}
/**
@@ -96,6 +103,7 @@ class rcube_string_replacer
$attrib['href'] = $url_prefix . $url;
$i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix);
+ $this->urls[$i] = $attrib['href'];
}
// Return valid link for recognized schemes, otherwise
@@ -104,6 +112,32 @@ class rcube_string_replacer
}
/**
+ * Callback to add an entry to the link index
+ */
+ public function linkref_addindex($matches)
+ {
+ $key = $matches[1];
+ $this->linkrefs[$key] = $this->urls[$matches[3]];
+
+ return $this->get_replacement($this->add('['.$key.']')) . $matches[2];
+ }
+
+ /**
+ * Callback to replace link references with real links
+ */
+ public function linkref_callback($matches)
+ {
+ $i = 0;
+ if ($url = $this->linkrefs[$matches[1]]) {
+ $attrib = (array)$this->options['link_attribs'];
+ $attrib['href'] = $url;
+ $i = $this->add(html::a($attrib, rcube::Q($matches[1])));
+ }
+
+ return $i > 0 ? '['.$this->get_replacement($i).']' : $matches[0];
+ }
+
+ /**
* Callback function used to build mailto: links around e-mail strings
*
* @param array Matches result from preg_replace_callback
@@ -142,6 +176,9 @@ class rcube_string_replacer
// search for patterns like links and e-mail addresses
$str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str);
$str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str);
+ // resolve link references
+ $str = preg_replace_callback($this->linkref_index, array($this, 'linkref_addindex'), $str);
+ $str = preg_replace_callback($this->linkref_pattern, array($this, 'linkref_callback'), $str);
return $str;
}
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index cf87ded..b73bc08 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -390,12 +390,13 @@ class rcube_utils
* Convert array of request parameters (prefixed with _)
* to a regular array with non-prefixed keys.
*
- * @param int $mode Source to get value from (GPC)
- * @param string $ignore PCRE expression to skip parameters by name
+ * @param int $mode Source to get value from (GPC)
+ * @param string $ignore PCRE expression to skip parameters by name
+ * @param boolean $allow_html Allow HTML tags in field value
*
* @return array Hash array with all request parameters
*/
- public static function request2param($mode = null, $ignore = 'task|action')
+ public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false)
{
$out = array();
$src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
@@ -403,7 +404,7 @@ class rcube_utils
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);
+ $out[$fname] = self::get_input_value($key, $mode, $allow_html);
}
}
@@ -444,41 +445,45 @@ class rcube_utils
$source = self::xss_entity_decode($source);
$stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
$evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');
+
if (preg_match("/$evilexpr/i", $stripped)) {
return '/* evil! */';
}
+ $strict_url_regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
+
// cut out all contents between { and }
while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
- $styles = substr($source, $pos+1, $pos2-($pos+1));
+ $length = $pos2 - $pos - 1;
+ $styles = substr($source, $pos+1, $length);
// check every line of a style block...
if ($allow_remote) {
$a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
+
foreach ($a_styles as $line) {
$stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
// ... and only allow strict url() values
- $regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
- if (stripos($stripped, 'url(') && !preg_match($regexp, $line)) {
+ if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) {
$a_styles = array('/* evil! */');
break;
}
}
+
$styles = join(";\n", $a_styles);
}
- $key = $replacements->add($styles);
- $source = substr($source, 0, $pos+1)
- . $replacements->get_replacement($key)
- . substr($source, $pos2, strlen($source)-$pos2);
- $last_pos = $pos+2;
+ $key = $replacements->add($styles);
+ $repl = $replacements->get_replacement($key);
+ $source = substr_replace($source, $repl, $pos+1, $length);
+ $last_pos = $pos2 - ($length - strlen($repl));
}
// remove html comments and add #container to each tag selector.
// also replace body definition because we also stripped off the <body> tag
- $styles = preg_replace(
+ $source = preg_replace(
array(
- '/(^\s*<!--)|(-->\s*$)/',
+ '/(^\s*<\!--)|(-->\s*$)/m',
'/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
'/'.preg_quote($container_id, '/').'\s+body/i',
),
@@ -490,9 +495,9 @@ class rcube_utils
$source);
// put block contents back in
- $styles = $replacements->resolve($styles);
+ $source = $replacements->resolve($source);
- return $styles;
+ return $source;
}
@@ -739,11 +744,22 @@ 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)(\d\d)$/', trim($date), $matches)) {
- return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
+ 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]));
}
- else if (is_numeric($date)) {
+
+ // unix timestamp
+ if (is_numeric($date)) {
return (int) $date;
}
@@ -776,6 +792,44 @@ class rcube_utils
return (int) $ts;
}
+ /**
+ * Date parsing function that turns the given value into a DateTime object
+ *
+ * @param string $date Date string
+ *
+ * @return object DateTime instance or false on failure
+ */
+ public static function anytodatetime($date)
+ {
+ if (is_object($date) && is_a($date, 'DateTime')) {
+ return $date;
+ }
+
+ $dt = false;
+ $date = trim($date);
+
+ // try to parse string with DateTime first
+ if (!empty($date)) {
+ try {
+ $dt = new DateTime($date);
+ }
+ catch (Exception $e) {
+ // ignore
+ }
+ }
+
+ // try our advanced strtotime() method
+ if (!$dt && ($timestamp = self::strtotime($date))) {
+ try {
+ $dt = new DateTime("@".$timestamp);
+ }
+ catch (Exception $e) {
+ // ignore
+ }
+ }
+
+ return $dt;
+ }
/*
* Idn_to_ascii wrapper.
diff --git a/lib/ext/Roundcube/rcube_vcard.php b/lib/ext/Roundcube/rcube_vcard.php
index a71305c..d54dc56 100644
--- a/lib/ext/Roundcube/rcube_vcard.php
+++ b/lib/ext/Roundcube/rcube_vcard.php
@@ -47,6 +47,7 @@ class rcube_vcard
'manager' => 'X-MANAGER',
'spouse' => 'X-SPOUSE',
'edit' => 'X-AB-EDIT',
+ 'groups' => 'CATEGORIES',
);
private $typemap = array(
'IPHONE' => 'mobile',
@@ -357,8 +358,8 @@ class rcube_vcard
case 'birthday':
case 'anniversary':
- if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) {
- $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
+ if (($val = rcube_utils::anytodatetime($value)) && ($fn = self::$fieldmap[$field])) {
+ $this->raw[$fn][] = array(0 => $val->format('Y-m-d'), 'value' => array('date'));
}
break;
@@ -756,7 +757,7 @@ class rcube_vcard
*
* @return string Joined and quoted string
*/
- private static function vcard_quote($s, $sep = ';')
+ public static function vcard_quote($s, $sep = ';')
{
if (is_array($s)) {
foreach($s as $part) {
@@ -765,7 +766,7 @@ class rcube_vcard
return(implode($sep, (array)$r));
}
- return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;'));
+ return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', $sep => '\\'.$sep));
}
/**
diff --git a/lib/ext/Roundcube/rcube_washtml.php b/lib/ext/Roundcube/rcube_washtml.php
index 8f7fe97..e746754 100644
--- a/lib/ext/Roundcube/rcube_washtml.php
+++ b/lib/ext/Roundcube/rcube_washtml.php
@@ -377,7 +377,14 @@ class rcube_washtml
// Detect max nesting level (for dumpHTML) (#1489110)
$this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
- @$node->loadHTML($html);
+ // Use optimizations if supported
+ if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+ @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
+ }
+ else {
+ @$node->loadHTML($html);
+ }
+
return $this->dumpHtml($node);
}
@@ -448,7 +455,7 @@ 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
commit f59e95cc68a08dde997a37ab3b9c7b9d3a1b763c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date: Thu Oct 17 18:19:33 2013 +0200
Rebase libkolab with cache refactoring
diff --git a/lib/plugins/libkolab/SQL/mysql.initial.sql b/lib/plugins/libkolab/SQL/mysql.initial.sql
index 764da2a..4f23a52 100644
--- a/lib/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,29 +1,175 @@
/**
* libkolab database schema
*
- * @version @package_version@
+ * @version 1.0
* @author Thomas Bruederli
* @licence GNU AGPL
**/
+
+DROP TABLE IF EXISTS `kolab_folders`;
+
+CREATE TABLE `kolab_folders` (
+ `folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `resource` VARCHAR(255) NOT NULL,
+ `type` VARCHAR(32) NOT NULL,
+ `synclock` INT(10) NOT NULL DEFAULT '0',
+ `ctag` VARCHAR(40) DEFAULT NULL,
+ PRIMARY KEY(`folder_id`),
+ INDEX `resource_type` (`resource`, `type`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
DROP TABLE IF EXISTS `kolab_cache`;
-CREATE TABLE `kolab_cache` (
- `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
+DROP TABLE IF EXISTS `kolab_cache_contact`;
+
+CREATE TABLE `kolab_cache_contact` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `contact_type` (`folder_id`,`type`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_event`;
+
+CREATE TABLE `kolab_cache_event` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
+ CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_task`;
+
+CREATE TABLE `kolab_cache_task` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
+ `dtstart` DATETIME,
+ `dtend` DATETIME,
+ CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_journal`;
+
+CREATE TABLE `kolab_cache_journal` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
+ `dtstart` DATETIME,
+ `dtend` DATETIME,
+ CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_note`;
+
+CREATE TABLE `kolab_cache_note` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
+ CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_file`;
+
+CREATE TABLE `kolab_cache_file` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`filename` varchar(255) DEFAULT NULL,
- PRIMARY KEY(`resource`,`type`,`msguid`),
- INDEX `resource_filename` (`resource`, `filename`)
+ CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `folder_filename` (`folder_id`, `filename`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013041900');
+DROP TABLE IF EXISTS `kolab_cache_configuration`;
+
+CREATE TABLE `kolab_cache_configuration` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
+ `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `configuration_type` (`folder_id`,`type`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_freebusy`;
+
+CREATE TABLE `kolab_cache_freebusy` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
+ `dtstart` DATETIME,
+ `dtend` DATETIME,
+ CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013100400');
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
index 5ac9a21..da6e4f8 100755
--- a/lib/plugins/libkolab/bin/modcache.sh
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -4,7 +4,7 @@
/**
* Kolab storage cache modification script
*
- * @version 3.0
+ * @version 3.1
* @author Thomas Bruederli <bruederli at kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
@@ -56,7 +56,14 @@ $opts = get_opt(array(
$opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
$action = $opts[0];
-$rcmail = rcube::get_instance();
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+
+
+// connect to database
+$db = $rcmail->get_dbh();
+$db->db_connect('w');
+if (!$db->is_connected() || $db->is_error())
+ die("No DB connection\n");
/*
@@ -68,32 +75,42 @@ switch (strtolower($action)) {
* Clear/expunge all cache records
*/
case 'expunge':
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
+ $folder_types_db = array_map(array($db, 'quote'), $folder_types);
$expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
- $sql_add = " AND created <= '" . date('Y-m-d 00:00:00', $expire) . "'";
+ $sql_where = "type IN (" . join(',', $folder_types_db) . ")";
+
+ if ($opts['username']) {
+ $sql_where .= ' AND resource LIKE ?';
+ }
+
+ $sql_query = "DELETE FROM %s WHERE folder_id IN (SELECT folder_id FROM kolab_folders WHERE $sql_where) AND created <= " . $db->quote(date('Y-m-d 00:00:00', $expire));
if ($opts['limit']) {
- $sql_add .= ' LIMIT ' . intval($opts['limit']);
+ $sql_query = ' LIMIT ' . intval($opts['limit']);
+ }
+ foreach ($folder_types as $type) {
+ $table_name = 'kolab_cache_' . $type;
+ $db->query(sprintf($sql_query, $table_name), resource_prefix($opts).'%');
+ echo $db->affected_rows() . " records deleted from '$table_name'\n";
}
-case 'clear':
- // connect to database
- $db = $rcmail->get_dbh();
- $db->db_connect('w');
- if (!$db->is_connected() || $db->is_error())
- die("No DB connection\n");
+ $db->query("UPDATE kolab_folders SET ctag='' WHERE $sql_where", resource_prefix($opts).'%');
+ break;
- $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','distribution-list','event','task','configuration','file');
+case 'clear':
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
$folder_types_db = array_map(array($db, 'quote'), $folder_types);
if ($opts['all']) {
- $sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ")";
+ $sql_query = "DELETE FROM kolab_folders WHERE 1";
}
else if ($opts['username']) {
- $sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
+ $sql_query = "DELETE FROM kolab_folders WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
}
if ($sql_query) {
$db->query($sql_query . $sql_add, resource_prefix($opts).'%');
- echo $db->affected_rows() . " records deleted from 'kolab_cache'\n";
+ echo $db->affected_rows() . " records deleted from 'kolab_folders'\n";
}
break;
@@ -106,7 +123,7 @@ case 'prewarm':
$rcmail->plugins->load_plugin('libkolab');
if (authenticate($opts)) {
- $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','event','task','configuration','file');
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
foreach ($folder_types as $type) {
// sync every folder of the given type
foreach (kolab_storage::get_folders($type) as $folder) {
@@ -140,7 +157,7 @@ default:
*/
function resource_prefix($opts)
{
- return 'imap://' . urlencode($opts['username']) . '@' . $opts['host'] . '/';
+ return 'imap://' . str_replace('%', '\\%', urlencode($opts['username'])) . '@' . $opts['host'] . '/';
}
diff --git a/lib/plugins/libkolab/config.inc.php.dist b/lib/plugins/libkolab/config.inc.php.dist
index aa0c8d0..0c612a3 100644
--- a/lib/plugins/libkolab/config.inc.php.dist
+++ b/lib/plugins/libkolab/config.inc.php.dist
@@ -12,9 +12,6 @@ $rcmail_config['kolab_format_version'] = '3.0';
// Defaults to https://<imap-server->/freebusy
$rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
-// Set this option to disable SSL certificate checks when triggering Free/Busy (enabled by default)
-$rcmail_config['kolab_ssl_verify_peer'] = false;
-
// Enables listing of only subscribed folders. This e.g. will limit
// folders in calendar view or available addressbooks
$rcmail_config['kolab_use_subscriptions'] = false;
@@ -23,4 +20,13 @@ $rcmail_config['kolab_use_subscriptions'] = false;
// for displaying resource folder names (experimental!)
$rcmail_config['kolab_custom_display_names'] = false;
-?>
+// Configuration of HTTP requests.
+// See http://pear.php.net/manual/en/package.http.http-request2.config.php
+// for list of supported configuration options (array keys)
+$rcmail_config['kolab_http_request'] = array();
+
+// When kolab_cache is enabled Roundcube's messages cache will be redundant
+// when working on kolab folders. Here we can:
+// 2 - bypass messages/indexes cache completely
+// 1 - bypass only messages, but use index cache
+$rcmail_config['kolab_messages_cache_bypass'] = 0;
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 66ba380..5bcc57a 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -40,6 +40,7 @@ abstract class kolab_format
protected $data;
protected $xmldata;
protected $xmlobject;
+ protected $formaterror;
protected $loaded = false;
protected $version = '3.0';
@@ -104,13 +105,17 @@ abstract class kolab_format
}
$result = new cDateTime();
- // got a unix timestamp (in UTC)
- if (is_numeric($datetime)) {
- $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
- if ($tz) $datetime->setTimezone($tz);
+ try {
+ // got a unix timestamp (in UTC)
+ if (is_numeric($datetime)) {
+ $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
+ if ($tz) $datetime->setTimezone($tz);
+ }
+ else if (is_string($datetime) && strlen($datetime)) {
+ $datetime = new DateTime($datetime, $tz ?: null);
+ }
}
- else if (is_string($datetime) && strlen($datetime))
- $datetime = new DateTime($datetime, $tz ?: null);
+ catch (Exception $e) {}
if ($datetime instanceof DateTime) {
$result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
@@ -244,7 +249,7 @@ abstract class kolab_format
$log = "Error";
}
- if ($log) {
+ if ($log && !isset($this->formaterror)) {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
@@ -252,6 +257,8 @@ abstract class kolab_format
'line' => __LINE__,
'message' => "kolabformat $log: " . kolabformat::errorMessage(),
), true);
+
+ $this->formaterror = $ret;
}
return $ret;
@@ -338,6 +345,7 @@ abstract class kolab_format
*/
public function load($xml)
{
+ $this->formaterror = null;
$read_func = $this->libfunc($this->read_func);
if (is_array($read_func))
@@ -361,6 +369,8 @@ abstract class kolab_format
*/
public function write($version = null)
{
+ $this->formaterror = null;
+
$this->init();
$write_func = $this->libfunc($this->write_func);
if (is_array($write_func))
@@ -389,24 +399,33 @@ abstract class kolab_format
$this->obj->setUid($object['uid']);
// set some automatic values if missing
- if (method_exists($this->obj, 'setCreated') && !$this->obj->created()) {
- if (empty($object['created']))
- $object['created'] = new DateTime('now', self::$timezone);
- $this->obj->setCreated(self::get_datetime($object['created']));
+ if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
+ $cdt = $this->obj->created();
+ $object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', self::$timezone);
+ if (!$cdt || !$cdt->isValid())
+ $this->obj->setCreated(self::get_datetime($object['created']));
}
$object['changed'] = new DateTime('now', self::$timezone);
$this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
// Save custom properties of the given object
- if (!empty($object['x-custom'])) {
+ if (isset($object['x-custom'])) {
$vcustom = new vectorcs;
- foreach ($object['x-custom'] as $cp) {
+ foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
$vcustom->push(new CustomProperty($cp[0], $cp[1]));
}
$this->obj->setCustomProperties($vcustom);
}
+ else { // load custom properties from XML for caching (#2238)
+ $object['x-custom'] = array();
+ $vcustom = $this->obj->customProperties();
+ for ($i=0; $i < $vcustom->size(); $i++) {
+ $cp = $vcustom->get($i);
+ $object['x-custom'][] = array($cp->identifier, $cp->value);
+ }
+ }
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 72867fc..0d0bc75 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -268,7 +268,7 @@ class kolab_format_contact extends kolab_format
*/
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
+ return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
index 304cdc4..46dda01 100644
--- a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -69,7 +69,7 @@ class kolab_format_distributionlist extends kolab_format
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->isValid());
+ return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
index f3d0470..9be9bdf 100644
--- a/lib/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -111,7 +111,8 @@ class kolab_format_event extends kolab_format_xcal
*/
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid());
+ return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
+ (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
}
/**
@@ -138,6 +139,17 @@ class kolab_format_event extends kolab_format_xcal
'attendees' => array(),
);
+ // derive event end from duration (#1916)
+ if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
+ $interval = new DateInterval('PT0S');
+ $interval->d = $duration->weeks() * 7 + $duration->days();
+ $interval->h = $duration->hours();
+ $interval->i = $duration->minutes();
+ $interval->s = $duration->seconds();
+ $object['end'] = clone $object['start'];
+ $object['end']->add($interval);
+ }
+
// organizer is part of the attendees list in Roundcube
if ($object['organizer']) {
$object['organizer']['role'] = 'ORGANIZER';
diff --git a/lib/plugins/libkolab/lib/kolab_format_file.php b/lib/plugins/libkolab/lib/kolab_format_file.php
index f5b153b..5f73bf1 100644
--- a/lib/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/plugins/libkolab/lib/kolab_format_file.php
@@ -95,7 +95,7 @@ class kolab_format_file extends kolab_format
*/
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->isValid());
+ return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_journal.php b/lib/plugins/libkolab/lib/kolab_format_journal.php
index b9a1b4f..f7ccd31 100644
--- a/lib/plugins/libkolab/lib/kolab_format_journal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_journal.php
@@ -54,7 +54,7 @@ class kolab_format_journal extends kolab_format
*/
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->isValid());
+ return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_note.php b/lib/plugins/libkolab/lib/kolab_format_note.php
index 466c536..04a8421 100644
--- a/lib/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/plugins/libkolab/lib/kolab_format_note.php
@@ -54,7 +54,7 @@ class kolab_format_note extends kolab_format
*/
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->isValid());
+ return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_task.php b/lib/plugins/libkolab/lib/kolab_format_task.php
index 56f22dc..a15cb0b 100644
--- a/lib/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/plugins/libkolab/lib/kolab_format_task.php
@@ -63,7 +63,7 @@ class kolab_format_task extends kolab_format_xcal
*/
public function is_valid()
{
- return $this->data || (is_object($this->obj) && $this->obj->isValid());
+ return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index 085e577..284a068 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -385,7 +385,7 @@ abstract class kolab_format_xcal extends kolab_format
if (preg_match('/^@(\d+)/', $offset, $d)) {
$alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
}
- else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
+ else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
$days = $hours = $minutes = $seconds = 0;
switch ($d[3]) {
case 'W': $days = 7*intval($d[2]); break;
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index ee6ede0..5f8b9c6 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -31,6 +31,9 @@ class kolab_storage
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
+ const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
+ const UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
+ const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
public static $version = '3.0';
public static $last_error;
@@ -103,15 +106,16 @@ class kolab_storage
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
+ * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
- public static function get_folders($type)
+ public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
- foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
+ foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
}
}
@@ -154,7 +158,7 @@ class kolab_storage
* This will search all folders storing objects of the given type.
*
* @param string Object UID
- * @param string Object type (contact,distribution-list,event,task,note)
+ * @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
@@ -167,7 +171,7 @@ class kolab_storage
else
$folder->set_folder($foldername);
- if ($object = $folder->get_object($uid))
+ if ($object = $folder->get_object($uid, '*'))
return $object;
}
@@ -276,9 +280,22 @@ class kolab_storage
{
self::setup();
+ $oldfolder = self::get_folder($oldname);
+ $active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
+ // pass active state to new folder name
+ if ($success && $active) {
+ self::set_state($oldnam, false);
+ self::set_state($newname, true);
+ }
+
+ // assign existing cache entries to new resource uri
+ if ($success && $oldfolder) {
+ $oldfolder->cache->rename($newname);
+ }
+
return $success;
}
@@ -388,11 +405,8 @@ class kolab_storage
self::setup();
// find custom display name in folder METADATA
- if (self::$config->get('kolab_custom_display_names', true)) {
- $metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
- if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
- return $name;
- }
+ if ($name = self::custom_displayname($folder)) {
+ return $name;
}
$found = false;
@@ -461,6 +475,21 @@ class kolab_storage
return $folder;
}
+ /**
+ * Get custom display name (saved in metadata) for the given folder
+ */
+ public static function custom_displayname($folder)
+ {
+ // find custom display name in folder METADATA
+ if (self::$config->get('kolab_custom_display_names', true)) {
+ $metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
+ if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
+ return $name;
+ }
+ }
+
+ return false;
+ }
/**
* Helper method to generate a truncated folder name to display
@@ -475,7 +504,7 @@ class kolab_storage
$length = strlen($names[$i] . ' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
- $name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
+ $name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
break;
}
}
@@ -497,7 +526,7 @@ class kolab_storage
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type
- $folders = self::get_folders($type);
+ $folders = self::get_folders($type, false);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
@@ -570,7 +599,7 @@ class kolab_storage
*
* @param string Optional root folder
* @param string Optional name pattern
- * @param string Data type to list folders for (contact,distribution-list,event,task,note,mail)
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
@@ -654,11 +683,12 @@ class kolab_storage
*/
public static function sort_folders($folders)
{
+ $pad = ' ';
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
foreach ($folders as $folder) {
$folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
- $nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)); // decode »
+ $nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode »
}
$names = array();
@@ -963,7 +993,7 @@ class kolab_storage
}
if (!self::$imap->folder_exists($folder)) {
- if (!self::$imap->folder_create($folder)) {
+ if (!self::$imap->create_folder($folder)) {
return;
}
}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index ba6c106..a4fd34c 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -6,7 +6,7 @@
* @version @package_version@
* @author Thomas Bruederli <bruederli at kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2013, Kolab Systems AG <contact at kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -24,24 +24,44 @@
class kolab_storage_cache
{
- private $db;
- private $imap;
- private $folder;
- private $uid2msg;
- private $objects;
- private $index = array();
- private $resource_uri;
- private $enabled = true;
- private $synched = false;
- private $synclock = false;
- private $ready = false;
- private $max_sql_packet;
- private $max_sync_lock_time = 600;
- private $binary_items = array(
- 'photo' => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
- 'pgppublickey' => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></photo>|i',
- 'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></photo>|i',
- );
+ protected $db;
+ protected $imap;
+ protected $folder;
+ protected $uid2msg;
+ protected $objects;
+ protected $index = array();
+ protected $metadata = array();
+ protected $folder_id;
+ protected $resource_uri;
+ protected $enabled = true;
+ protected $synched = false;
+ protected $synclock = false;
+ protected $ready = false;
+ protected $cache_table;
+ protected $folders_table;
+ protected $max_sql_packet;
+ protected $max_sync_lock_time = 600;
+ protected $binary_items = array();
+ protected $extra_cols = array();
+
+
+ /**
+ * Factory constructor
+ */
+ public static function factory(kolab_storage_folder $storage_folder)
+ {
+ $subclass = 'kolab_storage_cache_' . $storage_folder->type;
+ if (class_exists($subclass)) {
+ return new $subclass($storage_folder);
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 900,
+ 'type' => 'php',
+ 'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+ ), true);
+ }
+ }
/**
@@ -55,6 +75,8 @@ class kolab_storage_cache
$this->enabled = $rcmail->config->get('kolab_cache', false);
if ($this->enabled) {
+ // always read folder cache and lock state from DB master
+ $this->db->set_table_dsn('kolab_folders', 'w');
// remove sync-lock on script termination
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
}
@@ -80,9 +102,19 @@ class kolab_storage_cache
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
+ $this->folders_table = $this->db->table_name('kolab_folders');
+ $this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
$this->ready = $this->enabled;
+ $this->folder_id = null;
}
+ /**
+ * Returns true if this cache supports query by type
+ */
+ public function has_type_col()
+ {
+ return in_array('type', $this->extra_cols);
+ }
/**
* Synchronize local cache data with remote
@@ -96,52 +128,65 @@ class kolab_storage_cache
// increase time limit
@set_time_limit($this->max_sync_lock_time);
- // lock synchronization for this folder or wait if locked
- $this->_sync_lock();
+ // read cached folder metadata
+ $this->_read_folder_data();
- // synchronize IMAP mailbox cache
- $this->imap->folder_sync($this->folder->name);
+ // check cache status hash first ($this->metadata is set in _read_folder_data())
+ if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+ // lock synchronization for this folder or wait if locked
+ $this->_sync_lock();
- // compare IMAP index with object cache index
- $imap_index = $this->imap->index($this->folder->name);
- $this->index = $imap_index->get();
+ // disable messages cache if configured to do so
+ $this->bypass(true);
- // determine objects to fetch or to invalidate
- if ($this->ready) {
- // read cache index
- $sql_result = $this->db->query(
- "SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
- $this->resource_uri,
- 'lock'
- );
+ // synchronize IMAP mailbox cache
+ $this->imap->folder_sync($this->folder->name);
- $old_index = array();
- while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $old_index[] = $sql_arr['msguid'];
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
- }
+ // compare IMAP index with object cache index
+ $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
+ $this->index = $imap_index->get();
- // fetch new objects from imap
- foreach (array_diff($this->index, $old_index) as $msguid) {
- if ($object = $this->folder->read_object($msguid, '*')) {
- $this->_extended_insert($msguid, $object);
- }
- }
- $this->_extended_insert(0, null);
-
- // delete invalid entries from local DB
- $del_index = array_diff($old_index, $this->index);
- if (!empty($del_index)) {
- $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
- $this->db->query(
- "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
- $this->resource_uri
+ // determine objects to fetch or to invalidate
+ if ($this->ready) {
+ // read cache index
+ $sql_result = $this->db->query(
+ "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
+ $this->folder_id
);
+
+ $old_index = array();
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $old_index[] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ }
+
+ // fetch new objects from imap
+ foreach (array_diff($this->index, $old_index) as $msguid) {
+ if ($object = $this->folder->read_object($msguid, '*')) {
+ $this->_extended_insert($msguid, $object);
+ }
+ }
+ $this->_extended_insert(0, null);
+
+ // delete invalid entries from local DB
+ $del_index = array_diff($old_index, $this->index);
+ if (!empty($del_index)) {
+ $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+ $this->db->query(
+ "DELETE FROM $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
+ $this->folder_id
+ );
+ }
+
+ // update ctag value (will be written to database in _sync_unlock())
+ $this->metadata['ctag'] = $this->folder->get_ctag();
}
- }
- // remove lock
- $this->_sync_unlock();
+ $this->bypass(false);
+
+ // remove lock
+ $this->_sync_unlock();
+ }
$this->synched = time();
}
@@ -165,11 +210,12 @@ class kolab_storage_cache
// load object if not in memory
if (!isset($this->objects[$msguid])) {
if ($this->ready) {
+ $this->_read_folder_data();
+
$sql_result = $this->db->query(
- "SELECT * FROM kolab_cache ".
- "WHERE resource=? AND type=? AND msguid=?",
- $this->resource_uri,
- $type ?: $this->folder->type,
+ "SELECT * FROM $this->cache_table ".
+ "WHERE folder_id=? AND msguid=?",
+ $this->folder_id,
$msguid
);
@@ -210,8 +256,9 @@ class kolab_storage_cache
// remove old entry
if ($this->ready) {
- $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=? AND type<>?",
- $this->resource_uri, $msguid, 'lock');
+ $this->_read_folder_data();
+ $this->db->query("DELETE FROM $this->cache_table WHERE folder_id=? AND msguid=?",
+ $this->folder_id, $msguid);
}
if ($object) {
@@ -235,27 +282,33 @@ class kolab_storage_cache
{
// write to cache
if ($this->ready) {
+ $this->_read_folder_data();
+
$sql_data = $this->_serialize($object);
- $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
- $result = $this->db->query(
- "INSERT INTO kolab_cache ".
- " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words, filename)".
- " VALUES (?, ?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ?, ?, ?, ?)",
- $this->resource_uri,
- $objtype,
+ $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
+ $extra_fields = $this->extra_cols ? str_repeat(', ?', count($this->extra_cols)) : '';
+
+ $args = array(
+ "INSERT INTO $this->cache_table ".
+ " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
+ " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_fields)",
+ $this->folder_id,
$msguid,
$object['uid'],
$sql_data['changed'],
$sql_data['data'],
$sql_data['xml'],
- $sql_data['dtstart'],
- $sql_data['dtend'],
$sql_data['tags'],
$sql_data['words'],
- $sql_data['filename']
);
+ foreach ($this->extra_cols as $col) {
+ $args[] = $sql_data[$col];
+ }
+
+ $result = call_user_func_array(array($this->db, 'query'), $args);
+
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
@@ -283,14 +336,15 @@ class kolab_storage_cache
// resolve new message UID in target folder
if ($new_msguid = $target->cache->uid2msguid($uid)) {
+ $this->_read_folder_data();
+
$this->db->query(
- "UPDATE kolab_cache SET resource=?, msguid=? ".
- "WHERE resource=? AND msguid=? AND type<>?",
- $target->get_resource_uri(),
+ "UPDATE $this->cache_table SET folder_id=?, msguid=? ".
+ "WHERE folder_id=? AND msguid=?",
+ $target->folder_id,
$new_msguid,
- $this->resource_uri,
- $msguid,
- 'lock'
+ $this->folder_id,
+ $msguid
);
}
else {
@@ -307,15 +361,32 @@ class kolab_storage_cache
*/
public function purge($type = null)
{
+ $this->_read_folder_data();
+
$result = $this->db->query(
- "DELETE FROM kolab_cache WHERE resource=?".
- ($type ? ' AND type=?' : ''),
- $this->resource_uri,
- $type
+ "DELETE FROM $this->cache_table WHERE folder_id=?".
+ $this->folder_id
);
return $this->db->affected_rows($result);
}
+ /**
+ * Update resource URI for existing cache entries
+ *
+ * @param string Target IMAP folder to move it to
+ */
+ public function rename($new_folder)
+ {
+ $target = kolab_storage::get_folder($new_folder);
+
+ // resolve new message UID in target folder
+ $this->db->query(
+ "UPDATE $this->folders_table SET resource=? ".
+ "WHERE resource=?",
+ $target->get_resource_uri(),
+ $this->resource_uri
+ );
+ }
/**
* Select Kolab objects filtered by the given query
@@ -331,10 +402,12 @@ class kolab_storage_cache
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
+ $this->_read_folder_data();
+
$sql_result = $this->db->query(
- "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM kolab_cache ".
- "WHERE resource=? " . $this->_sql_where($query),
- $this->resource_uri
+ "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
+ "WHERE folder_id=? " . $this->_sql_where($query),
+ $this->folder_id
);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -391,10 +464,12 @@ class kolab_storage_cache
// cache is in sync, we can count records in local DB
if ($this->synched) {
+ $this->_read_folder_data();
+
$sql_result = $this->db->query(
- "SELECT COUNT(*) AS numrows FROM kolab_cache ".
- "WHERE resource=? " . $this->_sql_where($query),
- $this->resource_uri
+ "SELECT COUNT(*) AS numrows FROM $this->cache_table ".
+ "WHERE folder_id=? " . $this->_sql_where($query),
+ $this->folder_id
);
$sql_arr = $this->db->fetch_assoc($sql_result);
@@ -415,10 +490,10 @@ class kolab_storage_cache
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
*/
- private function _sql_where($query)
+ protected function _sql_where($query)
{
$sql_where = '';
- foreach ($query as $param) {
+ foreach ((array) $query as $param) {
if (is_array($param[0])) {
$subq = array();
foreach ($param[0] as $q) {
@@ -460,7 +535,7 @@ class kolab_storage_cache
* Helper method to convert the given pseudo-query triplets into
* an associative filter array with 'equals' values only
*/
- private function _query2assoc($query)
+ protected function _query2assoc($query)
{
// extract object type from query parameter
$filter = array();
@@ -479,7 +554,7 @@ class kolab_storage_cache
* @param string IMAP folder to read from
* @return array List of parsed Kolab objects
*/
- private function _fetch($index, $type = null, $folder = null)
+ protected function _fetch($index, $type = null, $folder = null)
{
$results = array();
foreach ((array)$index as $msguid) {
@@ -501,13 +576,19 @@ class kolab_storage_cache
* @param string IMAP folder to read from
* @return array List of parsed Kolab objects
*/
- private function _fetch_uids($index, $type = null)
+ protected function _fetch_uids($index, $type = null)
{
if (!$type)
$type = $this->folder->type;
+ $this->bypass(true);
+
$results = array();
- foreach ((array)$this->imap->fetch_headers($this->folder->name, $index, false) as $msguid => $headers) {
+ $headers = $this->imap->fetch_headers($this->folder->name, $index, false);
+
+ $this->bypass(false);
+
+ foreach ((array)$headers as $msguid => $headers) {
$object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
// check object type header and abort on mismatch
@@ -526,35 +607,9 @@ class kolab_storage_cache
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*/
- private function _serialize($object)
+ protected function _serialize($object)
{
- $sql_data = array('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
- $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
-
- // set type specific values
- if ($objtype == 'event') {
- // database runs in server's timezone so using date() is what we want
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
- $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']);
-
- // extend date range for recurring events
- if ($object['recurrence'] && $object['_formatobj']) {
- $recurrence = new kolab_date_recurrence($object['_formatobj']);
- $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year'));
- }
- }
- else if ($objtype == 'task') {
- if ($object['start'])
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
- if ($object['due'])
- $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['due']) ? $object['due']->format('U') : $object['due']);
- }
- else if ($objtype == 'file') {
- if (!empty($object['_attachments'])) {
- reset($object['_attachments']);
- $sql_data['filename'] = $object['_attachments'][key($object['_attachments'])]['name'];
- }
- }
+ $sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
if ($object['changed']) {
$sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
@@ -596,7 +651,7 @@ class kolab_storage_cache
/**
* Helper method to turn stored cache data into a valid storage object
*/
- private function _unserialize($sql_arr)
+ protected function _unserialize($sql_arr)
{
$object = unserialize($sql_arr['data']);
@@ -608,11 +663,11 @@ class kolab_storage_cache
}
// add meta data
- $object['_type'] = $sql_arr['type'];
+ $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
$object['_msguid'] = $sql_arr['msguid'];
$object['_mailbox'] = $this->folder->name;
$object['_size'] = strlen($sql_arr['xml']);
- $object['_formatobj'] = kolab_format::factory($sql_arr['type'], 3.0, $sql_arr['xml']);
+ $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
return $object;
}
@@ -623,37 +678,35 @@ class kolab_storage_cache
* @param int Message UID. Set 0 to commit buffered inserts
* @param array Kolab object to cache
*/
- private function _extended_insert($msguid, $object)
+ protected function _extended_insert($msguid, $object)
{
static $buffer = '';
$line = '';
if ($object) {
$sql_data = $this->_serialize($object);
- $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
-
$values = array(
- $this->db->quote($this->resource_uri),
- $this->db->quote($objtype),
+ $this->db->quote($this->folder_id),
$this->db->quote($msguid),
$this->db->quote($object['uid']),
$this->db->now(),
$this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']),
$this->db->quote($sql_data['xml']),
- $this->db->quote($sql_data['dtstart']),
- $this->db->quote($sql_data['dtend']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
- $this->db->quote($sql_data['filename']),
);
+ foreach ($this->extra_cols as $col) {
+ $values[] = $this->db->quote($sql_data[$col]);
+ }
$line = '(' . join(',', $values) . ')';
}
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
+ $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
$result = $this->db->query(
- "INSERT INTO kolab_cache ".
- " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words, filename)".
+ "INSERT INTO $this->cache_table ".
+ " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
" VALUES $buffer"
);
if (!$this->db->affected_rows($result)) {
@@ -672,7 +725,7 @@ class kolab_storage_cache
/**
* Returns max_allowed_packet from mysql config
*/
- private function max_sql_packet()
+ protected function max_sql_packet()
{
if (!$this->max_sql_packet) {
// mysql limit or max 4 MB
@@ -684,16 +737,36 @@ class kolab_storage_cache
}
/**
+ * Read this folder's ID and cache metadata
+ */
+ protected function _read_folder_data()
+ {
+ // already done
+ if (!empty($this->folder_id))
+ return;
+
+ $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT folder_id, synclock, ctag FROM $this->folders_table WHERE resource=?", $this->resource_uri));
+ if ($sql_arr) {
+ $this->metadata = $sql_arr;
+ $this->folder_id = $sql_arr['folder_id'];
+ }
+ else {
+ $this->db->query("INSERT INTO $this->folders_table (resource, type) VALUES (?, ?)", $this->resource_uri, $this->folder->type);
+ $this->folder_id = $this->db->insert_id('kolab_folders');
+ $this->metadata = array();
+ }
+ }
+
+ /**
* Check lock record for this folder and wait if locked or set lock
*/
- private function _sync_lock()
+ protected function _sync_lock()
{
if (!$this->ready)
return;
- $sql_query = "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
- "WHERE resource=? AND type=?";
- $sql_arr = $this->db->fetch_assoc($this->db->query($sql_query, $this->resource_uri, 'lock'));
+ $this->_read_folder_data();
+ $sql_query = "SELECT synclock, ctag FROM $this->folders_table WHERE folder_id=?";
// abort if database is not set-up
if ($this->db->is_error()) {
@@ -704,28 +777,13 @@ class kolab_storage_cache
$this->synclock = true;
// wait if locked (expire locks after 10 minutes)
- while ($sql_arr && intval($sql_arr['locked']) > 0 && $sql_arr['created'] + $this->max_sync_lock_time > time()) {
+ while ($this->metadata && intval($this->metadata['synclock']) > 0 && $this->metadata['synclock'] + $this->max_sync_lock_time > time()) {
usleep(500000);
- $sql_arr = $this->db->fetch_assoc($this->db->query($sql_query, $this->resource_uri, 'lock'));
+ $this->metadata = $this->db->fetch_assoc($this->db->query($sql_query, $this->folder_id));
}
- // create lock record if not exists
- if (!$sql_arr) {
- $this->db->query(
- "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml, tags, words)".
- " VALUES (?, ?, 1, " . $this->db->now() . ", '', '', '', '', '')",
- $this->resource_uri,
- 'lock'
- );
- }
- else {
- $this->db->query(
- "UPDATE kolab_cache SET msguid = 1, created = " . $this->db->now() .
- " WHERE resource = ? AND type = ?",
- $this->resource_uri,
- 'lock'
- );
- }
+ // set lock
+ $this->db->query("UPDATE $this->folders_table SET synclock = ? WHERE folder_id = ?", time(), $this->folder_id);
}
/**
@@ -737,9 +795,9 @@ class kolab_storage_cache
return;
$this->db->query(
- "UPDATE kolab_cache SET msguid = 0 WHERE resource = ? AND type = ?",
- $this->resource_uri,
- 'lock'
+ "UPDATE $this->folders_table SET synclock = 0, ctag = ? WHERE folder_id = ?",
+ $this->metadata['ctag'],
+ $this->folder_id
);
$this->synclock = false;
@@ -765,4 +823,72 @@ class kolab_storage_cache
return $this->uid2msg[$uid];
}
+ /**
+ * Getter for protected member variables
+ */
+ public function __get($name)
+ {
+ if ($name == 'folder_id') {
+ $this->_read_folder_data();
+ }
+
+ return $this->$name;
+ }
+
+ /**
+ * Bypass Roundcube messages cache.
+ * Roundcube cache duplicates information already stored in kolab_cache.
+ *
+ * @param bool $disable True disables, False enables messages cache
+ */
+ public function bypass($disable = false)
+ {
+ // if kolab cache is disabled do nothing
+ if (!$this->enabled) {
+ return;
+ }
+
+ static $messages_cache, $cache_bypass;
+
+ if ($messages_cache === null) {
+ $rcmail = rcube::get_instance();
+ $messages_cache = (bool) $rcmail->config->get('messages_cache');
+ $cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass');
+ }
+
+ if ($messages_cache) {
+ // handle recurrent (multilevel) bypass() calls
+ if ($disable) {
+ $this->cache_bypassed += 1;
+ if ($this->cache_bypassed > 1) {
+ return;
+ }
+ }
+ else {
+ $this->cache_bypassed -= 1;
+ if ($this->cache_bypassed > 0) {
+ return;
+ }
+ }
+
+ switch ($cache_bypass) {
+ case 2:
+ // Disable messages cache completely
+ $this->imap->set_messages_caching(!$disable);
+ break;
+
+ case 1:
+ // We'll disable messages cache, but keep index cache.
+ // Default mode is both (MODE_INDEX | MODE_MESSAGE)
+ $mode = rcube_imap_cache::MODE_INDEX;
+
+ if (!$disable) {
+ $mode |= rcube_imap_cache::MODE_MESSAGE;
+ }
+
+ $this->imap->set_messages_caching(true, $mode);
+ }
+ }
+ }
+
}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 303ed99..80f13fc 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -7,7 +7,7 @@
* @author Thomas Bruederli <bruederli at kolabsys.com>
* @author Aleksander Machniak <machniak at kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2013, Kolab Systems AG <contact at kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -43,8 +43,8 @@ class kolab_storage_folder
public $default = false;
/**
- * Is this folder set to be default
- * @var boolean
+ * The kolab_storage_cache instance for caching operations
+ * @var object
*/
public $cache;
@@ -64,7 +64,6 @@ class kolab_storage_folder
{
$this->imap = rcube::get_instance()->get_storage();
$this->imap->set_options(array('skip_deleted' => true));
- $this->cache = new kolab_storage_cache($this);
$this->set_folder($name, $type);
}
@@ -79,11 +78,16 @@ class kolab_storage_folder
{
$this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
+ $oldtype = $this->type;
list($this->type, $suffix) = explode('.', $this->type_annotation);
$this->default = $suffix == 'default';
$this->name = $name;
$this->resource_uri = null;
+ // get a new cache instance of folder type changed
+ if (!$this->cache || $type != $oldtype)
+ $this->cache = kolab_storage_cache::factory($this);
+
$this->imap->set_folder($this->name);
$this->cache->set_folder($this);
}
@@ -92,7 +96,7 @@ class kolab_storage_folder
/**
*
*/
- private function get_folder_info()
+ public function get_folder_info()
{
if (!isset($this->info))
$this->info = $this->imap->folder_info($this->name);
@@ -260,6 +264,52 @@ class kolab_storage_folder
}
/**
+ * Helper method to extract folder UID metadata
+ *
+ * @return string Folder's UID
+ */
+ public function get_uid()
+ {
+ // UID is defined in folder METADATA
+ $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS);
+ $metadata = $this->get_metadata($metakeys);
+ foreach ($metakeys as $key) {
+ if (($uid = $metadata[$key])) {
+ return $uid;
+ }
+ }
+
+ // generate a folder UID and set it to IMAP
+ $uid = rtrim(chunk_split(md5($this->name . $this->get_owner()), 12, '-'), '-');
+ $this->set_uid($uid);
+
+ return $uid;
+ }
+
+ /**
+ * Helper method to set an UID value to the given IMAP folder instance
+ *
+ * @param string Folder's UID
+ * @return boolean True on succes, False on failure
+ */
+ public function set_uid($uid)
+ {
+ if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) {
+ $success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid));
+ }
+ return $success;
+ }
+
+ /**
+ * Compose a folder Etag identifier
+ */
+ public function get_ctag()
+ {
+ $fdata = $this->get_imap_data();
+ return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
+ }
+
+ /**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
@@ -303,7 +353,6 @@ class kolab_storage_folder
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
-
/**
* Get number of objects stored in this folder
*
@@ -312,19 +361,12 @@ class kolab_storage_folder
* @return integer The number of objects of the given type
* @see self::select()
*/
- public function count($type_or_query = null)
+ public function count($query = null)
{
- if (!$type_or_query)
- $query = array(array('type','=',$this->type));
- else if (is_string($type_or_query))
- $query = array(array('type','=',$type_or_query));
- else
- $query = $this->_prepare_query((array)$type_or_query);
-
// synchronize cache first
$this->cache->synchronize();
- return $this->cache->count($query);
+ return $this->cache->count($this->_prepare_query($query));
}
@@ -342,7 +384,7 @@ class kolab_storage_folder
$this->cache->synchronize();
// fetch objects from cache
- return $this->cache->select(array(array('type','=',$type)));
+ return $this->cache->select($this->_prepare_query($type));
}
@@ -388,10 +430,15 @@ class kolab_storage_folder
*/
private function _prepare_query($query)
{
- $type = null;
- foreach ($query as $i => $param) {
- if ($param[0] == 'type') {
- $type = $param[2];
+ // string equals type query
+ // FIXME: should not be called this way!
+ if (is_string($query)) {
+ return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
+ }
+
+ foreach ((array)$query as $i => $param) {
+ if ($param[0] == 'type' && !$this->cache->has_type_col()) {
+ unset($query[$i]);
}
else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
if (is_object($param[2]) && is_a($param[2], 'DateTime'))
@@ -401,10 +448,6 @@ class kolab_storage_folder
}
}
- // add type selector if not in $query
- if (!$type)
- $query[] = array('type','=',$this->type);
-
return $query;
}
@@ -465,6 +508,7 @@ class kolab_storage_folder
* @param string The IMAP message UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string The folder name where the message is stored
+ *
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($msguid, $type = null, $folder = null)
@@ -474,31 +518,31 @@ class kolab_storage_folder
$this->imap->set_folder($folder);
- $headers = $this->imap->get_message_headers($msguid);
- $message = null;
+ $this->cache->bypass(true);
+ $message = new rcube_message($msguid);
+ $this->cache->bypass(false);
// Message doesn't exist?
- if (empty($headers)) {
+ if (empty($message->headers)) {
return false;
}
// extract the X-Kolab-Type header from the XML attachment part if missing
- if (empty($headers->others['x-kolab-type'])) {
- $message = new rcube_message($msguid);
+ if (empty($message->headers->others['x-kolab-type'])) {
foreach ((array)$message->attachments as $part) {
if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
- $headers->others['x-kolab-type'] = $part->mimetype;
+ $message->headers->others['x-kolab-type'] = $part->mimetype;
break;
}
}
}
// fix buggy messages stating the X-Kolab-Type header twice
- else if (is_array($headers->others['x-kolab-type'])) {
- $headers->others['x-kolab-type'] = reset($headers->others['x-kolab-type']);
+ else if (is_array($message->headers->others['x-kolab-type'])) {
+ $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
}
// no object type header found: abort
- if (empty($headers->others['x-kolab-type'])) {
+ if (empty($message->headers->others['x-kolab-type'])) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
@@ -509,14 +553,13 @@ class kolab_storage_folder
return false;
}
- $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
- $content_type = kolab_format::KTYPE_PREFIX . $object_type;
+ $object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
+ $content_type = kolab_format::KTYPE_PREFIX . $object_type;
// check object type header and abort on mismatch
if ($type != '*' && $object_type != $type)
return false;
- if (!$message) $message = new rcube_message($msguid);
$attachments = array();
// get XML part
@@ -558,7 +601,7 @@ class kolab_storage_folder
}
// check kolab format version
- $format_version = $headers->others['x-kolab-mime-version'];
+ $format_version = $message->headers->others['x-kolab-mime-version'];
if (empty($format_version)) {
list($xmltype, $subtype) = explode('.', $object_type);
$xmlhead = substr($xml, 0, 512);
@@ -651,8 +694,10 @@ class kolab_storage_folder
$numatt = count($object['_attachments']);
foreach ($object['_attachments'] as $key => $attachment) {
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
- if (empty($attachment['content']) && !empty($attachment['data']))
+ if (empty($attachment['content']) && !empty($attachment['data'])) {
$attachment['content'] = $attachment['data'];
+ unset($attachment['data'], $object['_attachments'][$key]['data']);
+ }
// make sure size is set, so object saved in cache contains this info
if (!isset($attachment['size'])) {
@@ -710,7 +755,9 @@ class kolab_storage_folder
// delete old message
if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
+ $this->cache->bypass(true);
$this->imap->delete_message($object['_msguid'], $object['_mailbox']);
+ $this->cache->bypass(false);
$this->cache->set($object['_msguid'], false, $object['_mailbox']);
}
@@ -805,6 +852,8 @@ class kolab_storage_folder
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
+ $this->cache->bypass(true);
+
if ($msguid && $expunge) {
$success = $this->imap->delete_message($msguid, $this->name);
}
@@ -812,6 +861,8 @@ class kolab_storage_folder
$success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
}
+ $this->cache->bypass(false);
+
if ($success) {
$this->cache->set($msguid, false);
}
@@ -826,7 +877,11 @@ class kolab_storage_folder
public function delete_all()
{
$this->cache->purge();
- return $this->imap->clear_folder($this->name);
+ $this->cache->bypass(true);
+ $result = $this->imap->clear_folder($this->name);
+ $this->cache->bypass(false);
+
+ return $result;
}
@@ -839,7 +894,11 @@ class kolab_storage_folder
public function undelete($uid)
{
if ($msguid = $this->cache->uid2msguid($uid, true)) {
- if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
+ $this->cache->bypass(true);
+ $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
+ $this->cache->bypass(false);
+
+ if ($result) {
return $msguid;
}
}
@@ -858,7 +917,11 @@ class kolab_storage_folder
public function move($uid, $target_folder)
{
if ($msguid = $this->cache->uid2msguid($uid)) {
- if ($this->imap->move_message($msguid, $target_folder, $this->name)) {
+ $this->cache->bypass(true);
+ $result = $this->imap->move_message($msguid, $target_folder, $this->name);
+ $this->cache->bypass(false);
+
+ if ($result) {
$this->cache->move($msguid, $uid, $target_folder);
return true;
}
@@ -1108,9 +1171,7 @@ class kolab_storage_folder
require_once('HTTP/Request2.php');
try {
- $rcmail = rcube::get_instance();
- $request = new HTTP_Request2($url);
- $request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
+ $request = libkolab::http_request($url);
// set authentication credentials
if ($auth_user && $auth_passwd)
diff --git a/lib/plugins/libkolab/libkolab.php b/lib/plugins/libkolab/libkolab.php
index b5ff968..48a5033 100644
--- a/lib/plugins/libkolab/libkolab.php
+++ b/lib/plugins/libkolab/libkolab.php
@@ -27,6 +27,8 @@
class libkolab extends rcube_plugin
{
+ static $http_requests = array();
+
/**
* Required startup method of a Roundcube plugin
*/
@@ -59,4 +61,66 @@ class libkolab extends rcube_plugin
$p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION');
return $p;
}
+
+ /**
+ * Wrapper function to load and initalize the HTTP_Request2 Object
+ *
+ * @param string|Net_Url2 Request URL
+ * @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
+ * @param array Configuration for this Request instance, that will be merged
+ * with default configuration
+ *
+ * @return HTTP_Request2 Request object
+ */
+ public static function http_request($url = '', $method = 'GET', $config = array())
+ {
+ $rcube = rcube::get_instance();
+ $http_config = (array) $rcube->config->get('kolab_http_request');
+
+ // deprecated configuration options
+ if (empty($http_config)) {
+ foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
+ $value = $rcube->config->get('kolab_' . $option, true);
+ if (is_bool($value)) {
+ $http_config[$option] = $value;
+ }
+ }
+ }
+
+ if (!empty($config)) {
+ $http_config = array_merge($http_config, $config);
+ }
+
+ $key = md5(serialize($http_config));
+
+ if (!($request = self::$http_requests[$key])) {
+ // load HTTP_Request2
+ require_once 'HTTP/Request2.php';
+
+ try {
+ $request = new HTTP_Request2();
+ $request->setConfig($http_config);
+ }
+ catch (Exception $e) {
+ rcube::raise_error($e, true, true);
+ }
+
+ // proxy User-Agent string
+ $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
+
+ self::$http_requests[$key] = $request;
+ }
+
+ // cleanup
+ try {
+ $request->setBody('');
+ $request->setUrl($url);
+ $request->setMethod($method);
+ }
+ catch (Exception $e) {
+ rcube::raise_error($e, true, true);
+ }
+
+ return $request;
+ }
}
More information about the commits
mailing list