summaryrefslogtreecommitdiff
path: root/program/lib/Roundcube
diff options
context:
space:
mode:
Diffstat (limited to 'program/lib/Roundcube')
-rw-r--r--program/lib/Roundcube/bootstrap.php12
-rw-r--r--program/lib/Roundcube/html.php9
-rw-r--r--program/lib/Roundcube/rcube.php10
-rw-r--r--program/lib/Roundcube/rcube_addressbook.php53
-rw-r--r--program/lib/Roundcube/rcube_base_replacer.php4
-rw-r--r--program/lib/Roundcube/rcube_config.php255
-rw-r--r--program/lib/Roundcube/rcube_contacts.php8
-rw-r--r--program/lib/Roundcube/rcube_csv2vcard.php2
-rw-r--r--program/lib/Roundcube/rcube_db.php122
-rw-r--r--program/lib/Roundcube/rcube_db_mssql.php2
-rw-r--r--program/lib/Roundcube/rcube_db_mysql.php27
-rw-r--r--program/lib/Roundcube/rcube_db_pgsql.php2
-rw-r--r--program/lib/Roundcube/rcube_db_sqlsrv.php2
-rw-r--r--program/lib/Roundcube/rcube_html2text.php13
-rw-r--r--program/lib/Roundcube/rcube_imap.php16
-rw-r--r--program/lib/Roundcube/rcube_imap_cache.php150
-rw-r--r--program/lib/Roundcube/rcube_imap_generic.php11
-rw-r--r--program/lib/Roundcube/rcube_ldap.php5
-rw-r--r--program/lib/Roundcube/rcube_ldap_generic.php6
-rw-r--r--program/lib/Roundcube/rcube_message.php29
-rw-r--r--program/lib/Roundcube/rcube_mime.php34
-rw-r--r--program/lib/Roundcube/rcube_plugin_api.php7
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_atd.php204
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_enchant.php182
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_engine.php91
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_googie.php176
-rw-r--r--program/lib/Roundcube/rcube_spellcheck_pspell.php189
-rw-r--r--program/lib/Roundcube/rcube_spellchecker.php487
-rw-r--r--program/lib/Roundcube/rcube_string_replacer.php41
-rw-r--r--program/lib/Roundcube/rcube_utils.php75
-rw-r--r--program/lib/Roundcube/rcube_vcard.php40
-rw-r--r--program/lib/Roundcube/rcube_washtml.php11
32 files changed, 1605 insertions, 670 deletions
diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php
index 5d5a22387..6e5143382 100644
--- a/program/lib/Roundcube/bootstrap.php
+++ b/program/lib/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).";
diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php
index 4295f26d6..f6f744cb2 100644
--- a/program/lib/Roundcube/html.php
+++ b/program/lib/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/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index d9c3dd8b9..399f84fd8 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -702,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)) {
@@ -1132,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
*/
diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php
index 4ed139c45..886f65cb9 100644
--- a/program/lib/Roundcube/rcube_addressbook.php
+++ b/program/lib/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
*/
@@ -208,6 +209,7 @@ abstract class rcube_addressbook
public function validate(&$save_data, $autofix = false)
{
$rcube = rcube::get_instance();
+ $valid = true;
// check validity of email addresses
foreach ($this->get_col_values('email', $save_data, true) as $email) {
@@ -215,12 +217,28 @@ abstract class rcube_addressbook
if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
$error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
$this->set_error(self::ERROR_VALIDATE, $error);
- return false;
+ $valid = false;
+ break;
}
}
}
- return true;
+ // allow plugins to do contact validation and auto-fixing
+ $plugin = $rcube->plugins->exec_hook('contact_validate', array(
+ 'record' => $save_data,
+ 'autofix' => $autofix,
+ 'valid' => $valid,
+ ));
+
+ if ($valid && !$plugin['valid']) {
+ $this->set_error(self::ERROR_VALIDATE, $plugin['error']);
+ }
+
+ if (is_array($plugin['record'])) {
+ $save_data = $plugin['record'];
+ }
+
+ return $plugin['valid'];
}
/**
@@ -423,7 +441,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 +494,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
@@ -513,8 +532,12 @@ abstract class rcube_addressbook
$fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
else if ($compose_mode == 1)
$fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
- else
+ else if ($compose_mode == 0)
$fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
+ else {
+ $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
+ $fn = $plugin['fn'];
+ }
$fn = trim($fn, ', ');
@@ -523,9 +546,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 +561,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 +584,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/program/lib/Roundcube/rcube_base_replacer.php b/program/lib/Roundcube/rcube_base_replacer.php
index a59bba926..fa6764753 100644
--- a/program/lib/Roundcube/rcube_base_replacer.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php
index 90e1394cf..04b914c3d 100644
--- a/program/lib/Roundcube/rcube_config.php
+++ b/program/lib/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,47 +209,73 @@ class rcube_config
*/
public function load_from_file($file)
{
- $fpath = $this->resolve_path($file);
- 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);
- 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 = realpath($this->basedir . '/' . $file);
- }
+ $files = array();
+ $abs_path = $this->_is_absolute($file);
- // 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;
+ 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;
+ }
+
+ 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
@@ -246,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)
@@ -305,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;
@@ -317,13 +363,6 @@ class rcube_config
$this->userprefs = $prefs;
$this->prop = array_merge($this->prop, $prefs);
-
- // 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']);
}
@@ -464,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 */ }
@@ -498,6 +536,77 @@ class rcube_config
}
}
+ // 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/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php
index 3919cdc6e..5c9e5ab39 100644
--- a/program/lib/Roundcube/rcube_contacts.php
+++ b/program/lib/Roundcube/rcube_contacts.php
@@ -592,8 +592,8 @@ class rcube_contacts extends rcube_addressbook
// validate e-mail addresses
$valid = parent::validate($save_data, $autofix);
- // require at least one e-mail address (syntax check is already done)
- if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) {
+ // require at least one email address or a name
+ if ($valid && !strlen($save_data['firstname'].$save_data['surname'].$save_data['name']) && !array_filter($this->get_col_values('email', $save_data, true))) {
$this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
$valid = false;
}
@@ -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/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php
index fb8d8f103..00e6d4e20 100644
--- a/program/lib/Roundcube/rcube_csv2vcard.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index 852070073..aaba28172 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_db_mssql.php b/program/lib/Roundcube/rcube_db_mssql.php
index 3c1b9d71f..726e4b421 100644
--- a/program/lib/Roundcube/rcube_db_mssql.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_db_mysql.php b/program/lib/Roundcube/rcube_db_mysql.php
index 6fa5ad768..d3d0ac5c8 100644
--- a/program/lib/Roundcube/rcube_db_mysql.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_db_pgsql.php b/program/lib/Roundcube/rcube_db_pgsql.php
index d72c9d6b3..68bf6d85d 100644
--- a/program/lib/Roundcube/rcube_db_pgsql.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_db_sqlsrv.php b/program/lib/Roundcube/rcube_db_sqlsrv.php
index 45c41cdaf..4339f3dfd 100644
--- a/program/lib/Roundcube/rcube_db_sqlsrv.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php
index 9b248a3a8..01362e6fb 100644
--- a/program/lib/Roundcube/rcube_html2text.php
+++ b/program/lib/Roundcube/rcube_html2text.php
@@ -608,24 +608,27 @@ class rcube_html2text
$this->width = $p_width;
// Add citation markers and create <pre> block
- $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body));
+ $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body));
$body = '<pre>' . htmlspecialchars($body) . '</pre>';
- $text = substr($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);
}
}
/**
* Callback function to correctly add citation markers for blockquote contents
*/
- public function blockquote_citation_ballback($m)
+ public function blockquote_citation_callback($m)
{
- $line = ltrim($m[2]);
+ $line = ltrim($m[2]);
$space = $line[0] == '>' ? '' : ' ';
+
return $m[1] . '>' . $space . $line;
}
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 689a6266d..9faf1bbc6 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -3760,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) {
@@ -3785,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);
}
}
@@ -3799,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/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index 33e45c365..a8166545e 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/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);
- if (!empty($result[$uid])) {
- // save memory, we don't need message body here (?)
- $result[$uid]->body = null;
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $uid = intval($sql_arr['uid']);
+ $result[$uid] = $this->build_message($sql_arr);
- unset($msgs[$uid]);
+ if (!empty($result[$uid])) {
+ // save memory, we don't need message body here (?)
+ $result[$uid]->body = null;
+
+ 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
@@ -1174,11 +1229,16 @@ class rcube_imap_cache
*
* @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
@@ -1192,13 +1252,13 @@ class rcube_imap_cache
if (is_array($msg->structure->parts)) {
foreach ($msg->structure->parts as $part) {
- $this->message_object_prepare($part);
+ $this->message_object_prepare($part, $size);
}
}
if (is_array($msg->parts)) {
foreach ($msg->parts as $part) {
- $this->message_object_prepare($part);
+ $this->message_object_prepare($part, $size);
}
}
}
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index bce4cd4e2..f9a62f010 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -48,6 +48,8 @@ class rcube_imap_generic
'*' => '\\*',
);
+ public static $mupdate;
+
private $fp;
private $host;
private $logged = false;
@@ -3156,6 +3158,11 @@ class rcube_imap_generic
}
foreach ($data as $entry) {
+ // Workaround cyrus-murder bug, the entry[2] string needs to be escaped
+ if (self::$mupdate) {
+ $entry[2] = addcslashes($entry[2], '\\"');
+ }
+
// ANNOTATEMORE drafts before version 08 require quoted parameters
$entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
$this->escape($entry[1], true), $this->escape($entry[2], true));
@@ -3720,6 +3727,10 @@ class rcube_imap_generic
$this->prefs['literal+'] = true;
}
+ if (preg_match('/(\[| )MUPDATE=.*/', $str)) {
+ self::$mupdate = true;
+ }
+
if ($trusted) {
$this->capability_readed = true;
}
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index cb7fa8466..64288f973 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php
index 88378dc22..923a12a41 100644
--- a/program/lib/Roundcube/rcube_ldap_generic.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index 0d33ea44d..9b662a286 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php
index 572540f47..a931c27c1 100644
--- a/program/lib/Roundcube/rcube_mime.php
+++ b/program/lib/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)];
}
@@ -802,7 +810,7 @@ class rcube_mime
}
$mime_types = $mime_extensions = array();
- $regex = "/([\w\+\-\.\/]+)\t+([\w\s]+)/i";
+ $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
foreach((array)$lines as $line) {
// skip comments or mime types w/o any extensions
if ($line[0] == '#' || !preg_match($regex, $line, $matches))
@@ -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/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php
index 33f04eaa5..2258f1486 100644
--- a/program/lib/Roundcube/rcube_plugin_api.php
+++ b/program/lib/Roundcube/rcube_plugin_api.php
@@ -35,8 +35,9 @@ class rcube_plugin_api
public $url = 'plugins/';
public $task = '';
public $output;
- public $handlers = array();
- public $allowed_prefs = array();
+ public $handlers = array();
+ public $allowed_prefs = array();
+ public $allowed_session_prefs = array();
protected $plugins = array();
protected $tasks = array();
@@ -403,7 +404,7 @@ class rcube_plugin_api
$args = $ret + $args;
}
- if ($args['abort']) {
+ if ($args['break']) {
break;
}
}
diff --git a/program/lib/Roundcube/rcube_spellcheck_atd.php b/program/lib/Roundcube/rcube_spellcheck_atd.php
new file mode 100644
index 000000000..9f073f56f
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_atd.php
@@ -0,0 +1,204 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation for afterthedeadline services |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with an After the Deadline service
+ * See http://www.afterthedeadline.com/ for more information
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_atd extends rcube_spellcheck_engine
+{
+ const SERVICE_HOST = 'service.afterthedeadline.com';
+ const SERVICE_PORT = 80;
+
+ private $matches = array();
+ private $content;
+ private $langhosts = array(
+ 'fr' => 'fr.',
+ 'de' => 'de.',
+ 'pt' => 'pt.',
+ 'es' => 'es.',
+ );
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $langs = array_values($this->langhosts);
+ $langs[] = 'en';
+ return $langs;
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->content = $text;
+
+ // spell check uri is configured
+ $rcube = rcube::get_instance();
+ $url = $rcube->config->get('spellcheck_uri');
+ $key = $rcube->config->get('spellcheck_atd_key');
+
+ 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::SERVICE_HOST;
+ $port = self::SERVICE_PORT;
+ $path = '/checkDocument';
+
+ // prefix host for other languages than 'en'
+ $lang = substr($this->lang, 0, 2);
+ if ($this->langhosts[$lang])
+ $host = $this->langhosts[$lang] . $host;
+ }
+
+ $postdata = 'data=' . urlencode($text);
+
+ if (!empty($key))
+ $postdata .= '&key=' . urlencode($key);
+
+ $response = $headers = '';
+ $in_header = true;
+ 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($postdata) . "\r\n";
+ $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
+ $out .= "Connection: Close\r\n\r\n";
+ $out .= $postdata;
+ fwrite($fp, $out);
+
+ while (!feof($fp)) {
+ if ($in_header) {
+ $line = fgets($fp, 512);
+ $headers .= $line;
+ if (trim($line) == '')
+ $in_header = false;
+ }
+ else {
+ $response .= fgets($fp, 1024);
+ }
+ }
+ fclose($fp);
+ }
+
+ // parse HTTP response headers
+ if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $headers, $m)) {
+ $http_status = $m[1];
+ if ($http_status != '200')
+ $this->error = 'HTTP ' . $m[1] . $m[2];
+ }
+
+ if (!$response) {
+ $this->error = "Empty result from spelling engine";
+ }
+
+ try {
+ $result = new SimpleXMLElement($response);
+ }
+ catch (Exception $e) {
+ $thid->error = "Unexpected response from server: " . $store;
+ return array();
+ }
+
+ foreach ($result->error as $error) {
+ if (strval($error->type) == 'spelling') {
+ $word = strval($error->string);
+
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ continue;
+ }
+
+ $prefix = strval($error->precontext);
+ $start = $prefix ? mb_strpos($text, $prefix) : 0;
+ $pos = mb_strpos($text, $word, $start);
+ $len = mb_strlen($word);
+ $num = 0;
+
+ $match = array($word, $pos, $len, null, array());
+ foreach ($error->suggestions->option as $option) {
+ $match[4][] = strval($option);
+ if (++$num == self::MAX_SUGGESTIONS)
+ break;
+ }
+ $matches[] = $match;
+ }
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $matches = $word ? $this->check($word) : $this->matches;
+
+ if ($matches[0][4]) {
+ return $matches[0][4];
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ if ($text) {
+ $matches = $this->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;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_enchant.php b/program/lib/Roundcube/rcube_spellcheck_enchant.php
new file mode 100644
index 000000000..14d6fff46
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_enchant.php
@@ -0,0 +1,182 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2011-2013, Kolab Systems AG |
+ | Copyright (C) 20011-2013, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation to work with Enchant |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with Pspell
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_enchant extends rcube_spellcheck_engine
+{
+ private $enchant_broker;
+ private $enchant_dictionary;
+ private $matches = array();
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $this->init();
+
+ $langs = array();
+ $dicts = enchant_broker_list_dicts($this->enchant_broker);
+ foreach ($dicts as $dict) {
+ $langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
+ }
+
+ return array_unique($langs);
+ }
+
+ /**
+ * Initializes Enchant dictionary
+ */
+ private function init()
+ {
+ 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);
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->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->dictionary->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);
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $this->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();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ $result = array();
+
+ if ($text) {
+ // init spellchecker
+ $this->init();
+
+ if (!$this->enchant_dictionary) {
+ return array();
+ }
+
+ // With Enchant we don't need to get suggestions to return misspelled words
+ $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->dictionary->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;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_engine.php b/program/lib/Roundcube/rcube_spellcheck_engine.php
new file mode 100644
index 000000000..3cb4ca3de
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_engine.php
@@ -0,0 +1,91 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | 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. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Interface class for a spell-checking backend |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Interface class for a spell-checking backend
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+abstract class rcube_spellcheck_engine
+{
+ const MAX_SUGGESTIONS = 10;
+
+ protected $lang;
+ protected $error;
+ protected $dictionary;
+ protected $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
+
+ /**
+ * Default constructor
+ */
+ public function __construct($dict, $lang)
+ {
+ $this->dictionary = $dict;
+ $this->lang = $lang;
+ }
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @return array Indexed list of language codes
+ */
+ abstract function languages();
+
+ /**
+ * Set content and check spelling
+ *
+ * @param string $text Text content for spellchecking
+ *
+ * @return bool True when no mispelling found, otherwise false
+ */
+ abstract function check($text);
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @param string $word The word
+ *
+ * @return array Suggestions list
+ */
+ abstract function get_suggestions($word);
+
+ /**
+ * Returns misspelled words
+ *
+ * @param string $text The content for spellchecking. If empty content
+ * used for check() method will be used.
+ *
+ * @return array List of misspelled words
+ */
+ abstract function get_words($text = null);
+
+ /**
+ * Returns error message
+ *
+ * @return string Error message
+ */
+ public function error()
+ {
+ return $this->error;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_googie.php b/program/lib/Roundcube/rcube_spellcheck_googie.php
new file mode 100644
index 000000000..3777942a6
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_googie.php
@@ -0,0 +1,176 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | 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. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation to work with Googiespell |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with a Googiespell service
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_googie extends rcube_spellcheck_engine
+{
+ const GOOGIE_HOST = 'ssl://spell.roundcube.net';
+ const GOOGIE_PORT = 443;
+
+ private $matches = array();
+ private $content;
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ return array('am','ar','ar','bg','br','ca','cs','cy','da',
+ 'de_CH','de_DE','el','en_GB','en_US',
+ 'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl',
+ 'he','hr','hu','hy','is','it','ku','lt','lv','nl',
+ 'pl','pt_BR','pt_PT','ro','ru',
+ 'sk','sl','sv','uk');
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->content = $text;
+
+ // spell check uri is configured
+ $url = rcube::get_instance()->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::GOOGIE_HOST;
+ $port = self::GOOGIE_PORT;
+ $path = '/tbproxy/spell?lang=' . $this->lang;
+ }
+
+ $path .= sprintf('&key=%06d', $_SESSION['user_id']);
+
+ $gtext = '<?xml version="1.0" encoding="utf-8" ?>'
+ .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
+ .'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>'
+ .'</spellrequest>';
+
+ $store = '';
+ if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
+ $out = "POST $path HTTP/1.0\r\n";
+ $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
+ $out .= "User-Agent: Roundcube Webmail/" . RCMAIL_VERSION . " (Googiespell Wrapper)\r\n";
+ $out .= "Content-Length: " . strlen($gtext) . "\r\n";
+ $out .= "Content-Type: text/xml\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];
+ $this->error .= "\n" . $store;
+ }
+ }
+
+ 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";
+ $this->error .= preg_match('/<errortext>([^<]+)/', $store, $m) ? ": " . html_entity_decode($m[1]) : '';
+ }
+
+ preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
+
+ // skip exceptions (if appropriate options are enabled)
+ foreach ($matches as $idx => $m) {
+ $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
+ // skip exceptions
+ if ($this->dictionary->is_exception($word)) {
+ unset($matches[$idx]);
+ }
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $matches = $word ? $this->check($word) : $this->matches;
+
+ if ($matches[0][4]) {
+ $suggestions = explode("\t", $matches[0][4]);
+ if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
+ $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+ }
+
+ return $suggestions;
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ if ($text) {
+ $matches = $this->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;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellcheck_pspell.php b/program/lib/Roundcube/rcube_spellcheck_pspell.php
new file mode 100644
index 000000000..b12684e43
--- /dev/null
+++ b/program/lib/Roundcube/rcube_spellcheck_pspell.php
@@ -0,0 +1,189 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | 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. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Spellchecking backend implementation to work with Pspell |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with Pspell
+ *
+ * @package Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_pspell extends rcube_spellcheck_engine
+{
+ private $plink;
+ private $matches = array();
+
+ /**
+ * Return a list of languages supported by this backend
+ *
+ * @see rcube_spellcheck_engine::languages()
+ */
+ function languages()
+ {
+ $defaults = array('en');
+ $langs = array();
+
+ // get aspell dictionaries
+ exec('aspell dump dicts', $dicts);
+ if (!empty($dicts)) {
+ $seen = array();
+ foreach ($dicts as $lang) {
+ $lang = preg_replace('/-.*$/', '', $lang);
+ $langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang;
+ if (!$seen[$langc]++)
+ $langs[] = $lang;
+ }
+ $langs = array_unique($langs);
+ }
+ else {
+ $langs = $defaults;
+ }
+
+ return $langs;
+ }
+
+ /**
+ * Initializes PSpell dictionary
+ */
+ private function init()
+ {
+ 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";
+ }
+ }
+
+ /**
+ * Set content and check spelling
+ *
+ * @see rcube_spellcheck_engine::check()
+ */
+ function check($text)
+ {
+ $this->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->dictionary->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);
+ }
+
+ $this->matches = $matches;
+ return $matches;
+ }
+
+ /**
+ * Returns suggestions for the specified word
+ *
+ * @see rcube_spellcheck_engine::get_words()
+ */
+ function get_suggestions($word)
+ {
+ $this->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();
+ }
+
+ /**
+ * Returns misspelled words
+ *
+ * @see rcube_spellcheck_engine::get_suggestions()
+ */
+ function get_words($text = null)
+ {
+ $result = array();
+
+ if ($text) {
+ // init spellchecker
+ $this->init();
+
+ if (!$this->plink) {
+ return array();
+ }
+
+ // With PSpell we don't need to get suggestions to return misspelled words
+ $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->dictionary->is_exception($word)) {
+ continue;
+ }
+
+ if (!pspell_check($this->plink, $word)) {
+ $result[] = $word;
+ }
+ }
+
+ return $result;
+ }
+
+ foreach ($this->matches as $m) {
+ $result[] = $m[0];
+ }
+
+ return $result;
+ }
+
+}
+
diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php
index df4365223..d087d2584 100644
--- a/program/lib/Roundcube/rcube_spellchecker.php
+++ b/program/lib/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,8 +54,60 @@ 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'";
+ }
}
+ /**
+ * Return a list of supported languages
+ */
+ function languages()
+ {
+ // trust configuration
+ $configured = $this->rc->config->get('spellcheck_languages');
+ if (!empty($configured) && is_array($configured) && !$configured[0]) {
+ return $configured;
+ }
+ else if (!empty($configured)) {
+ $langs = (array)$configured;
+ }
+ else if ($this->backend) {
+ $langs = $this->backend->languages();
+ }
+
+ // load index
+ @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
+
+ // add correct labels
+ $languages = array();
+ foreach ($langs as $lang) {
+ $langc = strtolower(substr($lang, 0, 2));
+ $alias = $rcube_language_aliases[$langc];
+ if (!$alias) {
+ $alias = $langc.'_'.strtoupper($langc);
+ }
+ if ($rcube_languages[$lang]) {
+ $languages[$lang] = $rcube_languages[$lang];
+ }
+ else if ($rcube_languages[$alias]) {
+ $languages[$lang] = $rcube_languages[$alias];
+ }
+ else {
+ $languages[$lang] = ucfirst($lang);
+ }
+ }
+
+ asort($languages);
+
+ return $languages;
+ }
/**
* Set content and check spelling
@@ -81,14 +127,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 +155,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 +173,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();
}
@@ -157,7 +195,7 @@ class rcube_spellchecker
// send output
$out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
- foreach ($this->matches as $item) {
+ foreach ((array)$this->matches as $item) {
$out .= '<c o="'.$item[1].'" l="'.$item[2].'">';
$out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
$out .= '</c>';
@@ -178,7 +216,7 @@ class rcube_spellchecker
{
$result = array();
- foreach ($this->matches as $item) {
+ foreach ((array)$this->matches as $item) {
if ($this->engine == 'pspell') {
$word = $item[0];
}
@@ -199,394 +237,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/program/lib/Roundcube/rcube_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php
index 354b4596d..77b91d18b 100644
--- a/program/lib/Roundcube/rcube_string_replacer.php
+++ b/program/lib/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/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php
index 2540f779d..b73bc0812 100644
--- a/program/lib/Roundcube/rcube_utils.php
+++ b/program/lib/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;
}
@@ -787,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/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index a71305c4b..5f74ccbd4 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/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;
@@ -517,20 +518,28 @@ class rcube_vcard
*/
public static function cleanup($vcard)
{
- // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
- $vcard = preg_replace(
- '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
- '\2;type=\5\3:\4',
- $vcard);
-
// convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
$vcard = preg_replace_callback(
'/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
array('self', 'x_abrelatednames_callback'),
$vcard);
- // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
- $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
+ // Cleanup
+ $vcard = preg_replace(array(
+ // convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
+ '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
+ '/^item\d*\.X-AB.*$/m', // remove cruft like item1.X-AB*
+ '/^item\d*\./m', // remove item1.ADR instead of ADR
+ '/\n+/', // remove empty lines
+ '/^(N:[^;\R]*)$/m', // if N doesn't have any semicolons, add some
+ ),
+ array(
+ '\2;type=\5\3:\4',
+ '',
+ '',
+ "\n",
+ '\1;;;;',
+ ), $vcard);
// convert X-WAB-GENDER to X-GENDER
if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
@@ -538,9 +547,6 @@ class rcube_vcard
$vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
}
- // if N doesn't have any semicolons, add some
- $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
-
return $vcard;
}
@@ -611,8 +617,8 @@ class rcube_vcard
$enc = null;
foreach($regs2[1] as $attrid => $attr) {
+ $attr = preg_replace('/[\s\t\n\r\0\x0B]/', '', $attr);
if ((list($key, $value) = explode('=', $attr)) && $value) {
- $value = trim($value);
if ($key == 'ENCODING') {
$value = strtoupper($value);
// add next line(s) to value string if QP line end detected
@@ -756,7 +762,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 +771,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));
}
/**
@@ -791,7 +797,7 @@ class rcube_vcard
return $result;
}
- $s = strtr($s, $rep2);
+ $s = trim(strtr($s, $rep2));
}
// some implementations (GMail) use non-standard backslash before colon (#1489085)
diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
index 8f7fe9749..e7467545f 100644
--- a/program/lib/Roundcube/rcube_washtml.php
+++ b/program/lib/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