Page MenuHomePhorge

No OneTemporary

Size
444 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index 3b16015..494b5c3 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -1,1197 +1,1202 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| Copyright (C) 2011-2012, Kolab Systems AG |
| |
| 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: |
| Framework base class providing core functions and holding |
| instances of all 'global' objects like db- and storage-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Base class of the Roundcube Framework
* implemented as singleton
*
* @package Core
*/
class rcube
{
const INIT_WITH_DB = 1;
const INIT_WITH_PLUGINS = 2;
/**
* Singleton instace of rcube
*
* @var rcmail
*/
static protected $instance;
/**
* Stores instance of rcube_config.
*
* @var rcube_config
*/
public $config;
/**
* Instace of database class.
*
- * @var rcube_mdb2
+ * @var rcube_pdo
*/
public $db;
/**
* Instace of Memcache class.
*
- * @var rcube_mdb2
+ * @var Memcache
*/
public $memcache;
/**
* Instace of rcube_session class.
*
* @var rcube_session
*/
public $session;
/**
* Instance of rcube_smtp class.
*
* @var rcube_smtp
*/
public $smtp;
/**
* Instance of rcube_storage class.
*
* @var rcube_storage
*/
public $storage;
/**
* Instance of rcube_output class.
*
* @var rcube_output
*/
public $output;
/**
* Instance of rcube_plugin_api.
*
* @var rcube_plugin_api
*/
public $plugins;
/* private/protected vars */
protected $texts;
protected $caches = array();
protected $shutdown_functions = array();
protected $expunge_cache = false;
/**
* This implements the 'singleton' design pattern
*
* @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants
* @return rcube The one and only instance
*/
static function get_instance($mode = 0)
{
if (!self::$instance) {
self::$instance = new rcube();
self::$instance->init($mode);
}
return self::$instance;
}
/**
* Private constructor
*/
protected function __construct()
{
// load configuration
$this->config = new rcube_config();
$this->plugins = new rcube_dummy_plugin_api;
register_shutdown_function(array($this, 'shutdown'));
}
/**
* Initial startup function
*/
protected function init($mode = 0)
{
// initialize syslog
if ($this->config->get('log_driver') == 'syslog') {
$syslog_id = $this->config->get('syslog_id', 'roundcube');
$syslog_facility = $this->config->get('syslog_facility', LOG_USER);
openlog($syslog_id, LOG_ODELAY, $syslog_facility);
}
// connect to database
if ($mode & self::INIT_WITH_DB) {
$this->get_dbh();
}
// create plugin API and load plugins
if ($mode & self::INIT_WITH_PLUGINS) {
$this->plugins = rcube_plugin_api::get_instance();
}
}
/**
* Get the current database connection
*
- * @return rcube_mdb2 Database connection object
+ * @return rcube_pdo Database connection object
*/
public function get_dbh()
{
if (!$this->db) {
$config_all = $this->config->all();
-
- $this->db = new rcube_mdb2($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
- $this->db->sqlite_initials = INSTALL_PATH . 'SQL/sqlite.initial.sql';
+ $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
$this->db->set_debug((bool)$config_all['sql_debug']);
}
return $this->db;
}
/**
* Get global handle for memcache access
*
* @return object Memcache
*/
public function get_memcache()
{
if (!isset($this->memcache)) {
// no memcache support in PHP
if (!class_exists('Memcache')) {
$this->memcache = false;
return false;
}
$this->memcache = new Memcache;
$this->mc_available = 0;
- // add alll configured hosts to pool
+ // add all configured hosts to pool
$pconnect = $this->config->get('memcache_pconnect', true);
foreach ($this->config->get('memcache_hosts', array()) as $host) {
- list($host, $port) = explode(':', $host);
- if (!$port) $port = 11211;
+ if (substr($host, 0, 7) != 'unix://') {
+ list($host, $port) = explode(':', $host);
+ if (!$port) $port = 11211;
+ }
+ else {
+ $port = 0;
+ }
+
$this->mc_available += intval($this->memcache->addServer($host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure')));
}
// test connection and failover (will result in $this->mc_available == 0 on complete failure)
$this->memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist
if (!$this->mc_available)
$this->memcache = false;
}
return $this->memcache;
}
/**
* Callback for memcache failure
*/
public function memcache_failure($host, $port)
{
static $seen = array();
// only report once
if (!$seen["$host:$port"]++) {
$this->mc_available--;
self::raise_error(array('code' => 604, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Memcache failure on host $host:$port"),
true, false);
}
}
/**
* Initialize and get cache object
*
* @param string $name Cache identifier
* @param string $type Cache type ('db', 'apc' or 'memcache')
* @param string $ttl Expiration time for cache items
* @param bool $packed Enables/disables data serialization
*
* @return rcube_cache Cache object
*/
public function get_cache($name, $type='db', $ttl=0, $packed=true)
{
if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) {
$this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed);
}
return $this->caches[$name];
}
/**
* Create SMTP object and connect to server
*
* @param boolean True if connection should be established
*/
public function smtp_init($connect = false)
{
$this->smtp = new rcube_smtp();
if ($connect)
$this->smtp->connect();
}
/**
* Initialize and get storage object
*
* @return rcube_storage Storage object
*/
public function get_storage()
{
// already initialized
if (!is_object($this->storage)) {
$this->storage_init();
}
return $this->storage;
}
/**
* Initialize storage object
*/
public function storage_init()
{
// already initialized
if (is_object($this->storage)) {
return;
}
$driver = $this->config->get('storage_driver', 'imap');
$driver_class = "rcube_{$driver}";
if (!class_exists($driver_class)) {
self::raise_error(array(
'code' => 700, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Storage driver class ($driver) not found!"),
true, true);
}
// Initialize storage object
$this->storage = new $driver_class;
// for backward compat. (deprecated, will be removed)
$this->imap = $this->storage;
// enable caching of mail data
$storage_cache = $this->config->get("{$driver}_cache");
$messages_cache = $this->config->get('messages_cache');
// for backward compatybility
if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
$storage_cache = 'db';
$messages_cache = true;
}
if ($storage_cache)
$this->storage->set_caching($storage_cache);
if ($messages_cache)
$this->storage->set_messages_caching(true);
// set pagesize from config
$pagesize = $this->config->get('mail_pagesize');
if (!$pagesize) {
$pagesize = $this->config->get('pagesize', 50);
}
$this->storage->set_pagesize($pagesize);
// set class options
$options = array(
'auth_type' => $this->config->get("{$driver}_auth_type", 'check'),
'auth_cid' => $this->config->get("{$driver}_auth_cid"),
'auth_pw' => $this->config->get("{$driver}_auth_pw"),
'debug' => (bool) $this->config->get("{$driver}_debug"),
'force_caps' => (bool) $this->config->get("{$driver}_force_caps"),
'timeout' => (int) $this->config->get("{$driver}_timeout"),
'skip_deleted' => (bool) $this->config->get('skip_deleted'),
'driver' => $driver,
);
if (!empty($_SESSION['storage_host'])) {
$options['host'] = $_SESSION['storage_host'];
$options['user'] = $_SESSION['username'];
$options['port'] = $_SESSION['storage_port'];
$options['ssl'] = $_SESSION['storage_ssl'];
$options['password'] = $this->decrypt($_SESSION['password']);
$_SESSION[$driver.'_host'] = $_SESSION['storage_host'];
}
$options = $this->plugins->exec_hook("storage_init", $options);
// for backward compat. (deprecated, to be removed)
$options = $this->plugins->exec_hook("imap_init", $options);
$this->storage->set_options($options);
$this->set_storage_prop();
}
/**
* Set storage parameters.
* This must be done AFTER connecting to the server!
*/
protected function set_storage_prop()
{
$storage = $this->get_storage();
$storage->set_charset($this->config->get('default_charset', RCMAIL_CHARSET));
if ($default_folders = $this->config->get('default_folders')) {
$storage->set_default_folders($default_folders);
}
if (isset($_SESSION['mbox'])) {
$storage->set_folder($_SESSION['mbox']);
}
if (isset($_SESSION['page'])) {
$storage->set_page($_SESSION['page']);
}
}
/**
* Create session object and start the session.
*/
public function session_init()
{
// session started (Installer?)
if (session_id()) {
return;
}
$sess_name = $this->config->get('session_name');
$sess_domain = $this->config->get('session_domain');
$lifetime = $this->config->get('session_lifetime', 0) * 60;
// set session domain
if ($sess_domain) {
ini_set('session.cookie_domain', $sess_domain);
}
// set session garbage collecting time according to session_lifetime
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
ini_set('session.cookie_secure', rcube_utils::https_check());
ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.serialize_handler', 'php');
+ ini_set('session.cookie_httponly', 1);
// use database for storing session data
$this->session = new rcube_session($this->get_dbh(), $this->config);
$this->session->register_gc_handler(array($this, 'temp_gc'));
$this->session->register_gc_handler(array($this, 'cache_gc'));
// start PHP session (if not in CLI mode)
if ($_SERVER['REMOTE_ADDR']) {
session_start();
}
}
/**
* Configure session object internals
*/
public function session_configure()
{
if (!$this->session) {
return;
}
$lifetime = $this->config->get('session_lifetime', 0) * 60;
$keep_alive = $this->config->get('keep_alive');
// set keep-alive/check-recent interval
if ($keep_alive) {
// be sure that it's less than session lifetime
if ($lifetime) {
$keep_alive = min($keep_alive, $lifetime - 30);
}
$keep_alive = max(60, $keep_alive);
$this->session->set_keep_alive($keep_alive);
}
$this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->config->get('ip_check'));
}
/**
* Garbage collector function for temp files.
* Remove temp files older than two days
*/
public function temp_gc()
{
$tmp = unslashify($this->config->get('temp_dir'));
$expire = time() - 172800; // expire in 48 hours
if ($tmp && ($dir = opendir($tmp))) {
while (($fname = readdir($dir)) !== false) {
if ($fname{0} == '.') {
continue;
}
if (filemtime($tmp.'/'.$fname) < $expire) {
@unlink($tmp.'/'.$fname);
}
}
closedir($dir);
}
}
/**
* Garbage collector for cache entries.
* Set flag to expunge caches on shutdown
*/
public function cache_gc()
{
// because this gc function is called before storage is initialized,
// we just set a flag to expunge storage cache on shutdown.
$this->expunge_cache = true;
}
/**
* Get localized text in the desired language
*
* @param mixed $attrib Named parameters array or label name
* @param string $domain Label domain (plugin) name
*
* @return string Localized text
*/
public function gettext($attrib, $domain=null)
{
// load localization files if not done yet
if (empty($this->texts))
$this->load_language();
// extract attributes
if (is_string($attrib))
$attrib = array('name' => $attrib);
$name = $attrib['name'] ? $attrib['name'] : '';
// attrib contain text values: use them from now
if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us']))
$this->texts[$name] = $setval;
// check for text with domain
if ($domain && ($text = $this->texts[$domain.'.'.$name]))
;
// text does not exist
else if (!($text = $this->texts[$name])) {
return "[$name]";
}
// replace vars in text
if (is_array($attrib['vars'])) {
foreach ($attrib['vars'] as $var_key => $var_value)
$text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
}
// format output
if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst'])
return ucfirst($text);
else if ($attrib['uppercase'])
return mb_strtoupper($text);
else if ($attrib['lowercase'])
return mb_strtolower($text);
return strtr($text, array('\n' => "\n"));
}
/**
* Check if the given text label exists
*
* @param string $name Label name
* @param string $domain Label domain (plugin) name or '*' for all domains
* @param string $ref_domain Sets domain name if label is found
*
* @return boolean True if text exists (either in the current language or in en_US)
*/
public function text_exists($name, $domain = null, &$ref_domain = null)
{
// load localization files if not done yet
if (empty($this->texts))
$this->load_language();
if (isset($this->texts[$name])) {
$ref_domain = '';
return true;
}
// any of loaded domains (plugins)
if ($domain == '*') {
foreach ($this->plugins->loaded_plugins() as $domain)
if (isset($this->texts[$domain.'.'.$name])) {
$ref_domain = $domain;
return true;
}
}
// specified domain
else if ($domain) {
$ref_domain = $domain;
return isset($this->texts[$domain.'.'.$name]);
}
return false;
}
/**
* Load a localization package
*
* @param string Language ID
*/
public function load_language($lang = null, $add = array())
{
$lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
// load localized texts
if (empty($this->texts) || $lang != $_SESSION['language']) {
$this->texts = array();
// handle empty lines after closing PHP tag in localization files
ob_start();
// get english labels (these should be complete)
@include(INSTALL_PATH . 'program/localization/en_US/labels.inc');
@include(INSTALL_PATH . 'program/localization/en_US/messages.inc');
if (is_array($labels))
$this->texts = $labels;
if (is_array($messages))
$this->texts = array_merge($this->texts, $messages);
// include user language files
if ($lang != 'en' && $lang != 'en_US' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc');
include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc');
if (is_array($labels))
$this->texts = array_merge($this->texts, $labels);
if (is_array($messages))
$this->texts = array_merge($this->texts, $messages);
}
ob_end_clean();
$_SESSION['language'] = $lang;
}
// append additional texts (from plugin)
if (is_array($add) && !empty($add))
$this->texts += $add;
}
/**
* Check the given string and return a valid language code
*
* @param string Language code
* @return string Valid language code
*/
protected function language_prop($lang)
{
static $rcube_languages, $rcube_language_aliases;
// 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]);
}
if (empty($rcube_languages)) {
@include(INSTALL_PATH . 'program/localization/index.inc');
}
// check if we have an alias for that language
if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
$lang = $rcube_language_aliases[$lang];
}
// try the first two chars
else if (!isset($rcube_languages[$lang])) {
$short = substr($lang, 0, 2);
// check if we have an alias for the short language code
if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
$lang = $rcube_language_aliases[$short];
}
// expand 'nn' to 'nn_NN'
else if (!isset($rcube_languages[$short])) {
$lang = $short.'_'.strtoupper($short);
}
}
if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
$lang = 'en_US';
}
return $lang;
}
/**
* Read directory program/localization and return a list of available languages
*
* @return array List of available localizations
*/
public function list_languages()
{
static $sa_languages = array();
if (!sizeof($sa_languages)) {
@include(INSTALL_PATH . 'program/localization/index.inc');
if ($dh = @opendir(INSTALL_PATH . 'program/localization')) {
while (($name = readdir($dh)) !== false) {
if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name))
continue;
if ($label = $rcube_languages[$name])
$sa_languages[$name] = $label;
}
closedir($dh);
}
}
return $sa_languages;
}
/**
* Encrypt using 3DES
*
* @param string $clear clear text input
* @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
* @param boolean $base64 whether or not to base64_encode() the result before returning
*
* @return string encrypted text
*/
public function encrypt($clear, $key = 'des_key', $base64 = true)
{
if (!$clear)
return '';
/*-
* Add a single canary byte to the end of the clear text, which
* will help find out how much of padding will need to be removed
* upon decryption; see http://php.net/mcrypt_generic#68082
*/
$clear = pack("a*H2", $clear, "80");
if (function_exists('mcrypt_module_open') &&
($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))) {
$iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
$cipher = $iv . mcrypt_generic($td, $clear);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
}
else {
@include_once 'des.inc';
if (function_exists('des')) {
$des_iv_size = 8;
$iv = $this->create_iv($des_iv_size);
$cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
}
else {
self::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
), true, true);
}
}
return $base64 ? base64_encode($cipher) : $cipher;
}
/**
* Decrypt 3DES-encrypted string
*
* @param string $cipher encrypted text
* @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
* @param boolean $base64 whether or not input is base64-encoded
*
* @return string decrypted text
*/
public function decrypt($cipher, $key = 'des_key', $base64 = true)
{
if (!$cipher)
return '';
$cipher = $base64 ? base64_decode($cipher) : $cipher;
if (function_exists('mcrypt_module_open') &&
($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))) {
$iv_size = mcrypt_enc_get_iv_size($td);
$iv = substr($cipher, 0, $iv_size);
// session corruption? (#1485970)
if (strlen($iv) < $iv_size)
return '';
$cipher = substr($cipher, $iv_size);
mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
$clear = mdecrypt_generic($td, $cipher);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
}
else {
@include_once 'des.inc';
if (function_exists('des')) {
$des_iv_size = 8;
$iv = substr($cipher, 0, $des_iv_size);
$cipher = substr($cipher, $des_iv_size);
$clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
}
else {
self::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
), true, true);
}
}
/*-
* Trim PHP's padding and the canary byte; see note in
* rcube::encrypt() and http://php.net/mcrypt_generic#68082
*/
$clear = substr(rtrim($clear, "\0"), 0, -1);
return $clear;
}
/**
* Generates encryption initialization vector (IV)
*
* @param int Vector size
* @return string Vector string
*/
private function create_iv($size)
{
// mcrypt_create_iv() can be slow when system lacks entrophy
// we'll generate IV vector manually
$iv = '';
for ($i = 0; $i < $size; $i++)
$iv .= chr(mt_rand(0, 255));
return $iv;
}
/**
* Build a valid URL to this instance of Roundcube
*
* @param mixed Either a string with the action or url parameters as key-value pairs
* @return string Valid application URL
*/
public function url($p)
{
// STUB: should be overloaded by the application
return '';
}
/**
* Function to be executed in script shutdown
* Registered with register_shutdown_function()
*/
public function shutdown()
{
foreach ($this->shutdown_functions as $function)
call_user_func($function);
if (is_object($this->smtp))
$this->smtp->disconnect();
foreach ($this->caches as $cache) {
if (is_object($cache))
$cache->close();
}
if (is_object($this->storage)) {
if ($this->expunge_cache)
$this->storage->expunge_cache();
$this->storage->close();
}
}
/**
* Registers shutdown function to be executed on shutdown.
* The functions will be executed before destroying any
* objects like smtp, imap, session, etc.
*
* @param callback Function callback
*/
public function add_shutdown_function($function)
{
$this->shutdown_functions[] = $function;
}
/**
* Construct shell command, execute it and return output as string.
* Keywords {keyword} are replaced with arguments
*
* @param $cmd Format string with {keywords} to be replaced
* @param $values (zero, one or more arrays can be passed)
* @return output of command. shell errors not detectable
*/
public static function exec(/* $cmd, $values1 = array(), ... */)
{
$args = func_get_args();
$cmd = array_shift($args);
$values = $replacements = array();
// merge values into one array
foreach ($args as $arg)
$values += (array)$arg;
preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
foreach ($matches as $tags) {
list(, $tag, $option, $key) = $tags;
$parts = array();
if ($option) {
foreach ((array)$values["-$key"] as $key => $value) {
if ($value === true || $value === false || $value === null)
$parts[] = $value ? $key : "";
else foreach ((array)$value as $val)
$parts[] = "$key " . escapeshellarg($val);
}
}
else {
foreach ((array)$values[$key] as $value)
$parts[] = escapeshellarg($value);
}
$replacements[$tag] = join(" ", $parts);
}
// use strtr behaviour of going through source string once
$cmd = strtr($cmd, $replacements);
return (string)shell_exec($cmd);
}
/**
* Print or write debug messages
*
* @param mixed Debug message or data
*/
public static function console()
{
$args = func_get_args();
if (class_exists('rcube', false)) {
$rcube = self::get_instance();
$plugin = $rcube->plugins->exec_hook('console', array('args' => $args));
if ($plugin['abort']) {
return;
}
$args = $plugin['args'];
}
$msg = array();
foreach ($args as $arg) {
$msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
}
self::write_log('console', join(";\n", $msg));
}
/**
* Append a line to a logfile in the logs directory.
* Date will be added automatically to the line.
*
* @param $name name of log file
* @param line Line to append
*/
public static function write_log($name, $line)
{
if (!is_string($line)) {
$line = var_export($line, true);
}
$date_format = self::$instance ? self::$instance->config->get('log_date_format') : null;
$log_driver = self::$instance ? self::$instance->config->get('log_driver') : null;
if (empty($date_format)) {
$date_format = 'd-M-Y H:i:s O';
}
$date = date($date_format);
// trigger logging hook
if (is_object(self::$instance) && is_object(self::$instance->plugins)) {
$log = self::$instance->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
$name = $log['name'];
$line = $log['line'];
$date = $log['date'];
if ($log['abort'])
return true;
}
if ($log_driver == 'syslog') {
$prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
syslog($prio, $line);
return true;
}
// log_driver == 'file' is assumed here
$line = sprintf("[%s]: %s\n", $date, $line);
$log_dir = self::$instance ? self::$instance->config->get('log_dir') : null;
if (empty($log_dir)) {
$log_dir = INSTALL_PATH . 'logs';
}
// try to open specific log file for writing
$logfile = $log_dir.'/'.$name;
if ($fp = @fopen($logfile, 'a')) {
fwrite($fp, $line);
fflush($fp);
fclose($fp);
return true;
}
trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
return false;
}
/**
* Throw system error (and show error page).
*
* @param array Named parameters
* - 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
* @param boolean True to log the error
* @param boolean Terminate script execution
*/
public static function raise_error($arg = array(), $log = false, $terminate = false)
{
// handle PHP exceptions
if (is_object($arg) && is_a($arg, 'Exception')) {
$err = array(
'type' => 'php',
'code' => $arg->getCode(),
'line' => $arg->getLine(),
'file' => $arg->getFile(),
'message' => $arg->getMessage(),
);
$arg = $err;
}
// installer
if (class_exists('rcube_install', false)) {
$rci = rcube_install::get_instance();
$rci->raise_error($arg);
return;
}
if (($log || $terminate) && $arg['type'] && $arg['message']) {
$arg['fatal'] = $terminate;
self::log_bug($arg);
}
// display error page and terminate script
if ($terminate && is_object(self::$instance->output)) {
self::$instance->output->raise_error($arg['code'], $arg['message']);
}
}
/**
* Report error according to configured debug_level
*
* @param array Named parameters
* @see self::raise_error()
*/
public static function log_bug($arg_arr)
{
$program = strtoupper($arg_arr['type']);
$level = self::get_instance()->config->get('debug_level');
// disable errors for ajax requests, write to log instead (#1487831)
if (($level & 4) && !empty($_REQUEST['_remote'])) {
$level = ($level ^ 4) | 1;
}
// write error to local log file
if (($level & 1) || !empty($arg_arr['fatal'])) {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$post_query = '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']);
}
else {
$post_query = '';
}
$log_entry = sprintf("%s Error: %s%s (%s %s)",
$program,
$arg_arr['message'],
$arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI'] . $post_query);
if (!self::write_log('errors', $log_entry)) {
// send error to PHPs error handler if write_log didn't succeed
trigger_error($arg_arr['message']);
}
}
// report the bug to the global bug reporting system
if ($level & 2) {
// TODO: Send error via HTTP
}
// show error if debug_mode is on
if ($level & 4) {
print "<b>$program Error";
if (!empty($arg_arr['file']) && !empty($arg_arr['line'])) {
print " in $arg_arr[file] ($arg_arr[line])";
}
print ':</b>&nbsp;';
print nl2br($arg_arr['message']);
print '<br />';
flush();
}
}
/**
* Returns current time (with microseconds).
*
* @return float Current time in seconds since the Unix
*/
public static function timer()
{
return microtime(true);
}
/**
* Logs time difference according to provided timer
*
* @param float $timer Timer (self::timer() result)
* @param string $label Log line prefix
* @param string $dest Log file name
*
* @see self::timer()
*/
public static function print_timer($timer, $label = 'Timer', $dest = 'console')
{
static $print_count = 0;
$print_count++;
$now = self::timer();
$diff = $now - $timer;
if (empty($label)) {
$label = 'Timer '.$print_count;
}
self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
}
/**
* Getter for logged user ID.
*
* @return mixed User identifier
*/
public function get_user_id()
{
if (is_object($this->user)) {
return $this->user->ID;
}
else if (isset($_SESSION['user_id'])) {
return $_SESSION['user_id'];
}
return null;
}
/**
* Getter for logged user name.
*
* @return string User name
*/
public function get_user_name()
{
if (is_object($this->user)) {
return $this->user->get_username();
}
return null;
}
}
/**
* Lightweight plugin API class serving as a dummy if plugins are not enabled
*
* @package Core
*/
class rcube_dummy_plugin_api
{
/**
* Triggers a plugin hook.
* @see rcube_plugin_api::exec_hook()
*/
public function exec_hook($hook, $args = array())
{
return $args;
}
}
diff --git a/lib/ext/Roundcube/rcube_cache.php b/lib/ext/Roundcube/rcube_cache.php
index 01f6203..17a8859 100644
--- a/lib/ext/Roundcube/rcube_cache.php
+++ b/lib/ext/Roundcube/rcube_cache.php
@@ -1,558 +1,556 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_cache.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011, The Roundcube Dev Team |
| Copyright (C) 2011, Kolab Systems AG |
| |
| 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: |
| Caching engine |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube cache
*
* @package Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @version 1.1
*/
class rcube_cache
{
/**
- * Instance of rcube_mdb2 or Memcache class
+ * Instance of database handler
*
- * @var rcube_mdb2/Memcache
+ * @var rcube_db|Memcache|bool
*/
private $db;
private $type;
private $userid;
private $prefix;
private $ttl;
private $packed;
private $index;
private $cache = array();
- private $cache_keys = array();
private $cache_changes = array();
private $cache_sums = array();
/**
* Object constructor.
*
* @param string $type Engine type ('db' or 'memcache' or 'apc')
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
{
$rcube = rcube::get_instance();
$type = strtolower($type);
if ($type == 'memcache') {
$this->type = 'memcache';
$this->db = $rcube->get_memcache();
}
else if ($type == 'apc') {
$this->type = 'apc';
$this->db = function_exists('apc_exists'); // APC 3.1.4 required
}
else {
$this->type = 'db';
$this->db = $rcube->get_dbh();
}
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->userid = (int) $userid;
$this->ttl = $ttl;
$this->packed = $packed;
$this->prefix = $prefix;
}
/**
* Returns cached value.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
function get($key)
{
if (!array_key_exists($key, $this->cache)) {
return $this->read_record($key);
}
return $this->cache[$key];
}
/**
* Sets (add/update) value in cache.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*/
function set($key, $data)
{
$this->cache[$key] = $data;
$this->cache_changed = true;
$this->cache_changes[$key] = true;
}
/**
* Returns cached value without storing it in internal memory.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
function read($key)
{
if (array_key_exists($key, $this->cache)) {
return $this->cache[$key];
}
return $this->read_record($key, true);
}
/**
* Sets (add/update) value in cache and immediately saves
* it in the backend, no internal memory will be used.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*
* @param boolean True on success, False on failure
*/
function write($key, $data)
{
return $this->write_record($key, $this->packed ? serialize($data) : $data);
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
function remove($key=null, $prefix_mode=false)
{
// Remove all keys
if ($key === null) {
$this->cache = array();
$this->cache_changed = false;
$this->cache_changes = array();
- $this->cache_keys = array();
+ $this->cache_sums = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach (array_keys($this->cache) as $k) {
if (strpos($k, $key) === 0) {
$this->cache[$k] = null;
$this->cache_changes[$k] = false;
- unset($this->cache_keys[$k]);
+ unset($this->cache_sums[$k]);
}
}
}
// Remove one key by name
else {
$this->cache[$key] = null;
$this->cache_changes[$key] = false;
- unset($this->cache_keys[$key]);
+ unset($this->cache_sums[$key]);
}
// Remove record(s) from the backend
$this->remove_record($key, $prefix_mode);
}
/**
* Remove cache records older than ttl
*/
function expunge()
{
if ($this->type == 'db' && $this->db) {
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache').
" WHERE user_id = ?".
" AND cache_key LIKE ?".
" AND " . $this->db->unixtimestamp('created')." < ?",
$this->userid,
$this->prefix.'.%',
time() - $this->ttl);
}
}
/**
* Writes the cache back to the DB.
*/
function close()
{
if (!$this->cache_changed) {
return;
}
foreach ($this->cache as $key => $data) {
// The key has been used
if ($this->cache_changes[$key]) {
// Make sure we're not going to write unchanged data
// by comparing current md5 sum with the sum calculated on DB read
$data = $this->packed ? serialize($data) : $data;
if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
$this->write_record($key, $data);
}
}
}
$this->write_index();
}
/**
* Reads cache entry.
*
* @param string $key Cache key name
* @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
*/
private function read_record($key, $nostore=false)
{
if (!$this->db) {
return null;
}
if ($this->type != 'db') {
if ($this->type == 'memcache') {
$data = $this->db->get($this->ckey($key));
}
else if ($this->type == 'apc') {
$data = apc_fetch($this->ckey($key));
}
if ($data) {
$md5sum = md5($data);
$data = $this->packed ? unserialize($data) : $data;
if ($nostore) {
return $data;
}
$this->cache_sums[$key] = $md5sum;
$this->cache[$key] = $data;
}
else {
$this->cache[$key] = null;
}
}
else {
$sql_result = $this->db->limitquery(
- "SELECT cache_id, data, cache_key".
+ "SELECT data, cache_key".
" FROM ".$this->db->table_name('cache').
" WHERE user_id = ?".
" AND cache_key = ?".
// for better performance we allow more records for one key
// get the newer one
" ORDER BY created DESC",
0, 1, $this->userid, $this->prefix.'.'.$key);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
$md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
if ($sql_arr['data']) {
$data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
}
if ($nostore) {
return $data;
}
$this->cache[$key] = $data;
$this->cache_sums[$key] = $md5sum;
- $this->cache_keys[$key] = $sql_arr['cache_id'];
}
else {
$this->cache[$key] = null;
}
}
return $this->cache[$key];
}
/**
* Writes single cache record into DB.
*
* @param string $key Cache key name
* @param mxied $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
private function write_record($key, $data)
{
if (!$this->db) {
return false;
}
if ($this->type == 'memcache' || $this->type == 'apc') {
return $this->add_record($this->ckey($key), $data);
}
- $key_exists = $this->cache_keys[$key];
+ $key_exists = array_key_exists($key, $this->cache_sums);
$key = $this->prefix . '.' . $key;
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache').
" WHERE user_id = ?".
" AND cache_key = ?",
$this->userid, $key);
return true;
}
// update existing cache record
if ($key_exists) {
$result = $this->db->query(
"UPDATE ".$this->db->table_name('cache').
" SET created = ". $this->db->now().", data = ?".
" WHERE user_id = ?".
" AND cache_key = ?",
$data, $this->userid, $key);
}
// add new cache record
else {
// for better performance we allow more records for one key
// so, no need to check if record exist (see rcube_cache::read_record())
$result = $this->db->query(
"INSERT INTO ".$this->db->table_name('cache').
" (created, user_id, cache_key, data)".
" VALUES (".$this->db->now().", ?, ?, ?)",
$this->userid, $key, $data);
}
return $this->db->affected_rows($result);
}
/**
* Deletes the cache record(s).
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*
*/
private function remove_record($key=null, $prefix_mode=false)
{
if (!$this->db) {
return;
}
if ($this->type != 'db') {
$this->load_index();
// Remove all keys
if ($key === null) {
foreach ($this->index as $key) {
$this->delete_record($key, false);
}
$this->index = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach ($this->index as $k) {
if (strpos($k, $key) === 0) {
$this->delete_record($k);
}
}
}
// Remove one key by name
else {
$this->delete_record($key);
}
return;
}
// Remove all keys (in specified cache)
if ($key === null) {
$where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
}
// Remove keys by name prefix
else if ($prefix_mode) {
$where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
}
// Remove one key by name
else {
$where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
}
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache').
" WHERE user_id = ?" . $where,
$this->userid);
}
/**
* Adds entry into memcache/apc DB.
*
* @param string $key Cache key name
* @param mxied $data Serialized cache data
* @param bollean $index Enables immediate index update
*
* @param boolean True on success, False on failure
*/
private function add_record($key, $data, $index=false)
{
if ($this->type == 'memcache') {
$result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
if (!$result)
$result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
}
else if ($this->type == 'apc') {
if (apc_exists($key))
apc_delete($key);
$result = apc_store($key, $data, $this->ttl);
}
// Update index
if ($index && $result) {
$this->load_index();
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
}
return $result;
}
/**
* Deletes entry from memcache/apc DB.
*/
private function delete_record($key, $index=true)
{
if ($this->type == 'memcache')
$this->db->delete($this->ckey($key));
else
apc_delete($this->ckey($key));
if ($index) {
if (($idx = array_search($key, $this->index)) !== false) {
unset($this->index[$idx]);
}
}
}
/**
* Writes the index entry into memcache/apc DB.
*/
private function write_index()
{
if (!$this->db) {
return;
}
if ($this->type == 'db') {
return;
}
$this->load_index();
// Make sure index contains new keys
foreach ($this->cache as $key => $value) {
if ($value !== null) {
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
}
}
}
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
/**
* Gets the index entry from memcache/apc DB.
*/
private function load_index()
{
if (!$this->db) {
return;
}
if ($this->index !== null) {
return;
}
$index_key = $this->ikey();
if ($this->type == 'memcache') {
$data = $this->db->get($index_key);
}
else if ($this->type == 'apc') {
$data = apc_fetch($index_key);
}
$this->index = $data ? unserialize($data) : array();
}
/**
* Creates per-user cache key name (for memcache and apc)
*
* @param string $key Cache key name
*
* @return string Cache key
*/
private function ckey($key)
{
return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
}
/**
* Creates per-user index cache key name (for memcache and apc)
*
* @return string Cache key
*/
private function ikey()
{
// This way each cache will have its own index
return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
}
}
diff --git a/lib/ext/Roundcube/rcube_config.php b/lib/ext/Roundcube/rcube_config.php
index 1b621e0..aae3656 100644
--- a/lib/ext/Roundcube/rcube_config.php
+++ b/lib/ext/Roundcube/rcube_config.php
@@ -1,400 +1,412 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_config.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to read configuration settings |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Configuration class for Roundcube
*
* @package Core
*/
class rcube_config
{
private $prop = array();
private $errors = array();
private $userprefs = array();
/**
* Renamed options
*
* @var array
*/
private $legacy_props = array(
// new name => old name
'default_folders' => 'default_imap_folders',
'mail_pagesize' => 'pagesize',
'addressbook_pagesize' => 'pagesize',
);
/**
* Object constructor
*/
public function __construct()
{
$this->load();
+
+ // Defaults, that we do not require you to configure,
+ // but contain information that is used in various
+ // locations in the code:
+ $this->set('contactlist_fields', array('name', 'firstname', 'surname', 'email'));
}
/**
* Load config from local config file
*
* @todo Remove global $CONFIG
*/
private function load()
{
// load main config file
if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/main.inc.php'))
$this->errors[] = 'main.inc.php was not found.';
// load database config
if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/db.inc.php'))
$this->errors[] = 'db.inc.php was not found.';
// load host-specific configuration
$this->load_host_config();
// set skin (with fallback to old 'skin_path' property)
if (empty($this->prop['skin'])) {
if (!empty($this->prop['skin_path'])) {
$this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
}
else {
- $this->prop['skin'] = 'default';
+ $this->prop['skin'] = 'larry';
}
}
+ // larry is the new default skin :-)
+ if ($this->prop['skin'] == 'default')
+ $this->prop['skin'] = 'larry';
+
// fix paths
$this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : INSTALL_PATH . 'logs';
$this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : INSTALL_PATH . 'temp';
// fix default imap folders encoding
foreach (array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox') as $folder)
$this->prop[$folder] = rcube_charset::convert($this->prop[$folder], RCMAIL_CHARSET, 'UTF7-IMAP');
if (!empty($this->prop['default_folders']))
foreach ($this->prop['default_folders'] as $n => $folder)
$this->prop['default_folders'][$n] = rcube_charset::convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
// set PHP error logging according to config
if ($this->prop['debug_level'] & 1) {
ini_set('log_errors', 1);
if ($this->prop['log_driver'] == 'syslog') {
ini_set('error_log', 'syslog');
}
else {
ini_set('error_log', $this->prop['log_dir'].'/errors');
}
}
// 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'])) {
$this->prop['timezone'] = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0);
}
// remove deprecated properties
unset($this->prop['dst_active']);
// export config data
$GLOBALS['CONFIG'] = &$this->prop;
}
/**
* Load a host-specific config file if configured
* This will merge the host specific configuration with the given one
*/
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 ($fname) {
$this->load_from_file(RCMAIL_CONFIG_DIR . '/' . $fname);
}
}
/**
* Read configuration from a file
* and merge with the already stored config values
*
* @param string $fpath Full path to the config file to be loaded
* @return booelan True on success, false on failure
*/
public function load_from_file($fpath)
{
if (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($rcmail_config)) {
$this->prop = array_merge($this->prop, $rcmail_config, $this->userprefs);
return true;
}
}
return false;
}
/**
* Getter for a specific config parameter
*
* @param string $name Parameter name
* @param mixed $def Default value if not set
* @return mixed The requested config value
*/
public function get($name, $def = null)
{
if (isset($this->prop[$name])) {
$result = $this->prop[$name];
}
else if (isset($this->legacy_props[$name])) {
return $this->get($this->legacy_props[$name], $def);
}
else {
$result = $def;
}
$rcube = rcube::get_instance();
if ($name == 'timezone' && isset($this->prop['_timezone_value']))
$result = $this->prop['_timezone_value'];
$plugin = $rcube->plugins->exec_hook('config_get', array(
'name' => $name, 'default' => $def, 'result' => $result));
return $plugin['result'];
}
/**
* Setter for a config parameter
*
* @param string $name Parameter name
* @param mixed $value Parameter value
*/
public function set($name, $value)
{
$this->prop[$name] = $value;
}
/**
* Override config options with the given values (eg. user prefs)
*
* @param array $prefs Hash array with config props to merge over
*/
public function merge($prefs)
{
$this->prop = array_merge($this->prop, $prefs, $this->userprefs);
}
/**
* Merge the given prefs over the current config
* and make sure that they survive further merging.
*
* @param array $prefs Hash array with user prefs
*/
public function set_user_prefs($prefs)
{
// Honor the dont_override setting for any existing user preferences
$dont_override = $this->get('dont_override');
if (is_array($dont_override) && !empty($dont_override)) {
- foreach ($prefs as $key => $pref) {
- if (in_array($key, $dont_override)) {
- unset($prefs[$key]);
- }
+ foreach ($dont_override as $key) {
+ unset($prefs[$key]);
}
}
// convert user's timezone into the new format
if (is_numeric($prefs['timezone'])) {
$prefs['timezone'] = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0);
}
+ // larry is the new default skin :-)
+ if ($prefs['skin'] == 'default') {
+ $prefs['skin'] = 'larry';
+ }
+
$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']);
}
/**
* Getter for all config options
*
* @return array Hash array containg all config properties
*/
public function all()
{
return $this->prop;
}
/**
* Special getter for user's timezone offset including DST
*
* @return float Timezone offset (in hours)
* @deprecated
*/
public function get_timezone()
{
if ($tz = $this->get('timezone')) {
try {
$tz = new DateTimeZone($tz);
return $tz->getOffset(new DateTime('now')) / 3600;
}
catch (Exception $e) {
}
}
return 0;
}
/**
* Return requested DES crypto key.
*
* @param string $key Crypto key name
* @return string Crypto key
*/
public function get_crypto_key($key)
{
// Bomb out if the requested key does not exist
if (!array_key_exists($key, $this->prop)) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Request for unconfigured crypto key \"$key\""
), true, true);
}
$key = $this->prop[$key];
// Bomb out if the configured key is not exactly 24 bytes long
if (strlen($key) != 24) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Configured crypto key '$key' is not exactly 24 bytes long"
), true, true);
}
return $key;
}
/**
* Try to autodetect operating system and find the correct line endings
*
* @return string The appropriate mail header delimiter
*/
public function header_delimiter()
{
// use the configured delimiter for headers
if (!empty($this->prop['mail_header_delimiter'])) {
$delim = $this->prop['mail_header_delimiter'];
if ($delim == "\n" || $delim == "\r\n")
return $delim;
else
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid mail_header_delimiter setting"
), true, false);
}
$php_os = strtolower(substr(PHP_OS, 0, 3));
if ($php_os == 'win')
return "\r\n";
if ($php_os == 'mac')
return "\r\n";
return "\n";
}
/**
* Return the mail domain configured for the given host
*
* @param string $host IMAP host
* @param boolean $encode If true, domain name will be converted to IDN ASCII
* @return string Resolved SMTP host
*/
public function mail_domain($host, $encode=true)
{
$domain = $host;
if (is_array($this->prop['mail_domain'])) {
if (isset($this->prop['mail_domain'][$host]))
$domain = $this->prop['mail_domain'][$host];
}
else if (!empty($this->prop['mail_domain'])) {
$domain = rcube_utils::parse_host($this->prop['mail_domain']);
}
if ($encode) {
$domain = rcube_utils::idn_to_ascii($domain);
}
return $domain;
}
/**
* Getter for error state
*
* @return mixed Error message on error, False if no errors
*/
public function get_error()
{
return empty($this->errors) ? false : join("\n", $this->errors);
}
/**
* Internal getter for client's (browser) timezone identifier
*/
private function client_timezone()
{
return isset($_SESSION['timezone']) ? timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0) : date_default_timezone_get();
}
}
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index f7fc651..534a65c 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -1,1000 +1,1001 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_contacts.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to the local address book database |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class for the local address book database
*
* @package Addressbook
*/
class rcube_contacts extends rcube_addressbook
{
// protected for backward compat. with some plugins
protected $db_name = 'contacts';
protected $db_groups = 'contactgroups';
protected $db_groupmembers = 'contactgroupmembers';
protected $vcard_fieldmap = array();
/**
* Store database connection.
*
- * @var rcube_mdb2
+ * @var rcube_db
*/
private $db = null;
private $user_id = 0;
private $filter = null;
private $result = null;
private $cache;
private $table_cols = array('name', 'email', 'firstname', 'surname');
private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname',
'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone',
'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes');
// public properties
public $primary_key = 'contact_id';
public $name;
public $readonly = false;
public $groups = true;
public $undelete = true;
public $list_page = 1;
public $page_size = 10;
public $group_id = 0;
public $ready = false;
public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
'jobtitle', 'organization', 'department', 'assistant', 'manager',
'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
const SEPARATOR = ',';
/**
* Object constructor
*
* @param object Instance of the rcube_db class
* @param integer User-ID
*/
function __construct($dbconn, $user)
{
$this->db = $dbconn;
$this->user_id = $user;
$this->ready = $this->db && !$this->db->is_error();
}
/**
* Returns addressbook name
*/
function get_name()
{
return $this->name;
}
/**
* Save a search string for future listings
*
* @param string SQL params to use in listing method
*/
function set_search_set($filter)
{
$this->filter = $filter;
$this->cache = null;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
function get_search_set()
{
return $this->filter;
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($gid)
{
$this->group_id = $gid;
$this->cache = null;
}
/**
* Reset all saved results and search parameters
*/
function reset()
{
$this->result = null;
$this->filter = null;
$this->cache = null;
}
/**
* List all active contact groups of this source
*
* @param string Search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
$results = array();
if (!$this->groups)
return $results;
$sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : '';
$sql_result = $this->db->query(
"SELECT * FROM ".$this->db->table_name($this->db_groups).
" WHERE del<>1".
" AND user_id=?".
$sql_filter.
" ORDER BY name",
$this->user_id);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr['contactgroup_id'];
$results[] = $sql_arr;
}
return $results;
}
/**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
* @return array Group properties as hash array
*/
function get_group($group_id)
{
$sql_result = $this->db->query(
"SELECT * FROM ".$this->db->table_name($this->db_groups).
" WHERE del<>1".
" AND contactgroup_id=?".
" AND user_id=?",
$group_id, $this->user_id);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr['contactgroup_id'];
return $sql_arr;
}
return null;
}
/**
* List the current set of contact records
*
* @param array List of cols to show, Null means all
* @param int Only return this number of records, use negative values for tail
* @param boolean True to skip the count query (select only)
* @return array Indexed list of contact records, each a hash array
*/
function list_records($cols=null, $subset=0, $nocount=false)
{
if ($nocount || $this->list_page <= 1) {
// create dummy result, we don't need a count now
$this->result = new rcube_result_set();
} else {
// count all records
$this->result = $this->count();
}
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$length = $subset != 0 ? abs($subset) : $this->page_size;
if ($this->group_id)
$join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m".
" ON (m.contact_id = c.".$this->primary_key.")";
$order_col = (in_array($this->sort_col, $this->table_cols) ? $this->sort_col : 'name');
$order_cols = array('c.'.$order_col);
if ($order_col == 'firstname')
$order_cols[] = 'c.surname';
else if ($order_col == 'surname')
$order_cols[] = 'c.firstname';
if ($order_col != 'name')
$order_cols[] = 'c.name';
$order_cols[] = 'c.email';
$sql_result = $this->db->limitquery(
"SELECT * FROM ".$this->db->table_name($this->db_name)." AS c" .
$join .
" WHERE c.del<>1" .
" AND c.user_id=?" .
($this->group_id ? " AND m.contactgroup_id=?" : "").
($this->filter ? " AND (".$this->filter.")" : "") .
" ORDER BY ". $this->db->concat($order_cols) .
" " . $this->sort_order,
$start_row,
$length,
$this->user_id,
$this->group_id);
// determine whether we have to parse the vcard or if only db cols are requested
$read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr[$this->primary_key];
if ($read_vcard)
$sql_arr = $this->convert_db_data($sql_arr);
else {
$sql_arr['email'] = explode(self::SEPARATOR, $sql_arr['email']);
$sql_arr['email'] = array_map('trim', $sql_arr['email']);
}
$this->result->add($sql_arr);
}
$cnt = count($this->result->records);
// update counter
if ($nocount)
$this->result->count = $cnt;
else if ($this->list_page <= 1) {
if ($cnt < $this->page_size && $subset == 0)
$this->result->count = $cnt;
else if (isset($this->cache['count']))
$this->result->count = $this->cache['count'];
else
$this->result->count = $this->_count();
}
return $this->result;
}
/**
* Search contacts
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set Contact records and 'count' value
*/
function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
$where = $and_where = array();
$mode = intval($mode);
$WS = ' ';
$AS = self::SEPARATOR;
foreach ($fields as $idx => $col) {
// direct ID search
if ($col == 'ID' || $col == $this->primary_key) {
$ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value;
$ids = $this->db->array2list($ids, 'integer');
$where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
continue;
}
// fulltext search in all fields
else if ($col == '*') {
$words = array();
foreach (explode($WS, rcube_utils::normalize_string($value)) as $word) {
switch ($mode) {
case 1: // strict
$words[] = '(' . $this->db->ilike('words', $word . '%')
. ' OR ' . $this->db->ilike('words', '%' . $WS . $word . $WS . '%')
. ' OR ' . $this->db->ilike('words', '%' . $WS . $word) . ')';
break;
case 2: // prefix
$words[] = '(' . $this->db->ilike('words', $word . '%')
. ' OR ' . $this->db->ilike('words', '%' . $WS . $word . '%') . ')';
break;
default: // partial
$words[] = $this->db->ilike('words', '%' . $word . '%');
}
}
$where[] = '(' . join(' AND ', $words) . ')';
}
else {
$val = is_array($value) ? $value[$idx] : $value;
// table column
if (in_array($col, $this->table_cols)) {
switch ($mode) {
case 1: // strict
$where[] = '(' . $this->db->quoteIdentifier($col) . ' = ' . $this->db->quote($val)
. ' OR ' . $this->db->ilike($col, $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')';
break;
case 2: // prefix
$where[] = '(' . $this->db->ilike($col, $val . '%')
. ' OR ' . $this->db->ilike($col, $AS . $val . '%') . ')';
break;
default: // partial
$where[] = $this->db->ilike($col, '%' . $val . '%');
}
}
// vCard field
else {
if (in_array($col, $this->fulltext_cols)) {
foreach (rcube_utils::normalize_string($val, true) as $word) {
switch ($mode) {
case 1: // strict
$words[] = '(' . $this->db->ilike('words', $word . $WS . '%')
. ' OR ' . $this->db->ilike('words', '%' . $AS . $word . $WS .'%')
. ' OR ' . $this->db->ilike('words', '%' . $AS . $word) . ')';
break;
case 2: // prefix
$words[] = '(' . $this->db->ilike('words', $word . '%')
. ' OR ' . $this->db->ilike('words', $AS . $word . '%') . ')';
break;
default: // partial
$words[] = $this->db->ilike('words', '%' . $word . '%');
}
}
$where[] = '(' . join(' AND ', $words) . ')';
}
if (is_array($value))
$post_search[$col] = mb_strtolower($val);
}
}
}
foreach (array_intersect($required, $this->table_cols) as $col) {
$and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote('');
}
if (!empty($where)) {
// use AND operator for advanced searches
$where = join(is_array($value) ? ' AND ' : ' OR ', $where);
}
if (!empty($and_where))
$where = ($where ? "($where) AND " : '') . join(' AND ', $and_where);
// Post-searching in vCard data fields
// we will search in all records and then build a where clause for their IDs
if (!empty($post_search)) {
$ids = array(0);
// build key name regexp
$regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/';
// use initial WHERE clause, to limit records number if possible
if (!empty($where))
$this->set_search_set($where);
// count result pages
$cnt = $this->count();
$pages = ceil($cnt / $this->page_size);
$scnt = count($post_search);
// get (paged) result
for ($i=0; $i<$pages; $i++) {
$this->list_records(null, $i, true);
while ($row = $this->result->next()) {
$id = $row[$this->primary_key];
$found = array();
foreach (preg_grep($regexp, array_keys($row)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $post_search[$colname];
foreach ((array)$row[$col] as $value) {
// composite field, e.g. address
foreach ((array)$value as $val) {
$val = mb_strtolower($val);
switch ($mode) {
case 1:
$got = ($val == $search);
break;
case 2:
$got = ($search == substr($val, 0, strlen($search)));
break;
default:
$got = (strpos($val, $search) !== false);
break;
}
if ($got) {
$found[$colname] = true;
break 2;
}
}
}
}
// all fields match
if (count($found) >= $scnt) {
$ids[] = $id;
}
}
}
// build WHERE clause
$ids = $this->db->array2list($ids, 'integer');
$where = 'c.' . $this->primary_key.' IN ('.$ids.')';
// reset counter
unset($this->cache['count']);
// when we know we have an empty result
if ($ids == '0') {
$this->set_search_set($where);
return ($this->result = new rcube_result_set(0, 0));
}
}
if (!empty($where)) {
$this->set_search_set($where);
if ($select)
$this->list_records(null, 0, $nocount);
else
$this->result = $this->count();
}
return $this->result;
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result object
*/
function count()
{
$count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Count number of available contacts in database
*
* @return int Contacts count
*/
private function _count()
{
if ($this->group_id)
$join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m".
" ON (m.contact_id=c.".$this->primary_key.")";
// count contacts for this user
$sql_result = $this->db->query(
"SELECT COUNT(c.contact_id) AS rows".
" FROM ".$this->db->table_name($this->db_name)." AS c".
$join.
" WHERE c.del<>1".
" AND c.user_id=?".
($this->group_id ? " AND m.contactgroup_id=?" : "").
($this->filter ? " AND (".$this->filter.")" : ""),
$this->user_id,
$this->group_id
);
$sql_arr = $this->db->fetch_assoc($sql_result);
$this->cache['count'] = (int) $sql_arr['rows'];
return $this->cache['count'];
}
/**
* Return the last result set
*
* @return mixed Result array or NULL if nothing selected yet
*/
function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @return mixed Result object with all record fields or False if not found
*/
function get_record($id, $assoc=false)
{
// return cached result
if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id)
return $assoc ? $first : $this->result;
$this->db->query(
"SELECT * FROM ".$this->db->table_name($this->db_name).
" WHERE contact_id=?".
" AND user_id=?".
" AND del<>1",
$id,
$this->user_id
);
if ($sql_arr = $this->db->fetch_assoc()) {
$record = $this->convert_db_data($sql_arr);
$this->result = new rcube_result_set(1);
$this->result->add($record);
}
return $assoc && $record ? $record : $this->result;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
function get_record_groups($id)
{
$results = array();
if (!$this->groups)
return $results;
$sql_result = $this->db->query(
"SELECT cgm.contactgroup_id, cg.name FROM " . $this->db->table_name($this->db_groupmembers) . " AS cgm" .
" LEFT JOIN " . $this->db->table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" .
" WHERE cgm.contact_id=?",
$id
);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$results[$sql_arr['contactgroup_id']] = $sql_arr['name'];
}
return $results;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Assoziative array with data to save
* @param boolean Try to fix/complete record automatically
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// 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))) {
$this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
$valid = false;
}
return $valid;
}
/**
* Create a new contact record
*
* @param array Associative array with save data
* @return integer|boolean The created record ID on success, False on error
*/
function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
if ($check) {
foreach ($save_data as $col => $values) {
if (strpos($col, 'email') === 0) {
foreach ((array)$values as $email) {
if ($existing = $this->search('email', $email, false, false))
break 2;
}
}
}
}
$save_data = $this->convert_save_data($save_data);
$a_insert_cols = $a_insert_values = array();
foreach ($save_data as $col => $value) {
$a_insert_cols[] = $this->db->quoteIdentifier($col);
$a_insert_values[] = $this->db->quote($value);
}
if (!$existing->count && !empty($a_insert_cols)) {
$this->db->query(
"INSERT INTO ".$this->db->table_name($this->db_name).
" (user_id, changed, del, ".join(', ', $a_insert_cols).")".
" VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
);
$insert_id = $this->db->insert_id($this->db_name);
}
// also add the newly created contact to the active group
if ($insert_id && $this->group_id)
$this->add_to_group($this->group_id, $insert_id);
$this->cache = null;
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
$updated = false;
$write_sql = array();
$record = $this->get_record($id, true);
$save_cols = $this->convert_save_data($save_cols, $record);
foreach ($save_cols as $col => $value) {
$write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
}
if (!empty($write_sql)) {
$this->db->query(
"UPDATE ".$this->db->table_name($this->db_name).
" SET changed=".$this->db->now().", ".join(', ', $write_sql).
" WHERE contact_id=?".
" AND user_id=?".
" AND del<>1",
$id,
$this->user_id
);
$updated = $this->db->affected_rows();
$this->result = null; // clear current result (from get_record())
}
return $updated;
}
private function convert_db_data($sql_arr)
{
$record = array();
$record['ID'] = $sql_arr[$this->primary_key];
if ($sql_arr['vcard']) {
unset($sql_arr['email']);
$vcard = new rcube_vcard($sql_arr['vcard'], RCMAIL_CHARSET, false, $this->vcard_fieldmap);
$record += $vcard->get_assoc() + $sql_arr;
}
else {
$record += $sql_arr;
$record['email'] = explode(self::SEPARATOR, $record['email']);
$record['email'] = array_map('trim', $record['email']);
}
return $record;
}
private function convert_save_data($save_data, $record = array())
{
$out = array();
$words = '';
// copy values into vcard object
$vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard'], RCMAIL_CHARSET, false, $this->vcard_fieldmap);
$vcard->reset();
foreach ($save_data as $key => $values) {
list($field, $section) = explode(':', $key);
$fulltext = in_array($field, $this->fulltext_cols);
foreach ((array)$values as $value) {
if (isset($value))
$vcard->set($field, $value, $section);
if ($fulltext && is_array($value))
$words .= ' ' . rcube_utils::normalize_string(join(" ", $value));
else if ($fulltext && strlen($value) >= 3)
$words .= ' ' . rcube_utils::normalize_string($value);
}
}
$out['vcard'] = $vcard->export(false);
foreach ($this->table_cols as $col) {
$key = $col;
if (!isset($save_data[$key]))
$key .= ':home';
if (isset($save_data[$key])) {
if (is_array($save_data[$key]))
$out[$col] = join(self::SEPARATOR, $save_data[$key]);
else
$out[$col] = $save_data[$key];
}
}
// save all e-mails in database column
$out['email'] = join(self::SEPARATOR, $vcard->email);
// join words for fulltext search
$out['words'] = join(" ", array_unique(explode(" ", $words)));
return $out;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (unsupported)
*/
function delete($ids, $force=true)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
// flag record as deleted (always)
$this->db->query(
"UPDATE ".$this->db->table_name($this->db_name).
" SET del=1, changed=".$this->db->now().
" WHERE user_id=?".
" AND contact_id IN ($ids)",
$this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Undelete one or more contact records
*
* @param array Record identifiers
*/
function undelete($ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
// clear deleted flag
$this->db->query(
"UPDATE ".$this->db->table_name($this->db_name).
" SET del=0, changed=".$this->db->now().
" WHERE user_id=?".
" AND contact_id IN ($ids)",
$this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Remove all records from the database
*/
function delete_all()
{
$this->cache = null;
$this->db->query("UPDATE ".$this->db->table_name($this->db_name).
" SET del=1, changed=".$this->db->now().
" WHERE user_id = ?", $this->user_id);
return $this->db->affected_rows();
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$result = false;
// make sure we have a unique name
$name = $this->unique_groupname($name);
$this->db->query(
"INSERT INTO ".$this->db->table_name($this->db_groups).
" (user_id, changed, name)".
" VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
);
if ($insert_id = $this->db->insert_id($this->db_groups))
$result = array('id' => $insert_id, 'name' => $name);
return $result;
}
/**
* Delete the given group (and all linked group members)
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
// flag group record as deleted
$sql_result = $this->db->query(
"UPDATE ".$this->db->table_name($this->db_groups).
" SET del=1, changed=".$this->db->now().
" WHERE contactgroup_id=?".
" AND user_id=?",
$gid, $this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname, &$new_gid)
{
// make sure we have a unique name
$name = $this->unique_groupname($newname);
$sql_result = $this->db->query(
"UPDATE ".$this->db->table_name($this->db_groups).
" SET name=?, changed=".$this->db->now().
" WHERE contactgroup_id=?".
" AND user_id=?",
$name, $gid, $this->user_id
);
return $this->db->affected_rows() ? $name : false;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($group_id, $ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$added = 0;
$exists = array();
// get existing assignments ...
$sql_result = $this->db->query(
"SELECT contact_id FROM ".$this->db->table_name($this->db_groupmembers).
" WHERE contactgroup_id=?".
" AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
$group_id
);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$exists[] = $sql_arr['contact_id'];
}
// ... and remove them from the list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
$this->db->query(
"INSERT INTO ".$this->db->table_name($this->db_groupmembers).
" (contactgroup_id, contact_id, created)".
" VALUES (?, ?, ".$this->db->now().")",
$group_id,
$contact_id
);
- if ($this->db->db_error)
- $this->set_error(self::ERROR_SAVING, $this->db->db_error_msg);
+ if ($error = $this->db->is_error())
+ $this->set_error(self::ERROR_SAVING, $error);
else
$added++;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
$sql_result = $this->db->query(
"DELETE FROM ".$this->db->table_name($this->db_groupmembers).
" WHERE contactgroup_id=?".
" AND contact_id IN ($ids)",
$group_id
);
return $this->db->affected_rows();
}
/**
* Check for existing groups with the same name
*
* @param string Name to check
* @return string A group name which is unique for the current use
*/
private function unique_groupname($name)
{
$checkname = $name;
$num = 2; $hit = false;
do {
$sql_result = $this->db->query(
"SELECT 1 FROM ".$this->db->table_name($this->db_groups).
" WHERE del<>1".
" AND user_id=?".
" AND name=?",
$this->user_id,
$checkname);
// append number to make name unique
- if ($hit = $this->db->num_rows($sql_result))
+ if ($hit = $this->db->fetch_array($sql_result)) {
$checkname = $name . ' ' . $num++;
- } while ($hit > 0);
+ }
+ } while ($hit);
return $checkname;
}
}
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
new file mode 100644
index 0000000..042ca15
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -0,0 +1,998 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_db.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Database wrapper class that implements PHP PDO functions |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Database
+ * @version 1.0
+ */
+class rcube_db
+{
+ protected $db_dsnw; // DSN for write operations
+ protected $db_dsnr; // DSN for read operations
+ protected $db_connected = false; // Already connected ?
+ protected $db_mode; // Connection mode
+ protected $dbh; // Connection handle
+
+ protected $db_error = false;
+ protected $db_error_msg = '';
+ protected $conn_failure = false;
+ protected $a_query_results = array('dummy');
+ protected $last_res_id = 0;
+ protected $db_index = 0;
+ protected $tables;
+ protected $variables;
+
+ protected $options = array(
+ // column/table quotes
+ 'identifier_start' => '"',
+ 'identifier_end' => '"',
+ );
+
+
+ /**
+ * Factory, returns driver-specific instance of the class
+ *
+ * @param string $db_dsnw DSN for read/write operations
+ * @param string $db_dsnr Optional DSN for read only operations
+ * @param bool $pconn Enables persistent connections
+ *
+ * @return rcube_db Object instance
+ */
+ public static function factory($db_dsnw, $db_dsnr = '', $pconn = false)
+ {
+ $driver = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':')));
+ $driver_map = array(
+ 'sqlite2' => 'sqlite',
+ 'sybase' => 'mssql',
+ 'dblib' => 'mssql',
+ 'mysqli' => 'mysql',
+ );
+
+ $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver;
+ $class = "rcube_db_$driver";
+
+ if (!class_exists($class)) {
+ rcube::raise_error(array('code' => 600, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => "Configuration error. Unsupported database driver: $driver"),
+ true, true);
+ }
+
+ return new $class($db_dsnw, $db_dsnr, $pconn);
+ }
+
+ /**
+ * Object constructor
+ *
+ * @param string $db_dsnw DSN for read/write operations
+ * @param string $db_dsnr Optional DSN for read only operations
+ * @param bool $pconn Enables persistent connections
+ */
+ public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
+ {
+ if (empty($db_dsnr)) {
+ $db_dsnr = $db_dsnw;
+ }
+
+ $this->db_dsnw = $db_dsnw;
+ $this->db_dsnr = $db_dsnr;
+ $this->db_pconn = $pconn;
+
+ $this->db_dsnw_array = self::parse_dsn($db_dsnw);
+ $this->db_dsnr_array = self::parse_dsn($db_dsnr);
+
+ // Initialize driver class
+ $this->init();
+ }
+
+ /**
+ * Initialization of the object with driver specific code
+ */
+ protected function init()
+ {
+ // To be used by driver classes
+ }
+
+ /**
+ * Connect to specific database
+ *
+ * @param array $dsn DSN for DB connections
+ *
+ * @return PDO database handle
+ */
+ protected function dsn_connect($dsn)
+ {
+ $this->db_error = false;
+ $this->db_error_msg = null;
+
+ // Get database specific connection options
+ $dsn_string = $this->dsn_string($dsn);
+ $dsn_options = $this->dsn_options($dsn);
+
+ if ($db_pconn) {
+ $dsn_options[PDO::ATTR_PERSISTENT] = true;
+ }
+
+ // Connect
+ try {
+ // with this check we skip fatal error on PDO object creation
+ if (!class_exists('PDO', false)) {
+ throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php');
+ }
+
+ $this->conn_prepare($dsn);
+
+ $dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options);
+
+ // don't throw exceptions or warnings
+ $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
+ }
+ catch (Exception $e) {
+ $this->db_error = true;
+ $this->db_error_msg = $e->getMessage();
+
+ rcube::raise_error(array('code' => 500, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => $this->db_error_msg), true, false);
+
+ return null;
+ }
+
+ $this->conn_configure($dsn, $dbh);
+
+ return $dbh;
+ }
+
+ /**
+ * Driver-specific preparation of database connection
+ *
+ * @param array $dsn DSN for DB connections
+ */
+ protected function conn_prepare($dsn)
+ {
+ }
+
+ /**
+ * Driver-specific configuration of database connection
+ *
+ * @param array $dsn DSN for DB connections
+ * @param PDO $dbh Connection handler
+ */
+ protected function conn_configure($dsn, $dbh)
+ {
+ }
+
+ /**
+ * Driver-specific database character set setting
+ *
+ * @param string $charset Character set name
+ */
+ protected function set_charset($charset)
+ {
+ $this->query("SET NAMES 'utf8'");
+ }
+
+ /**
+ * Connect to appropriate database depending on the operation
+ *
+ * @param string $mode Connection mode (r|w)
+ */
+ public function db_connect($mode)
+ {
+ // previous connection failed, don't attempt to connect again
+ if ($this->conn_failure) {
+ return;
+ }
+
+ // no replication
+ if ($this->db_dsnw == $this->db_dsnr) {
+ $mode = 'w';
+ }
+
+ // Already connected
+ if ($this->db_connected) {
+ // connected to db with the same or "higher" mode
+ if ($this->db_mode == 'w' || $this->db_mode == $mode) {
+ return;
+ }
+ }
+
+ $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
+
+ $this->dbh = $this->dsn_connect($dsn);
+ $this->db_connected = is_object($this->dbh);
+
+ // use write-master when read-only fails
+ if (!$this->db_connected && $mode == 'r') {
+ $mode = 'w';
+ $this->dbh = $this->dsn_connect($this->db_dsnw_array);
+ $this->db_connected = is_object($this->dbh);
+ }
+
+ if ($this->db_connected) {
+ $this->db_mode = $mode;
+ $this->set_charset('utf8');
+ }
+ else {
+ $this->conn_failure = true;
+ }
+ }
+
+ /**
+ * Activate/deactivate debug mode
+ *
+ * @param boolean $dbg True if SQL queries should be logged
+ */
+ public function set_debug($dbg = true)
+ {
+ $this->options['debug_mode'] = $dbg;
+ }
+
+ /**
+ * Writes debug information/query to 'sql' log file
+ *
+ * @param string $query SQL query
+ */
+ protected function debug($query)
+ {
+ if ($this->options['debug_mode']) {
+ rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
+ }
+ }
+
+ /**
+ * Getter for error state
+ *
+ * @param int $res_id Optional query result identifier
+ *
+ * @return string Error message
+ */
+ public function is_error($res_id = null)
+ {
+ if ($res_id !== null) {
+ return $this->_get_result($res_id) === false ? $this->db_error_msg : null;
+ }
+
+ return $this->db_error ? $this->db_error_msg : null;
+ }
+
+ /**
+ * Connection state checker
+ *
+ * @return boolean True if in connected state
+ */
+ public function is_connected()
+ {
+ return !is_object($this->dbh) ? false : $this->db_connected;
+ }
+
+ /**
+ * Is database replication configured?
+ *
+ * @return bool Returns true if dsnw != dsnr
+ */
+ public function is_replicated()
+ {
+ return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
+ }
+
+ /**
+ * Get database runtime variables
+ *
+ * @param string $varname Variable name
+ * @param mixed $default Default value if variable is not set
+ *
+ * @return mixed Variable value or default
+ */
+ public function get_variable($varname, $default = null)
+ {
+ // to be implemented by driver class
+ return $default;
+ }
+
+ /**
+ * Execute a SQL query
+ *
+ * @param string SQL query to execute
+ * @param mixed Values to be inserted in query
+ *
+ * @return number Query handle identifier
+ */
+ public function query()
+ {
+ $params = func_get_args();
+ $query = array_shift($params);
+
+ // Support one argument of type array, instead of n arguments
+ if (count($params) == 1 && is_array($params[0])) {
+ $params = $params[0];
+ }
+
+ return $this->_query($query, 0, 0, $params);
+ }
+
+ /**
+ * Execute a SQL query with limits
+ *
+ * @param string SQL query to execute
+ * @param int Offset for LIMIT statement
+ * @param int Number of rows for LIMIT statement
+ * @param mixed Values to be inserted in query
+ *
+ * @return int Query handle identifier
+ */
+ public function limitquery()
+ {
+ $params = func_get_args();
+ $query = array_shift($params);
+ $offset = array_shift($params);
+ $numrows = array_shift($params);
+
+ return $this->_query($query, $offset, $numrows, $params);
+ }
+
+ /**
+ * Execute a SQL query with limits
+ *
+ * @param string $query SQL query to execute
+ * @param int $offset Offset for LIMIT statement
+ * @param int $numrows Number of rows for LIMIT statement
+ * @param array $params Values to be inserted in query
+ *
+ * @return int Query handle identifier
+ */
+ protected function _query($query, $offset, $numrows, $params)
+ {
+ // Read or write ?
+ $mode = preg_match('/^(select|show)/i', ltrim($query)) ? 'r' : 'w';
+
+ $this->db_connect($mode);
+
+ // check connection before proceeding
+ if (!$this->is_connected()) {
+ return null;
+ }
+
+ if ($numrows || $offset) {
+ $query = $this->set_limit($query, $numrows, $offset);
+ }
+
+ $params = (array) $params;
+
+ // Because in Roundcube we mostly use queries that are
+ // executed only once, we will not use prepared queries
+ $pos = 0;
+ $idx = 0;
+
+ while ($pos = strpos($query, '?', $pos)) {
+ $val = $this->quote($params[$idx++]);
+ unset($params[$idx-1]);
+ $query = substr_replace($query, $val, $pos, 1);
+ $pos += strlen($val);
+ }
+
+ $query = rtrim($query, ';');
+
+ $this->debug($query);
+
+ $query = $this->dbh->query($query);
+
+ if ($query === false) {
+ $error = $this->dbh->errorInfo();
+ $this->db_error = true;
+ $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
+
+ rcube::raise_error(array('code' => 500, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => $this->db_error_msg), true, false);
+ }
+
+ // add result, even if it's an error
+ return $this->_add_result($query);
+ }
+
+ /**
+ * Get number of affected rows for the last query
+ *
+ * @param number $res_id Optional query handle identifier
+ *
+ * @return int Number of rows or false on failure
+ */
+ public function affected_rows($res_id = null)
+ {
+ if ($result = $this->_get_result($res_id)) {
+ return $result->rowCount();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Get last inserted record ID
+ *
+ * @param string $table Table name (to find the incremented sequence)
+ *
+ * @return mixed ID or false on failure
+ */
+ public function insert_id($table = '')
+ {
+ if (!$this->db_connected || $this->db_mode == 'r') {
+ return false;
+ }
+
+ if ($table) {
+ // resolve table name
+ $table = $this->table_name($table);
+ }
+
+ $id = $this->dbh->lastInsertId($table);
+
+ return $id;
+ }
+
+ /**
+ * Get an associative array for one row
+ * If no query handle is specified, the last query will be taken as reference
+ *
+ * @param int $res_id Optional query handle identifier
+ *
+ * @return mixed Array with col values or false on failure
+ */
+ public function fetch_assoc($res_id = null)
+ {
+ $result = $this->_get_result($res_id);
+ return $this->_fetch_row($result, PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Get an index array for one row
+ * If no query handle is specified, the last query will be taken as reference
+ *
+ * @param int $res_id Optional query handle identifier
+ *
+ * @return mixed Array with col values or false on failure
+ */
+ public function fetch_array($res_id = null)
+ {
+ $result = $this->_get_result($res_id);
+ return $this->_fetch_row($result, PDO::FETCH_NUM);
+ }
+
+ /**
+ * Get col values for a result row
+ *
+ * @param PDOStatement $result Result handle
+ * @param int $mode Fetch mode identifier
+ *
+ * @return mixed Array with col values or false on failure
+ */
+ protected function _fetch_row($result, $mode)
+ {
+ if (!is_object($result) || !$this->is_connected()) {
+ return false;
+ }
+
+ return $result->fetch($mode);
+ }
+
+ /**
+ * Adds LIMIT,OFFSET clauses to the query
+ *
+ * @param string $query SQL query
+ * @param int $limit Number of rows
+ * @param int $offset Offset
+ *
+ * @return string SQL query
+ */
+ protected function set_limit($query, $limit = 0, $offset = 0)
+ {
+ if ($limit) {
+ $query .= ' LIMIT ' . intval($limit);
+ }
+
+ if ($offset) {
+ $query .= ' OFFSET ' . intval($offset);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Returns list of tables in a database
+ *
+ * @return array List of all tables of the current database
+ */
+ public function list_tables()
+ {
+ // get tables if not cached
+ if ($this->tables === null) {
+ $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME');
+
+ if ($res = $this->_get_result($q)) {
+ $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+ else {
+ $this->tables = array();
+ }
+ }
+
+ return $this->tables;
+ }
+
+ /**
+ * Returns list of columns in database table
+ *
+ * @param string $table Table name
+ *
+ * @return array List of table cols
+ */
+ public function list_cols($table)
+ {
+ $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?',
+ array($table));
+
+ if ($res = $this->_get_result($q)) {
+ return $res->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+
+ return array();
+ }
+
+ /**
+ * Formats input so it can be safely used in a query
+ *
+ * @param mixed $input Value to quote
+ * @param string $type Type of data
+ *
+ * @return string Quoted/converted string for use in query
+ */
+ public function quote($input, $type = null)
+ {
+ // handle int directly for better performance
+ if ($type == 'integer' || $type == 'int') {
+ return intval($input);
+ }
+
+ // create DB handle if not available
+ if (!$this->dbh) {
+ $this->db_connect('r');
+ }
+
+ if ($this->dbh) {
+ $map = array(
+ 'bool' => PDO::PARAM_BOOL,
+ 'integer' => PDO::PARAM_INT,
+ );
+ $type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR;
+ return $this->dbh->quote($input, $type);
+ }
+
+ return 'NULL';
+ }
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ *
+ * @param string $str Value to quote
+ *
+ * @return string Quoted string for use in query
+ * @deprecated Replaced by rcube_db::quote_identifier
+ * @see rcube_db::quote_identifier
+ */
+ public function quoteIdentifier($str)
+ {
+ return $this->quote_identifier($str);
+ }
+
+ /**
+ * Quotes a string so it can be safely used as a table or column name
+ *
+ * @param string $str Value to quote
+ *
+ * @return string Quoted string for use in query
+ */
+ public function quote_identifier($str)
+ {
+ $start = $this->options['identifier_start'];
+ $end = $this->options['identifier_end'];
+ $name = array();
+
+ foreach (explode('.', $str) as $elem) {
+ $elem = str_replace(array($start, $end), '', $elem);
+ $name[] = $start . $elem . $end;
+ }
+
+ return implode($name, '.');
+ }
+
+ /**
+ * Return SQL function for current time and date
+ *
+ * @return string SQL function to use in query
+ */
+ public function now()
+ {
+ return "now()";
+ }
+
+ /**
+ * Return list of elements for use with SQL's IN clause
+ *
+ * @param array $arr Input array
+ * @param string $type Type of data
+ *
+ * @return string Comma-separated list of quoted values for use in query
+ */
+ public function array2list($arr, $type = null)
+ {
+ if (!is_array($arr)) {
+ return $this->quote($arr, $type);
+ }
+
+ foreach ($arr as $idx => $item) {
+ $arr[$idx] = $this->quote($item, $type);
+ }
+
+ return implode(',', $arr);
+ }
+
+ /**
+ * Return SQL statement to convert a field value into a unix timestamp
+ *
+ * This method is deprecated and should not be used anymore due to limitations
+ * of timestamp functions in Mysql (year 2038 problem)
+ *
+ * @param string $field Field name
+ *
+ * @return string SQL statement to use in query
+ * @deprecated
+ */
+ public function unixtimestamp($field)
+ {
+ return "UNIX_TIMESTAMP($field)";
+ }
+
+ /**
+ * Return SQL statement to convert from a unix timestamp
+ *
+ * @param int $timestamp Unix timestamp
+ *
+ * @return string Date string in db-specific format
+ */
+ public function fromunixtime($timestamp)
+ {
+ return date("'Y-m-d H:i:s'", $timestamp);
+ }
+
+ /**
+ * Return SQL statement for case insensitive LIKE
+ *
+ * @param string $column Field name
+ * @param string $value Search value
+ *
+ * @return string SQL statement to use in query
+ */
+ public function ilike($column, $value)
+ {
+ return $this->quote_identifier($column).' LIKE '.$this->quote($value);
+ }
+
+ /**
+ * Abstract SQL statement for value concatenation
+ *
+ * @return string SQL statement to be used in query
+ */
+ public function concat(/* col1, col2, ... */)
+ {
+ $args = func_get_args();
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return '(' . join(' || ', $args) . ')';
+ }
+
+ /**
+ * Encodes non-UTF-8 characters in string/array/object (recursive)
+ *
+ * @param mixed $input Data to fix
+ *
+ * @return mixed Properly UTF-8 encoded data
+ */
+ public static function encode($input)
+ {
+ if (is_object($input)) {
+ foreach (get_object_vars($input) as $idx => $value) {
+ $input->$idx = self::encode($value);
+ }
+ return $input;
+ }
+ else if (is_array($input)) {
+ foreach ($input as $idx => $value) {
+ $input[$idx] = self::encode($value);
+ }
+ return $input;
+ }
+
+ return utf8_encode($input);
+ }
+
+ /**
+ * Decodes encoded UTF-8 string/object/array (recursive)
+ *
+ * @param mixed $input Input data
+ *
+ * @return mixed Decoded data
+ */
+ public static function decode($input)
+ {
+ if (is_object($input)) {
+ foreach (get_object_vars($input) as $idx => $value) {
+ $input->$idx = self::decode($value);
+ }
+ return $input;
+ }
+ else if (is_array($input)) {
+ foreach ($input as $idx => $value) {
+ $input[$idx] = self::decode($value);
+ }
+ return $input;
+ }
+
+ return utf8_decode($input);
+ }
+
+ /**
+ * Adds a query result and returns a handle ID
+ *
+ * @param object $res Query handle
+ *
+ * @return int Handle ID
+ */
+ protected function _add_result($res)
+ {
+ $this->last_res_id = sizeof($this->a_query_results);
+ $this->a_query_results[$this->last_res_id] = $res;
+
+ return $this->last_res_id;
+ }
+
+ /**
+ * Resolves a given handle ID and returns the according query handle
+ * If no ID is specified, the last resource handle will be returned
+ *
+ * @param int $res_id Handle ID
+ *
+ * @return mixed Resource handle or false on failure
+ */
+ protected function _get_result($res_id = null)
+ {
+ if ($res_id == null) {
+ $res_id = $this->last_res_id;
+ }
+
+ if (!empty($this->a_query_results[$res_id])) {
+ return $this->a_query_results[$res_id];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return correct name for a specific database table
+ *
+ * @param string $table Table name
+ *
+ * @return string Translated table name
+ */
+ public function table_name($table)
+ {
+ $rcube = rcube::get_instance();
+
+ // return table name if configured
+ $config_key = 'db_table_'.$table;
+
+ if ($name = $rcube->config->get($config_key)) {
+ return $name;
+ }
+
+ return $table;
+ }
+
+ /**
+ * MDB2 DSN string parser
+ *
+ * @param string $sequence Secuence name
+ *
+ * @return array DSN parameters
+ */
+ public static function parse_dsn($dsn)
+ {
+ if (empty($dsn)) {
+ return null;
+ }
+
+ // Find phptype and dbsyntax
+ if (($pos = strpos($dsn, '://')) !== false) {
+ $str = substr($dsn, 0, $pos);
+ $dsn = substr($dsn, $pos + 3);
+ }
+ else {
+ $str = $dsn;
+ $dsn = null;
+ }
+
+ // Get phptype and dbsyntax
+ // $str => phptype(dbsyntax)
+ if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
+ $parsed['phptype'] = $arr[1];
+ $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
+ }
+ else {
+ $parsed['phptype'] = $str;
+ $parsed['dbsyntax'] = $str;
+ }
+
+ if (empty($dsn)) {
+ return $parsed;
+ }
+
+ // Get (if found): username and password
+ // $dsn => username:password@protocol+hostspec/database
+ if (($at = strrpos($dsn,'@')) !== false) {
+ $str = substr($dsn, 0, $at);
+ $dsn = substr($dsn, $at + 1);
+ if (($pos = strpos($str, ':')) !== false) {
+ $parsed['username'] = rawurldecode(substr($str, 0, $pos));
+ $parsed['password'] = rawurldecode(substr($str, $pos + 1));
+ }
+ else {
+ $parsed['username'] = rawurldecode($str);
+ }
+ }
+
+ // Find protocol and hostspec
+
+ // $dsn => proto(proto_opts)/database
+ if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
+ $proto = $match[1];
+ $proto_opts = $match[2] ? $match[2] : false;
+ $dsn = $match[3];
+ }
+ // $dsn => protocol+hostspec/database (old format)
+ else {
+ if (strpos($dsn, '+') !== false) {
+ list($proto, $dsn) = explode('+', $dsn, 2);
+ }
+ if ( strpos($dsn, '//') === 0
+ && strpos($dsn, '/', 2) !== false
+ && $parsed['phptype'] == 'oci8'
+ ) {
+ //oracle's "Easy Connect" syntax:
+ //"username/password@[//]host[:port][/service_name]"
+ //e.g. "scott/tiger@//mymachine:1521/oracle"
+ $proto_opts = $dsn;
+ $pos = strrpos($proto_opts, '/');
+ $dsn = substr($proto_opts, $pos + 1);
+ $proto_opts = substr($proto_opts, 0, $pos);
+ }
+ else if (strpos($dsn, '/') !== false) {
+ list($proto_opts, $dsn) = explode('/', $dsn, 2);
+ }
+ else {
+ $proto_opts = $dsn;
+ $dsn = null;
+ }
+ }
+
+ // process the different protocol options
+ $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
+ $proto_opts = rawurldecode($proto_opts);
+ if (strpos($proto_opts, ':') !== false) {
+ list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
+ }
+ if ($parsed['protocol'] == 'tcp') {
+ $parsed['hostspec'] = $proto_opts;
+ }
+ else if ($parsed['protocol'] == 'unix') {
+ $parsed['socket'] = $proto_opts;
+ }
+
+ // Get dabase if any
+ // $dsn => database
+ if ($dsn) {
+ // /database
+ if (($pos = strpos($dsn, '?')) === false) {
+ $parsed['database'] = rawurldecode($dsn);
+ // /database?param1=value1&param2=value2
+ }
+ else {
+ $parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
+ $dsn = substr($dsn, $pos + 1);
+ if (strpos($dsn, '&') !== false) {
+ $opts = explode('&', $dsn);
+ }
+ else { // database?param1=value1
+ $opts = array($dsn);
+ }
+ foreach ($opts as $opt) {
+ list($key, $value) = explode('=', $opt);
+ if (!array_key_exists($key, $parsed) || false === $parsed[$key]) {
+ // don't allow params overwrite
+ $parsed[$key] = rawurldecode($value);
+ }
+ }
+ }
+ }
+
+ return $parsed;
+ }
+
+ /**
+ * Returns PDO DSN string from DSN array
+ *
+ * @param array $dsn DSN parameters
+ *
+ * @return string DSN string
+ */
+ protected function dsn_string($dsn)
+ {
+ $params = array();
+ $result = $dsn['phptype'] . ':';
+
+ if ($dsn['hostspec']) {
+ $params[] = 'host=' . $dsn['hostspec'];
+ }
+
+ if ($dsn['port']) {
+ $params[] = 'port=' . $dsn['port'];
+ }
+
+ if ($dsn['database']) {
+ $params[] = 'dbname=' . $dsn['database'];
+ }
+
+ if (!empty($params)) {
+ $result .= implode(';', $params);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns driver-specific connection options
+ *
+ * @param array $dsn DSN parameters
+ *
+ * @return array Connection options
+ */
+ protected function dsn_options($dsn)
+ {
+ $result = array();
+
+ return $result;
+ }
+}
diff --git a/lib/ext/Roundcube/rcube_db_mssql.php b/lib/ext/Roundcube/rcube_db_mssql.php
new file mode 100644
index 0000000..5cbcfab
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_db_mssql.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_db_mssql.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Database wrapper class that implements PHP PDO functions |
+ | for MS SQL Server database |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Database
+ * @version 1.0
+ */
+class rcube_db_mssql extends rcube_db
+{
+ /**
+ * Driver initialization
+ */
+ protected function init()
+ {
+ $this->options['identifier_start'] = '[';
+ $this->options['identifier_end'] = ']';
+ }
+
+ /**
+ * Character setting
+ */
+ protected function set_charset($charset)
+ {
+ // UTF-8 is default
+ }
+
+ /**
+ * Return SQL function for current time and date
+ *
+ * @return string SQL function to use in query
+ */
+ public function now()
+ {
+ return "getdate()";
+ }
+
+ /**
+ * Return SQL statement to convert a field value into a unix timestamp
+ *
+ * This method is deprecated and should not be used anymore due to limitations
+ * of timestamp functions in Mysql (year 2038 problem)
+ *
+ * @param string $field Field name
+ *
+ * @return string SQL statement to use in query
+ * @deprecated
+ */
+ public function unixtimestamp($field)
+ {
+ return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
+ }
+
+ /**
+ * Abstract SQL statement for value concatenation
+ *
+ * @return string SQL statement to be used in query
+ */
+ public function concat(/* col1, col2, ... */)
+ {
+ $args = func_get_args();
+
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return '(' . join('+', $args) . ')';
+ }
+
+ /**
+ * Adds TOP (LIMIT,OFFSET) clause to the query
+ *
+ * @param string $query SQL query
+ * @param int $limit Number of rows
+ * @param int $offset Offset
+ *
+ * @return string SQL query
+ */
+ protected function set_limit($query, $limit = 0, $offset = 0)
+ {
+ $limit = intval($limit);
+ $offset = intval($offset);
+
+ $orderby = stristr($query, 'ORDER BY');
+ if ($orderby !== false) {
+ $sort = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc';
+ $order = str_ireplace('ORDER BY', '', $orderby);
+ $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order));
+ }
+
+ $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($limit + $offset) . ' ', $query);
+
+ $query = 'SELECT * FROM (SELECT TOP ' . $limit . ' * FROM (' . $query . ') AS inner_tbl';
+ if ($orderby !== false) {
+ $query .= ' ORDER BY ' . $order . ' ';
+ $query .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC';
+ }
+ $query .= ') AS outer_tbl';
+ if ($orderby !== false) {
+ $query .= ' ORDER BY ' . $order . ' ' . $sort;
+ }
+
+ return $query;
+ }
+
+ /**
+ * Returns PDO DSN string from DSN array
+ */
+ protected function dsn_string($dsn)
+ {
+ $params = array();
+ $result = $dsn['phptype'] . ':';
+
+ if ($dsn['hostspec']) {
+ $host = $dsn['hostspec'];
+ if ($dsn['port']) {
+ $host .= ',' . $dsn['port'];
+ }
+ $params[] = 'host=' . $host;
+ }
+
+ if ($dsn['database']) {
+ $params[] = 'dbname=' . $dsn['database'];
+ }
+
+ if (!empty($params)) {
+ $result .= implode(';', $params);
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/ext/Roundcube/rcube_db_mysql.php b/lib/ext/Roundcube/rcube_db_mysql.php
new file mode 100644
index 0000000..3606ec1
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_db_mysql.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_db_mysql.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Database wrapper class that implements PHP PDO functions |
+ | for MySQL database |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Database
+ * @version 1.0
+ */
+class rcube_db_mysql extends rcube_db
+{
+ /**
+ * Driver initialization/configuration
+ */
+ protected function init()
+ {
+ // SQL identifiers quoting
+ $this->options['identifier_start'] = '`';
+ $this->options['identifier_end'] = '`';
+ }
+
+ /**
+ * Abstract SQL statement for value concatenation
+ *
+ * @return string SQL statement to be used in query
+ */
+ public function concat(/* col1, col2, ... */)
+ {
+ $args = func_get_args();
+
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return 'CONCAT(' . join(', ', $args) . ')';
+ }
+
+ /**
+ * Returns PDO DSN string from DSN array
+ *
+ * @param array $dsn DSN parameters
+ *
+ * @return string Connection string
+ */
+ protected function dsn_string($dsn)
+ {
+ $params = array();
+ $result = 'mysql:';
+
+ if ($dsn['database']) {
+ $params[] = 'dbname=' . $dsn['database'];
+ }
+
+ if ($dsn['hostspec']) {
+ $params[] = 'host=' . $dsn['hostspec'];
+ }
+
+ if ($dsn['port']) {
+ $params[] = 'port=' . $dsn['port'];
+ }
+
+ if ($dsn['socket']) {
+ $params[] = 'unix_socket=' . $dsn['socket'];
+ }
+
+ $params[] = 'charset=utf8';
+
+ if (!empty($params)) {
+ $result .= implode(';', $params);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns driver-specific connection options
+ *
+ * @param array $dsn DSN parameters
+ *
+ * @return array Connection options
+ */
+ protected function dsn_options($dsn)
+ {
+ $result = array();
+
+ if (!empty($dsn['key'])) {
+ $result[PDO::MYSQL_ATTR_KEY] = $dsn['key'];
+ }
+
+ if (!empty($dsn['cipher'])) {
+ $result[PDO::MYSQL_ATTR_CIPHER] = $dsn['cipher'];
+ }
+
+ if (!empty($dsn['cert'])) {
+ $result[PDO::MYSQL_ATTR_SSL_CERT] = $dsn['cert'];
+ }
+
+ if (!empty($dsn['capath'])) {
+ $result[PDO::MYSQL_ATTR_SSL_CAPATH] = $dsn['capath'];
+ }
+
+ if (!empty($dsn['ca'])) {
+ $result[PDO::MYSQL_ATTR_SSL_CA] = $dsn['ca'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get database runtime variables
+ *
+ * @param string $varname Variable name
+ * @param mixed $default Default value if variable is not set
+ *
+ * @return mixed Variable value or default
+ */
+ public function get_variable($varname, $default = null)
+ {
+ if (!isset($this->variables)) {
+ $this->variables = array();
+
+ $result = $this->query('SHOW VARIABLES');
+
+ while ($sql_arr = $this->fetch_array($result)) {
+ $this->variables[$row[0]] = $row[1];
+ }
+ }
+
+ return isset($this->variables[$varname]) ? $this->variables[$varname] : $default;
+ }
+
+}
diff --git a/lib/ext/Roundcube/rcube_db_pgsql.php b/lib/ext/Roundcube/rcube_db_pgsql.php
new file mode 100644
index 0000000..285b8e2
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_db_pgsql.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_db_pgsql.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Database wrapper class that implements PHP PDO functions |
+ | for PostgreSQL database |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Database
+ * @version 1.0
+ */
+class rcube_db_pgsql extends rcube_db
+{
+ /**
+ * Get last inserted record ID
+ *
+ * @param string $table Table name (to find the incremented sequence)
+ *
+ * @return mixed ID or false on failure
+ */
+ public function insert_id($table = null)
+ {
+ if (!$this->db_connected || $this->db_mode == 'r') {
+ return false;
+ }
+
+ if ($table) {
+ $table = $this->sequence_name($table);
+ }
+
+ $id = $this->dbh->lastInsertId($table);
+
+ return $id;
+ }
+
+ /**
+ * Return correct name for a specific database sequence
+ *
+ * @param string $sequence Secuence name
+ *
+ * @return string Translated sequence name
+ */
+ protected function sequence_name($sequence)
+ {
+ $rcube = rcube::get_instance();
+
+ // return sequence name if configured
+ $config_key = 'db_sequence_'.$sequence;
+
+ if ($name = $rcube->config->get($config_key)) {
+ return $name;
+ }
+
+ return $sequence;
+ }
+
+ /**
+ * Return SQL statement to convert a field value into a unix timestamp
+ *
+ * This method is deprecated and should not be used anymore due to limitations
+ * of timestamp functions in Mysql (year 2038 problem)
+ *
+ * @param string $field Field name
+ *
+ * @return string SQL statement to use in query
+ * @deprecated
+ */
+ public function unixtimestamp($field)
+ {
+ return "EXTRACT (EPOCH FROM $field)";
+ }
+
+ /**
+ * Return SQL statement for case insensitive LIKE
+ *
+ * @param string $column Field name
+ * @param string $value Search value
+ *
+ * @return string SQL statement to use in query
+ */
+ public function ilike($column, $value)
+ {
+ return $this->quote_identifier($column) . ' ILIKE ' . $this->quote($value);
+ }
+
+ /**
+ * Get database runtime variables
+ *
+ * @param string $varname Variable name
+ * @param mixed $default Default value if variable is not set
+ *
+ * @return mixed Variable value or default
+ */
+ public function get_variable($varname, $default = null)
+ {
+ // There's a known case when max_allowed_packet is queried
+ // PostgreSQL doesn't have such limit, return immediately
+ if ($varname == 'max_allowed_packet') {
+ return $default;
+ }
+
+ if (!isset($this->variables)) {
+ $this->variables = array();
+
+ $result = $this->query('SHOW ALL');
+
+ while ($row = $this->fetch_array($result)) {
+ $this->variables[$row[0]] = $row[1];
+ }
+ }
+
+ return isset($this->variables[$varname]) ? $this->variables[$varname] : $default;
+ }
+
+}
diff --git a/lib/ext/Roundcube/rcube_db_sqlite.php b/lib/ext/Roundcube/rcube_db_sqlite.php
new file mode 100644
index 0000000..a9774cd
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_db_sqlite.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_db_sqlite.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Database wrapper class that implements PHP PDO functions |
+ | for SQLite database |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Database
+ * @version 1.0
+ */
+class rcube_db_sqlite extends rcube_db
+{
+ /**
+ * Database character set
+ */
+ protected function set_charset($charset)
+ {
+ }
+
+ /**
+ * Prepare connection
+ */
+ protected function conn_prepare($dsn)
+ {
+ // Create database file, required by PDO to exist on connection
+ if (!empty($dsn['database']) && !file_exists($dsn['database'])) {
+ $created = touch($dsn['database']);
+
+ // File mode setting, for compat. with MDB2
+ if (!empty($dsn['mode']) && $created) {
+ chmod($dsn['database'], octdec($dsn['mode']));
+ }
+ }
+ }
+
+ /**
+ * Configure connection, create database if not exists
+ */
+ protected function conn_configure($dsn, $dbh)
+ {
+ // we emulate via callback some missing functions
+ $dbh->sqliteCreateFunction('unix_timestamp', array('rcube_db_sqlite', 'sqlite_unix_timestamp'), 1);
+ $dbh->sqliteCreateFunction('now', array('rcube_db_sqlite', 'sqlite_now'), 0);
+
+ // Initialize database structure in file is empty
+ if (!empty($dsn['database']) && !filesize($dsn['database'])) {
+ $data = file_get_contents(INSTALL_PATH . 'SQL/sqlite.initial.sql');
+
+ if (strlen($data)) {
+ $this->debug('INITIALIZE DATABASE');
+
+ $q = $dbh->exec($data);
+
+ if ($q === false) {
+ $error = $dbh->errorInfo();
+ $this->db_error = true;
+ $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
+
+ rcube::raise_error(array('code' => 500, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => $this->db_error_msg), true, false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback for sqlite: unix_timestamp()
+ */
+ public static function sqlite_unix_timestamp($timestamp = '')
+ {
+ $timestamp = trim($timestamp);
+ if (!$timestamp) {
+ $ret = time();
+ }
+ else if (!preg_match('/^[0-9]+$/s', $timestamp)) {
+ $ret = strtotime($timestamp);
+ }
+ else {
+ $ret = $timestamp;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Callback for sqlite: now()
+ */
+ public static function sqlite_now()
+ {
+ return date("Y-m-d H:i:s");
+ }
+
+ /**
+ * Returns list of tables in database
+ *
+ * @return array List of all tables of the current database
+ */
+ public function list_tables()
+ {
+ if ($this->tables === null) {
+ $q = $this->query('SELECT name FROM sqlite_master'
+ .' WHERE type = \'table\' ORDER BY name');
+
+ if ($res = $this->_get_result($q)) {
+ $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+ else {
+ $this->tables = array();
+ }
+ }
+
+ return $this->tables;
+ }
+
+ /**
+ * Returns list of columns in database table
+ *
+ * @param string $table Table name
+ *
+ * @return array List of table cols
+ */
+ public function list_cols($table)
+ {
+ $q = $this->query('SELECT sql FROM sqlite_master WHERE type = ? AND name = ?',
+ array('table', $table));
+
+ $columns = array();
+
+ if ($sql = $this->fetch_array($q)) {
+ $sql = $sql[0];
+ $start_pos = strpos($sql, '(');
+ $end_pos = strrpos($sql, ')');
+ $sql = substr($sql, $start_pos+1, $end_pos-$start_pos-1);
+ $lines = explode(',', $sql);
+
+ foreach ($lines as $line) {
+ $line = explode(' ', trim($line));
+
+ if ($line[0] && strpos($line[0], '--') !== 0) {
+ $column = $line[0];
+ $columns[] = trim($column, '"');
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Build DSN string for PDO constructor
+ */
+ protected function dsn_string($dsn)
+ {
+ return $dsn['phptype'] . ':' . $dsn['database'];
+ }
+}
diff --git a/lib/ext/Roundcube/rcube_db_sqlsrv.php b/lib/ext/Roundcube/rcube_db_sqlsrv.php
new file mode 100644
index 0000000..feddbe7
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_db_sqlsrv.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | program/include/rcube_db_sqlsrv.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Database wrapper class that implements PHP PDO functions |
+ | for MS SQL Server database |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Database independent query interface
+ *
+ * This is a wrapper for the PHP PDO
+ *
+ * @package Database
+ * @version 1.0
+ */
+class rcube_db_sqlsrv extends rcube_db
+{
+ /**
+ * Driver initialization
+ */
+ protected function init()
+ {
+ $this->options['identifier_start'] = '[';
+ $this->options['identifier_end'] = ']';
+ }
+
+ /**
+ * Database character set setting
+ */
+ protected function set_charset($charset)
+ {
+ // UTF-8 is default
+ }
+
+ /**
+ * Return SQL function for current time and date
+ *
+ * @return string SQL function to use in query
+ */
+ public function now()
+ {
+ return "getdate()";
+ }
+
+ /**
+ * Return SQL statement to convert a field value into a unix timestamp
+ *
+ * This method is deprecated and should not be used anymore due to limitations
+ * of timestamp functions in Mysql (year 2038 problem)
+ *
+ * @param string $field Field name
+ *
+ * @return string SQL statement to use in query
+ * @deprecated
+ */
+ public function unixtimestamp($field)
+ {
+ return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
+ }
+
+ /**
+ * Abstract SQL statement for value concatenation
+ *
+ * @return string SQL statement to be used in query
+ */
+ public function concat(/* col1, col2, ... */)
+ {
+ $args = func_get_args();
+
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return '(' . join('+', $args) . ')';
+ }
+
+ /**
+ * Adds TOP (LIMIT,OFFSET) clause to the query
+ *
+ * @param string $query SQL query
+ * @param int $limit Number of rows
+ * @param int $offset Offset
+ *
+ * @return string SQL query
+ */
+ protected function set_limit($query, $limit = 0, $offset = 0)
+ {
+ $limit = intval($limit);
+ $offset = intval($offset);
+
+ $orderby = stristr($query, 'ORDER BY');
+ if ($orderby !== false) {
+ $sort = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc';
+ $order = str_ireplace('ORDER BY', '', $orderby);
+ $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order));
+ }
+
+ $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($limit + $offset) . ' ', $query);
+
+ $query = 'SELECT * FROM (SELECT TOP ' . $limit . ' * FROM (' . $query . ') AS inner_tbl';
+ if ($orderby !== false) {
+ $query .= ' ORDER BY ' . $order . ' ';
+ $query .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC';
+ }
+ $query .= ') AS outer_tbl';
+ if ($orderby !== false) {
+ $query .= ' ORDER BY ' . $order . ' ' . $sort;
+ }
+
+ return $query;
+ }
+
+ /**
+ * Returns PDO DSN string from DSN array
+ */
+ protected function dsn_string($dsn)
+ {
+ $params = array();
+ $result = 'sqlsrv:';
+
+ if ($dsn['hostspec']) {
+ $host = $dsn['hostspec'];
+
+ if ($dsn['port']) {
+ $host .= ',' . $dsn['port'];
+ }
+ $params[] = 'Server=' . $host;
+ }
+
+ if ($dsn['database']) {
+ $params[] = 'Database=' . $dsn['database'];
+ }
+
+ if (!empty($params)) {
+ $result .= implode(';', $params);
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index 530284a..a061a1f 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -1,1146 +1,1146 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_imap_cache.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2011, 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: |
| Caching of IMAP folder contents (messages and index) |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube messages cache
*
* @package Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @version 1.0
*/
class rcube_imap_cache
{
/**
* Instance of rcube_imap
*
* @var rcube_imap
*/
private $imap;
/**
- * Instance of rcube_mdb2
+ * Instance of rcube_db
*
- * @var rcube_mdb2
+ * @var rcube_db
*/
private $db;
/**
* User ID
*
* @var int
*/
private $userid;
/**
* Internal (in-memory) cache
*
* @var array
*/
private $icache = array();
private $skip_deleted = false;
/**
* List of known flags. Thanks to this we can handle flag changes
* with good performance. Bad thing is we need to know used flags.
*/
public $flags = array(
1 => 'SEEN', // RFC3501
2 => 'DELETED', // RFC3501
4 => 'ANSWERED', // RFC3501
8 => 'FLAGGED', // RFC3501
16 => 'DRAFT', // RFC3501
32 => 'MDNSENT', // RFC3503
64 => 'FORWARDED', // RFC5550
128 => 'SUBMITPENDING', // RFC5550
256 => 'SUBMITTED', // RFC5550
512 => 'JUNK',
1024 => 'NONJUNK',
2048 => 'LABEL1',
4096 => 'LABEL2',
8192 => 'LABEL3',
16384 => 'LABEL4',
32768 => 'LABEL5',
);
/**
* Object constructor.
*/
function __construct($db, $imap, $userid, $skip_deleted)
{
$this->db = $db;
$this->imap = $imap;
$this->userid = $userid;
$this->skip_deleted = $skip_deleted;
}
/**
* Cleanup actions (on shutdown).
*/
public function close()
{
$this->save_icache();
$this->icache = null;
}
/**
* Return (sorted) messages index (UIDs).
* If index doesn't exist or is invalid, will be updated.
*
* @param string $mailbox Folder name
* @param string $sort_field Sorting column
* @param string $sort_order Sorting order (ASC|DESC)
* @param bool $exiting Skip index initialization if it doesn't exist in DB
*
* @return array Messages index
*/
function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
{
if (empty($this->icache[$mailbox])) {
$this->icache[$mailbox] = array();
}
$sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
// Seek in internal cache
if (array_key_exists('index', $this->icache[$mailbox])) {
// The index was fetched from database already, but not validated yet
if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
$index = $this->icache[$mailbox]['index'];
}
// We've got a valid index
else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) {
$result = $this->icache[$mailbox]['index']['object'];
if ($result->get_parameters('ORDER') != $sort_order) {
$result->revert();
}
return $result;
}
}
// Get index from DB (if DB wasn't already queried)
if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
$index = $this->get_index_row($mailbox);
// set the flag that DB was already queried for index
// this way we'll be able to skip one SELECT, when
// get_index() is called more than once
$this->icache[$mailbox]['index_queried'] = true;
}
$data = null;
// @TODO: Think about skipping validation checks.
// If we could check only every 10 minutes, we would be able to skip
// expensive checks, mailbox selection or even IMAP connection, this would require
// additional logic to force cache invalidation in some cases
// and many rcube_imap changes to connect when needed
// Entry exists, check cache status
if (!empty($index)) {
$exists = true;
if ($sort_field == 'ANY') {
$sort_field = $index['sort_field'];
}
if ($sort_field != $index['sort_field']) {
$is_valid = false;
}
else {
$is_valid = $this->validate($mailbox, $index, $exists);
}
if ($is_valid) {
$data = $index['object'];
// revert the order if needed
if ($data->get_parameters('ORDER') != $sort_order) {
$data->revert();
}
}
}
else {
if ($existing) {
return null;
}
else if ($sort_field == 'ANY') {
$sort_field = '';
}
// Got it in internal cache, so the row already exist
$exists = array_key_exists('index', $this->icache[$mailbox]);
}
// Index not found, not valid or sort field changed, get index from IMAP server
if ($data === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
// insert/update
$this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']);
}
$this->icache[$mailbox]['index'] = array(
'object' => $data,
'sort_field' => $sort_field,
'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
);
return $data;
}
/**
* Return messages thread.
* If threaded index doesn't exist or is invalid, will be updated.
*
* @param string $mailbox Folder name
* @param string $sort_field Sorting column
* @param string $sort_order Sorting order (ASC|DESC)
*
* @return array Messages threaded index
*/
function get_thread($mailbox)
{
if (empty($this->icache[$mailbox])) {
$this->icache[$mailbox] = array();
}
// Seek in internal cache
if (array_key_exists('thread', $this->icache[$mailbox])) {
return $this->icache[$mailbox]['thread']['object'];
}
// Get thread from DB (if DB wasn't already queried)
if (empty($this->icache[$mailbox]['thread_queried'])) {
$index = $this->get_thread_row($mailbox);
// set the flag that DB was already queried for thread
// this way we'll be able to skip one SELECT, when
// get_thread() is called more than once or after clear()
$this->icache[$mailbox]['thread_queried'] = true;
}
// Entry exist, check cache status
if (!empty($index)) {
$exists = true;
$is_valid = $this->validate($mailbox, $index, $exists);
if (!$is_valid) {
$index = null;
}
}
// Index not found or not valid, get index from IMAP server
if ($index === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
if ($mbox_data['EXISTS']) {
// get all threads (default sort order)
$threads = $this->imap->fetch_threads($mailbox, true);
}
else {
$threads = new rcube_result_thread($mailbox, '* THREAD');
}
$index['object'] = $threads;
// insert/update
$this->add_thread_row($mailbox, $threads, $mbox_data, $exists);
}
$this->icache[$mailbox]['thread'] = $index;
return $index['object'];
}
/**
* Returns list of messages (headers). See rcube_imap::fetch_headers().
*
* @param string $mailbox Folder name
* @param array $msgs Message UIDs
*
* @return array The list of messages (rcube_message_header) indexed by UID
*/
function get_messages($mailbox, $msgs = array())
{
if (empty($msgs)) {
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 (!empty($result[$uid])) {
// save memory, we don't need message body here (?)
$result[$uid]->body = null;
unset($msgs[$uid]);
}
}
// Fetch not found messages from IMAP server
if (!empty($msgs)) {
$messages = $this->imap->fetch_headers($mailbox, array_keys($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));
$result[$msg->uid] = $msg;
}
}
}
return $result;
}
/**
* Returns message data.
*
* @param string $mailbox Folder name
* @param int $uid Message UID
* @param bool $update If message doesn't exists in cache it will be fetched
* from IMAP server
* @param bool $no_cache Enables internal cache usage
*
* @return rcube_message_header Message data
*/
function get_message($mailbox, $uid, $update = true, $cache = true)
{
// Check internal cache
if ($this->icache['message']
&& $this->icache['message']['mailbox'] == $mailbox
&& $this->icache['message']['object']->uid == $uid
) {
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 ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$message = $this->build_message($sql_arr);
$found = true;
}
// Get the message from IMAP server
if (empty($message) && $update) {
$message = $this->imap->get_message_headers($uid, $mailbox, true);
// cache will be updated in close(), see below
}
// Save the message in internal cache, will be written to DB in close()
// Common scenario: user opens unseen message
// - get message (SELECT)
// - set message headers/structure (INSERT or UPDATE)
// - set \Seen flag (UPDATE)
// This way we can skip one UPDATE
if (!empty($message) && $cache) {
// Save current message from internal cache
$this->save_icache();
$this->icache['message'] = array(
'object' => $message,
'mailbox' => $mailbox,
'exists' => $found,
'md5sum' => md5(serialize($message)),
);
}
return $message;
}
/**
* Saves the message in cache.
*
* @param string $mailbox Folder name
* @param rcube_message_header $message Message data
* @param bool $force Skips message in-cache existance check
*/
function add_message($mailbox, $message, $force = false)
{
if (!is_object($message) || empty($message->uid)) {
return;
}
$msg = serialize($this->db->encode(clone $message));
$flags = 0;
if (!empty($message->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($message->flags[$flag])) {
$flags += $idx;
}
}
}
unset($msg->flags);
// update cache record (even if it exists, the update
// here will work as select, assume row exist if affected_rows=0)
if (!$force) {
$res = $this->db->query(
"UPDATE ".$this->db->table_name('cache_messages')
." SET flags = ?, data = ?, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?"
." AND uid = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
if ($this->db->affected_rows()) {
return;
}
}
// insert new record
$this->db->query(
"INSERT INTO ".$this->db->table_name('cache_messages')
." (user_id, mailbox, uid, flags, changed, data)"
." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
$this->userid, $mailbox, (int) $message->uid, $flags, $msg);
}
/**
* Sets the flag for specified message.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs or null to change flag
* of all messages in a folder
* @param string $flag The name of the flag
* @param bool $enabled Flag state
*/
function change_flag($mailbox, $uids, $flag, $enabled = false)
{
$flag = strtoupper($flag);
$idx = (int) array_search($flag, $this->flags);
if (!$idx) {
return;
}
// Internal cache update
if ($uids && count($uids) == 1 && ($uid = current($uids))
&& ($message = $this->icache['message'])
&& $message['mailbox'] == $mailbox && $message['object']->uid == $uid
) {
$message['object']->flags[$flag] = $enabled;
return;
}
$this->db->query(
"UPDATE ".$this->db->table_name('cache_messages')
." SET changed = ".$this->db->now()
.", flags = flags ".($enabled ? "+ $idx" : "- $idx")
." WHERE user_id = ?"
." AND mailbox = ?"
.($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : "")
." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
$this->userid, $mailbox);
}
/**
* Removes message(s) from cache.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs, NULL removes all messages
*/
function remove_message($mailbox = null, $uids = null)
{
if (!strlen($mailbox)) {
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache_messages')
." WHERE user_id = ?",
$this->userid);
}
else {
// Remove the message from internal cache
if (!empty($uids) && !is_array($uids) && ($message = $this->icache['message'])
&& $message['mailbox'] == $mailbox && $message['object']->uid == $uids
) {
$this->icache['message'] = null;
}
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache_messages')
." WHERE user_id = ?"
." AND mailbox = ?"
.($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
$this->userid, $mailbox);
}
}
/**
* Clears index cache.
*
* @param string $mailbox Folder name
* @param bool $remove Enable to remove the DB row
*/
function remove_index($mailbox = null, $remove = false)
{
// The index should be only removed from database when
// UIDVALIDITY was detected or the mailbox is empty
// otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
if ($remove) {
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache_index')
." WHERE user_id = ?"
.(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
$this->userid
);
}
else {
$this->db->query(
"UPDATE ".$this->db->table_name('cache_index')
." SET valid = 0"
." WHERE user_id = ?"
.(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
$this->userid
);
}
if (strlen($mailbox)) {
unset($this->icache[$mailbox]['index']);
// Index removed, set flag to skip SELECT query in get_index()
$this->icache[$mailbox]['index_queried'] = true;
}
else {
$this->icache = array();
}
}
/**
* Clears thread cache.
*
* @param string $mailbox Folder name
*/
function remove_thread($mailbox = null)
{
$this->db->query(
"DELETE FROM ".$this->db->table_name('cache_thread')
." WHERE user_id = ?"
.(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
$this->userid
);
if (strlen($mailbox)) {
unset($this->icache[$mailbox]['thread']);
// Thread data removed, set flag to skip SELECT query in get_thread()
$this->icache[$mailbox]['thread_queried'] = true;
}
else {
$this->icache = array();
}
}
/**
* Clears the cache.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs, NULL removes all messages in a folder
*/
function clear($mailbox = null, $uids = null)
{
$this->remove_index($mailbox, true);
$this->remove_thread($mailbox);
$this->remove_message($mailbox, $uids);
}
/**
* Delete cache entries older than TTL
*
* @param string $ttl Lifetime of message cache entries
*/
function expunge($ttl)
{
// get expiration timestamp
$ts = get_offset_time($ttl, -1);
$this->db->query("DELETE FROM ".get_table_name('cache_messages')
." WHERE changed < " . $this->db->fromunixtime($ts));
$this->db->query("DELETE FROM ".get_table_name('cache_index')
." WHERE changed < " . $this->db->fromunixtime($ts));
$this->db->query("DELETE FROM ".get_table_name('cache_thread')
." WHERE changed < " . $this->db->fromunixtime($ts));
}
/**
* Fetches index data from database
*/
private function get_index_row($mailbox)
{
// Get index from DB
$sql_result = $this->db->query(
"SELECT data, valid"
." FROM ".$this->db->table_name('cache_index')
." WHERE user_id = ?"
." AND mailbox = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$index = @unserialize($data[0]);
unset($data[0]);
if (empty($index)) {
$index = new rcube_result_index($mailbox);
}
return array(
'valid' => $sql_arr['valid'],
'object' => $index,
'sort_field' => $data[1],
'deleted' => $data[2],
'validity' => $data[3],
'uidnext' => $data[4],
'modseq' => $data[5],
);
}
return null;
}
/**
* Fetches thread data from database
*/
private function get_thread_row($mailbox)
{
// Get thread from DB
$sql_result = $this->db->query(
"SELECT data"
." FROM ".$this->db->table_name('cache_thread')
." WHERE user_id = ?"
." AND mailbox = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$thread = @unserialize($data[0]);
unset($data[0]);
if (empty($thread)) {
$thread = new rcube_result_thread($mailbox);
}
return array(
'object' => $thread,
'deleted' => $data[1],
'validity' => $data[2],
'uidnext' => $data[3],
);
}
return null;
}
/**
* Saves index data into database
*/
private function add_index_row($mailbox, $sort_field,
$data, $mbox_data = array(), $exists = false, $modseq = null)
{
$data = array(
serialize($data),
$sort_field,
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
$modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
);
$data = implode('@', $data);
if ($exists) {
$sql_result = $this->db->query(
"UPDATE ".$this->db->table_name('cache_index')
." SET data = ?, valid = 1, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?",
$data, $this->userid, $mailbox);
}
else {
$sql_result = $this->db->query(
"INSERT INTO ".$this->db->table_name('cache_index')
." (user_id, mailbox, data, valid, changed)"
." VALUES (?, ?, ?, 1, ".$this->db->now().")",
$this->userid, $mailbox, $data);
}
}
/**
* Saves thread data into database
*/
private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
{
$data = array(
serialize($data),
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
);
$data = implode('@', $data);
if ($exists) {
$sql_result = $this->db->query(
"UPDATE ".$this->db->table_name('cache_thread')
." SET data = ?, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?",
$data, $this->userid, $mailbox);
}
else {
$sql_result = $this->db->query(
"INSERT INTO ".$this->db->table_name('cache_thread')
." (user_id, mailbox, data, changed)"
." VALUES (?, ?, ?, ".$this->db->now().")",
$this->userid, $mailbox, $data);
}
}
/**
* Checks index/thread validity
*/
private function validate($mailbox, $index, &$exists = true)
{
$object = $index['object'];
$is_thread = is_a($object, 'rcube_result_thread');
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
// @TODO: Think about skipping validation checks.
// If we could check only every 10 minutes, we would be able to skip
// expensive checks, mailbox selection or even IMAP connection, this would require
// additional logic to force cache invalidation in some cases
// and many rcube_imap changes to connect when needed
// Check UIDVALIDITY
if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
$this->clear($mailbox);
$exists = false;
return false;
}
// Folder is empty but cache isn't
if (empty($mbox_data['EXISTS'])) {
if (!$object->is_empty()) {
$this->clear($mailbox);
$exists = false;
return false;
}
}
// Folder is not empty but cache is
else if ($object->is_empty()) {
unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
return false;
}
// Validation flag
if (!$is_thread && empty($index['valid'])) {
unset($this->icache[$mailbox]['index']);
return false;
}
// Index was created with different skip_deleted setting
if ($this->skip_deleted != $index['deleted']) {
return false;
}
// Check HIGHESTMODSEQ
if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
&& $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
) {
return true;
}
// Check UIDNEXT
if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
return false;
}
// @TODO: find better validity check for threaded index
if ($is_thread) {
// check messages number...
if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->count_messages()) {
return false;
}
return true;
}
// The rest of checks, more expensive
if (!empty($this->skip_deleted)) {
// compare counts if available
if (!empty($mbox_data['UNDELETED'])
&& $mbox_data['UNDELETED']->count() != $object->count()
) {
return false;
}
// compare UID sets
if (!empty($mbox_data['UNDELETED'])) {
$uids_new = $mbox_data['UNDELETED']->get();
$uids_old = $object->get();
if (count($uids_new) != count($uids_old)) {
return false;
}
sort($uids_new, SORT_NUMERIC);
sort($uids_old, SORT_NUMERIC);
if ($uids_old != $uids_new)
return false;
}
else {
// get all undeleted messages excluding cached UIDs
$ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
rcube_imap_generic::compressMessageSet($object->get()));
if (!$ids->is_empty()) {
return false;
}
}
}
else {
// check messages number...
if ($mbox_data['EXISTS'] != $object->count()) {
return false;
}
// ... and max UID
if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) {
return false;
}
}
return true;
}
/**
* Synchronizes the mailbox.
*
* @param string $mailbox Folder name
*/
function synchronize($mailbox)
{
// RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
// RFC4551: IMAP Extension for Conditional STORE Operation
// or Quick Flag Changes Resynchronization
// RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
// @TODO: synchronize with other methods?
$qresync = $this->imap->get_capability('QRESYNC');
$condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
if (!$qresync && !$condstore) {
return;
}
// Get stored index
$index = $this->get_index_row($mailbox);
// database is empty
if (empty($index)) {
// set the flag that DB was already queried for index
// this way we'll be able to skip one SELECT in get_index()
$this->icache[$mailbox]['index_queried'] = true;
return;
}
$this->icache[$mailbox]['index'] = $index;
// no last HIGHESTMODSEQ value
if (empty($index['modseq'])) {
return;
}
if (!$this->imap->check_connection()) {
return;
}
// Enable QRESYNC
$res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
if ($res === false) {
return;
}
// Close mailbox if already selected to get most recent data
if ($this->imap->conn->selected == $mailbox) {
$this->imap->conn->close();
}
// Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
$mbox_data = $this->imap->folder_data($mailbox);
if (empty($mbox_data)) {
return;
}
// Check UIDVALIDITY
if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
$this->clear($mailbox);
return;
}
// QRESYNC not supported on specified mailbox
if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
return;
}
// Nothing new
if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
return;
}
$uids = array();
$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);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$uids[] = $sql_arr['uid'];
}
// Synchronize messages data
if (!empty($uids)) {
// Get modified flags and vanished messages
// UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
$result = $this->imap->conn->fetch($mailbox,
$uids, true, array('FLAGS'), $index['modseq'], $qresync);
if (!empty($result)) {
foreach ($result as $id => $msg) {
$uid = $msg->uid;
// Remove deleted message
if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
$removed[] = $uid;
// Invalidate index
$index['valid'] = false;
continue;
}
$flags = 0;
if (!empty($msg->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($msg->flags[$flag])) {
$flags += $idx;
}
}
}
$this->db->query(
"UPDATE ".$this->db->table_name('cache_messages')
." SET flags = ?, changed = ".$this->db->now()
." WHERE user_id = ?"
." AND mailbox = ?"
." AND uid = ?"
." AND flags <> ?",
$flags, $this->userid, $mailbox, $uid, $flags);
}
}
// VANISHED found?
if ($qresync) {
$mbox_data = $this->imap->folder_data($mailbox);
// Removed messages found
$uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
if (!empty($uids)) {
$removed = array_merge($removed, $uids);
// Invalidate index
$index['valid'] = false;
}
}
// remove messages from database
if (!empty($removed)) {
$this->remove_message($mailbox, $removed);
}
}
// Invalidate thread index (?)
if (!$index['valid']) {
$this->remove_thread($mailbox);
}
$sort_field = $index['sort_field'];
$sort_order = $index['object']->get_parameters('ORDER');
$exists = true;
// Validate index
if (!$this->validate($mailbox, $index, $exists)) {
// Update index
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
}
else {
$data = $index['object'];
}
// update index and/or HIGHESTMODSEQ value
$this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists);
// update internal cache for get_index()
$this->icache[$mailbox]['index']['object'] = $data;
}
/**
* Converts cache row into message object.
*
* @param array $sql_arr Message row data
*
* @return rcube_message_header Message object
*/
private function build_message($sql_arr)
{
$message = $this->db->decode(unserialize($sql_arr['data']));
if ($message) {
$message->flags = array();
foreach ($this->flags as $idx => $flag) {
if (($sql_arr['flags'] & $idx) == $idx) {
$message->flags[$flag] = true;
}
}
}
return $message;
}
/**
* Saves message stored in internal cache
*/
private function save_icache()
{
// Save current message from internal cache
if ($message = $this->icache['message']) {
// clean up some object's data
$object = $this->message_object_prepare($message['object']);
// calculate current md5 sum
$md5sum = md5(serialize($object));
if ($message['md5sum'] != $md5sum) {
$this->add_message($message['mailbox'], $object, !$message['exists']);
}
$this->icache['message']['md5sum'] = $md5sum;
}
}
/**
* Prepares message object to be stored in database.
*/
private function message_object_prepare($msg)
{
// Remove body too big (>25kB)
if ($msg->body && strlen($msg->body) > 25 * 1024) {
unset($msg->body);
}
// Fix mimetype which might be broken by some code when message is displayed
// Another solution would be to use object's copy in rcube_message class
// to prevent related issues, however I'm not sure which is better
if ($msg->mimetype) {
list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
}
if (is_array($msg->structure->parts)) {
foreach ($msg->structure->parts as $idx => $part) {
$msg->structure->parts[$idx] = $this->message_object_prepare($part);
}
}
return $msg;
}
/**
* Fetches index data from IMAP server
*/
private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
{
if (empty($mbox_data)) {
$mbox_data = $this->imap->folder_data($mailbox);
}
if ($mbox_data['EXISTS']) {
// fetch sorted sequence numbers
$index = $this->imap->index_direct($mailbox, $sort_field, $sort_order);
}
else {
$index = new rcube_result_index($mailbox, '* SORT');
}
return $index;
}
}
// for backward compat.
class rcube_mail_header extends rcube_message_header { }
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index 8f23a40..3a7fc18 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -1,2292 +1,2294 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_ldap.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2012, The Roundcube Dev Team |
| Copyright (C) 2011, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to an LDAP address directory |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Andreas Dick <andudi (at) gmx (dot) ch> |
| Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class to access an LDAP address directory
*
* @package Addressbook
*/
class rcube_ldap extends rcube_addressbook
{
/** public properties */
public $primary_key = 'ID';
public $groups = false;
public $readonly = true;
public $ready = false;
public $group_id = 0;
public $coltypes = array();
/** private properties */
protected $conn;
protected $prop = array();
protected $fieldmap = array();
protected $sub_filter;
protected $filter = '';
protected $result = null;
protected $ldap_result = null;
protected $mail_domain = '';
protected $debug = false;
private $base_dn = '';
private $groups_base_dn = '';
private $group_url = null;
private $cache;
private $vlv_active = false;
private $vlv_count = 0;
/**
* Object constructor
*
* @param array $p LDAP connection properties
* @param boolean $debug Enables debug mode
* @param string $mail_domain Current user mail domain name
*/
function __construct($p, $debug = false, $mail_domain = null)
{
$this->prop = $p;
if (isset($p['searchonly']))
$this->searchonly = $p['searchonly'];
// check if groups are configured
if (is_array($p['groups']) && count($p['groups'])) {
$this->groups = true;
// set member field
if (!empty($p['groups']['member_attr']))
$this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
else if (empty($p['member_attr']))
$this->prop['member_attr'] = 'member';
// set default name attribute to cn
if (empty($this->prop['groups']['name_attr']))
$this->prop['groups']['name_attr'] = 'cn';
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
}
// fieldmap property is given
if (is_array($p['fieldmap'])) {
foreach ($p['fieldmap'] as $rf => $lf)
$this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
}
else if (!empty($p)) {
// read deprecated *_field properties to remain backwards compatible
foreach ($p as $prop => $value)
if (preg_match('/^(.+)_field$/', $prop, $matches))
$this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
}
// use fieldmap to advertise supported coltypes to the application
foreach ($this->fieldmap as $colv => $lfv) {
list($col, $type) = explode(':', $colv);
list($lf, $limit, $delim) = explode(':', $lfv);
if ($limit == '*') $limit = null;
else $limit = max(1, intval($limit));
if (!is_array($this->coltypes[$col])) {
$subtypes = $type ? array($type) : null;
$this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes);
}
elseif ($type) {
$this->coltypes[$col]['subtypes'][] = $type;
$this->coltypes[$col]['limit'] += $limit;
}
if ($delim)
$this->coltypes[$col]['serialized'][$type] = $delim;
if ($type && !$this->fieldmap[$col])
$this->fieldmap[$col] = $lf;
$this->fieldmap[$colv] = $lf;
}
// support for composite address
if ($this->fieldmap['street'] && $this->fieldmap['locality']) {
$this->coltypes['address'] = array(
'limit' => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']),
'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']),
'childs' => array(),
) + (array)$this->coltypes['address'];
foreach (array('street','locality','zipcode','region','country') as $childcol) {
if ($this->fieldmap[$childcol]) {
$this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
unset($this->coltypes[$childcol]); // remove address child col from global coltypes list
}
}
}
else if ($this->coltypes['address']) {
$this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40);
// 'serialized' means the UI has to present a composite address field
if ($this->coltypes['address']['serialized']) {
$childprop = array('type' => 'text');
$this->coltypes['address']['type'] = 'composite';
$this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop);
}
}
// make sure 'required_fields' is an array
if (!is_array($this->prop['required_fields'])) {
$this->prop['required_fields'] = (array) $this->prop['required_fields'];
}
// make sure LDAP_rdn field is required
if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])) {
$this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
}
foreach ($this->prop['required_fields'] as $key => $val) {
$this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
}
// Build sub_fields filter
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
$this->sub_filter = '';
foreach ($this->prop['sub_fields'] as $attr => $class) {
if (!empty($class)) {
$class = is_array($class) ? array_pop($class) : $class;
$this->sub_filter .= '(objectClass=' . $class . ')';
}
}
if (count($this->prop['sub_fields']) > 1) {
$this->sub_filter = '(|' . $this->sub_filter . ')';
}
}
$this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
$this->debug = $debug;
$this->mail_domain = $mail_domain;
// initialize cache
$rcube = rcube::get_instance();
$this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
$this->_connect();
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
$rcube = rcube::get_instance();
if (!function_exists('ldap_connect'))
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No ldap support in this installation of PHP"),
true, true);
if (is_resource($this->conn))
return true;
if (!is_array($this->prop['hosts']))
$this->prop['hosts'] = array($this->prop['hosts']);
if (empty($this->prop['ldap_version']))
$this->prop['ldap_version'] = 3;
foreach ($this->prop['hosts'] as $host)
{
$host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
$hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
$this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
if ($lc = @ldap_connect($host, $this->prop['port']))
{
if ($this->prop['use_tls'] === true)
if (!ldap_start_tls($lc))
continue;
$this->_debug("S: OK");
ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
$this->prop['host'] = $host;
$this->conn = $lc;
if (isset($this->prop['referrals']))
ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
break;
}
$this->_debug("S: NOT OK");
}
// See if the directory is writeable.
if ($this->prop['writable']) {
$this->readonly = false;
}
if (!is_resource($this->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
return false;
}
$bind_pass = $this->prop['bind_pass'];
$bind_user = $this->prop['bind_user'];
$bind_dn = $this->prop['bind_dn'];
$this->base_dn = $this->prop['base_dn'];
$this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
$this->prop['groups']['base_dn'] : $this->base_dn;
// User specific access, generate the proper values to use.
if ($this->prop['user_specific']) {
// No password set, use the session password
if (empty($bind_pass)) {
$bind_pass = $rcube->decrypt($_SESSION['password']);
}
// Get the pieces needed for variable replacement.
if ($fu = $rcube->get_user_name())
list($u, $d) = explode('@', $fu);
else
$d = $this->mail_domain;
$dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
$this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
}
// Search for the dn to use to authenticate
$this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
$this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
$this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
$res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
if ($res) {
if (($entry = ldap_first_entry($this->conn, $res))
&& ($bind_dn = ldap_get_dn($this->conn, $entry))
) {
$this->_debug("S: search returned dn: $bind_dn");
$dn = ldap_explode_dn($bind_dn, 1);
$replaces['%dn'] = $dn[0];
}
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
// DN not found
if (empty($replaces['%dn'])) {
if (!empty($this->prop['search_dn_default']))
$replaces['%dn'] = $this->prop['search_dn_default'];
else {
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
return false;
}
}
}
// Replace the bind_dn and base_dn variables.
$bind_dn = strtr($bind_dn, $replaces);
$this->base_dn = strtr($this->base_dn, $replaces);
$this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
if (empty($bind_user)) {
$bind_user = $u;
}
}
if (empty($bind_pass)) {
$this->ready = true;
}
else {
if (!empty($bind_dn)) {
$this->ready = $this->bind($bind_dn, $bind_pass);
}
else if (!empty($this->prop['auth_cid'])) {
$this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
}
else {
$this->ready = $this->sasl_bind($bind_user, $bind_pass);
}
}
return $this->ready;
}
/**
* Bind connection with (SASL-) user and password
*
* @param string $authc Authentication user
* @param string $pass Bind password
* @param string $authz Autorization user
*
* @return boolean True on success, False on error
*/
public function sasl_bind($authc, $pass, $authz=null)
{
if (!$this->conn) {
return false;
}
if (!function_exists('ldap_sasl_bind')) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Unable to bind: ldap_sasl_bind() not exists"),
true, true);
}
if (!empty($authz)) {
$authz = 'u:' . $authz;
}
if (!empty($this->prop['auth_method'])) {
$method = $this->prop['auth_method'];
}
else {
$method = 'DIGEST-MD5';
}
$this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
$this->_debug("S: OK");
return true;
}
$this->_debug("S: ".ldap_error($this->conn));
rcube::raise_error(array(
'code' => ldap_errno($this->conn), 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
true);
return false;
}
/**
* Bind connection with DN and password
*
* @param string Bind DN
* @param string Bind password
*
* @return boolean True on success, False on error
*/
public function bind($dn, $pass)
{
if (!$this->conn) {
return false;
}
$this->_debug("C: Bind [dn: $dn] [pass: $pass]");
if (@ldap_bind($this->conn, $dn, $pass)) {
$this->_debug("S: OK");
return true;
}
$this->_debug("S: ".ldap_error($this->conn));
rcube::raise_error(array(
'code' => ldap_errno($this->conn), 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
true);
return false;
}
/**
* Close connection to LDAP server
*/
function close()
{
if ($this->conn)
{
$this->_debug("C: Close");
ldap_unbind($this->conn);
$this->conn = null;
}
}
/**
* Returns address book name
*
* @return string Address book name
*/
function get_name()
{
return $this->prop['name'];
}
/**
* Set internal sort settings
*
* @param string $sort_col Sort column
* @param string $sort_order Sort order
*/
function set_sort_order($sort_col, $sort_order = null)
{
if ($this->fieldmap[$sort_col])
$this->sort_col = $this->fieldmap[$sort_col];
}
/**
* Save a search string for future listings
*
* @param string $filter Filter string
*/
function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
function get_search_set()
{
return $this->filter;
}
/**
* Reset all saved results and search parameters
*/
function reset()
{
$this->result = null;
$this->ldap_result = null;
$this->filter = '';
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records
*
* @return array Indexed list of contact records, each a hash array
*/
function list_records($cols=null, $subset=0)
{
if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
{
$this->result = new rcube_result_set(0);
$this->result->searchonly = true;
return $this->result;
}
// fetch group members recursively
if ($this->group_id && $this->group_data['dn'])
{
$entries = $this->list_group_members($this->group_data['dn']);
// make list of entries unique and sort it
$seen = array();
foreach ($entries as $i => $rec) {
if ($seen[$rec['dn']]++)
unset($entries[$i]);
}
usort($entries, array($this, '_entry_sort_cmp'));
$entries['count'] = count($entries);
$this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
}
else
{
// add general filter to query
if (!empty($this->prop['filter']) && empty($this->filter))
$this->set_search_set($this->prop['filter']);
// exec LDAP search if no result resource is stored
if ($this->conn && !$this->ldap_result)
$this->_exec_search();
// count contacts for this user
$this->result = $this->count();
// we have a search result resource
if ($this->ldap_result && $this->result->count > 0)
{
// sorting still on the ldap server
if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
// get all entries from the ldap server
$entries = ldap_get_entries($this->conn, $this->ldap_result);
}
} // end else
// start and end of the page
$start_row = $this->vlv_active ? 0 : $this->result->first;
$start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
$last_row = $this->result->first + $this->page_size;
$last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
// filter entries for this page
for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
$this->result->add($this->_ldap2result($entries[$i]));
return $this->result;
}
/**
* Get all members of the given group
*
* @param string Group DN
* @param array Group entries (if called recursively)
* @return array Accumulated group members
*/
function list_group_members($dn, $count = false, $entries = null)
{
$group_members = array();
// fetch group object
if (empty($entries)) {
$result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
if ($result === false)
{
$this->_debug("S: ".ldap_error($this->conn));
return $group_members;
}
$entries = @ldap_get_entries($this->conn, $result);
}
for ($i=0; $i < $entries['count']; $i++)
{
$entry = $entries[$i];
if (empty($entry['objectclass']))
continue;
foreach ((array)$entry['objectclass'] as $objectclass)
{
switch (strtolower($objectclass)) {
case "group":
case "groupofnames":
case "kolabgroupofnames":
$group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
break;
case "groupofuniquenames":
case "kolabgroupofuniquenames":
$group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
break;
case "groupofurls":
$group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
break;
}
}
if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
break;
}
return array_filter($group_members);
}
/**
* Fetch members of the given group entry from server
*
* @param string Group DN
* @param array Group entry
* @param string Member attribute to use
* @return array Accumulated group members
*/
private function _list_group_members($dn, $entry, $attr, $count)
{
// Use the member attributes to return an array of member ldap objects
// NOTE that the member attribute is supposed to contain a DN
$group_members = array();
if (empty($entry[$attr]))
return $group_members;
// read these attributes for all members
$attrib = $count ? array('dn') : array_values($this->fieldmap);
$attrib[] = 'objectClass';
$attrib[] = 'member';
$attrib[] = 'uniqueMember';
$attrib[] = 'memberURL';
for ($i=0; $i < $entry[$attr]['count']; $i++)
{
if (empty($entry[$attr][$i]))
continue;
$result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
$attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
$members = @ldap_get_entries($this->conn, $result);
if ($members == false)
{
$this->_debug("S: ".ldap_error($this->conn));
$members = array();
}
// for nested groups, call recursively
$nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
unset($members['count']);
$group_members = array_merge($group_members, array_filter($members), $nested_group_members);
}
return $group_members;
}
/**
* List members of group class groupOfUrls
*
* @param string Group DN
* @param array Group entry
* @param boolean True if only used for counting
* @return array Accumulated group members
*/
private function _list_group_memberurl($dn, $entry, $count)
{
$group_members = array();
for ($i=0; $i < $entry['memberurl']['count']; $i++)
{
// extract components from url
if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
continue;
// add search filter if any
$filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
$func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
$attrib = $count ? array('dn') : array_values($this->fieldmap);
if ($result = @$func($this->conn, $m[1], $filter,
$attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
) {
$this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
}
else {
$this->_debug("S: ".ldap_error($this->conn));
return $group_members;
}
$entries = @ldap_get_entries($this->conn, $result);
for ($j = 0; $j < $entries['count']; $j++)
{
if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
$group_members = array_merge($group_members, $nested_group_members);
else
$group_members[] = $entries[$j];
}
}
return $group_members;
}
/**
* Callback for sorting entries
*/
function _entry_sort_cmp($a, $b)
{
return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
}
/**
* Search contacts
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount (Not used)
* @param array $required List of fields that cannot be empty
*
* @return array Indexed list of contact records and 'count' value
*/
function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
$mode = intval($mode);
// special treatment for ID-based search
if ($fields == 'ID' || $fields == $this->primary_key)
{
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id)
{
if ($rec = $this->get_record($id, true))
{
$result->add($rec);
$result->count++;
}
}
return $result;
}
// use VLV pseudo-search for autocompletion
- if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == 'email,name')
+ $rcmail = rcmail::get_instance();
+
+ if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $rcmail->config->get('contactlist_fields')))
{
// add general filter to query
if (!empty($this->prop['filter']) && empty($this->filter))
$this->set_search_set($this->prop['filter']);
// set VLV controls with encoded search string
$this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
$function = $this->_scope2func($this->prop['scope']);
$this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
$this->result = new rcube_result_set(0);
if (!$this->ldap_result) {
$this->_debug("S: ".ldap_error($this->conn));
return $this->result;
}
$this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
// get all entries of this page and post-filter those that really match the query
$search = mb_strtolower($value);
$entries = ldap_get_entries($this->conn, $this->ldap_result);
for ($i = 0; $i < $entries['count']; $i++) {
$rec = $this->_ldap2result($entries[$i]);
foreach (array('email', 'name') as $f) {
$val = mb_strtolower($rec[$f]);
switch ($mode) {
case 1:
$got = ($val == $search);
break;
case 2:
$got = ($search == substr($val, 0, strlen($search)));
break;
default:
$got = (strpos($val, $search) !== false);
break;
}
if ($got) {
$this->result->add($rec);
$this->result->count++;
break;
}
}
}
return $this->result;
}
// use AND operator for advanced searches
$filter = is_array($value) ? '(&' : '(|';
// set wildcards
$wp = $ws = '';
if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
$ws = '*';
if (!$mode) {
$wp = '*';
}
}
if ($fields == '*')
{
// search_fields are required for fulltext search
if (empty($this->prop['search_fields']))
{
$this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
$this->result = new rcube_result_set();
return $this->result;
}
if (is_array($this->prop['search_fields']))
{
foreach ($this->prop['search_fields'] as $field) {
$filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
}
}
}
else
{
foreach ((array)$fields as $idx => $field) {
$val = is_array($value) ? $value[$idx] : $value;
if ($f = $this->_map_field($field)) {
$filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
}
}
}
$filter .= ')';
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field)
if ($f = $this->_map_field($field))
$req_filter .= "($f=*)";
if (!empty($req_filter))
$filter = '(&' . $req_filter . $filter . ')';
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
// add general filter to query
if (!empty($this->prop['filter']))
$filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
// set filter string and execute search
$this->set_search_set($filter);
$this->_exec_search();
if ($select)
$this->list_records();
else
$this->result = $this->count();
return $this->result;
}
/**
* Count number of available contacts in database
*
* @return object rcube_result_set Resultset with values for 'count' and 'first'
*/
function count()
{
$count = 0;
if ($this->conn && $this->ldap_result) {
$count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
}
else if ($this->group_id && $this->group_data['dn']) {
$count = count($this->list_group_members($this->group_data['dn'], true));
}
else if ($this->conn) {
// We have a connection but no result set, attempt to get one.
if (empty($this->filter)) {
// The filter is not set, set it.
$this->filter = $this->prop['filter'];
}
$count = (int) $this->_exec_search(true);
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return object rcube_result_set Current resultset or NULL if nothing selected yet
*/
function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed Record identifier
* @param boolean Return as associative array
*
* @return mixed Hash array or rcube_result_set with all record fields
*/
function get_record($dn, $assoc=false)
{
$res = $this->result = null;
if ($this->conn && $dn)
{
$dn = self::dn_decode($dn);
$this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
$this->_debug("S: OK");
$entry = ldap_first_entry($this->conn, $ldap_result);
if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
$rec = array_change_key_case($rec, CASE_LOWER);
}
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
foreach ($entries as $entry) {
$lrec = array_change_key_case($entry, CASE_LOWER);
$rec = array_merge($lrec, $rec);
}
}
}
if (!empty($rec)) {
// Add in the dn for the entry.
$rec['dn'] = $dn;
$res = $this->_ldap2result($rec);
$this->result = new rcube_result_set();
$this->result->add($res);
}
}
return $assoc ? $res : $this->result;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Assoziative array with data to save
* @param boolean Try to fix/complete record automatically
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
if (!parent::validate($save_data, $autofix)) {
return false;
}
// check for name input
if (empty($save_data['name'])) {
$this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
return false;
}
// Verify that the required fields are set.
$missing = null;
$ldap_data = $this->_map_data($save_data);
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
$missing[$fld] = 1;
}
}
if ($missing) {
// try to complete record automatically
if ($autofix) {
$sn_field = $this->fieldmap['surname'];
$fn_field = $this->fieldmap['firstname'];
$mail_field = $this->fieldmap['email'];
// try to extract surname and firstname from displayname
$reverse_map = array_flip($this->fieldmap);
$name_parts = preg_split('/[\s,.]+/', $save_data['name']);
if ($sn_field && $missing[$sn_field]) {
$save_data['surname'] = array_pop($name_parts);
unset($missing[$sn_field]);
}
if ($fn_field && $missing[$fn_field]) {
$save_data['firstname'] = array_shift($name_parts);
unset($missing[$fn_field]);
}
// try to fix missing e-mail, very often on import
// from vCard we have email:other only defined
if ($mail_field && $missing[$mail_field]) {
$emails = $this->get_col_values('email', $save_data, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$save_data['email'] = $email;
unset($missing[$mail_field]);
}
}
}
// TODO: generate message saying which fields are missing
if (!empty($missing)) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
}
return true;
}
/**
* Create a new contact record
*
* @param array Hash array with save data
*
* @return encoded record ID on success, False on error
*/
function insert($save_cols)
{
// Map out the column names to their LDAP ones to build the new entry.
$newentry = $this->_map_data($save_cols);
$newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
// Verify that the required fields are set.
$missing = null;
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($newentry[$fld])) {
$missing[] = $fld;
}
}
// abort process if requiered fields are missing
// TODO: generate message saying which fields are missing
if ($missing) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
// Build the new entries DN.
$dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
// Remove attributes that need to be added separately (child objects)
$xfields = array();
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
foreach ($this->prop['sub_fields'] as $xf => $xclass) {
if (!empty($newentry[$xf])) {
$xfields[$xf] = $newentry[$xf];
unset($newentry[$xf]);
}
}
}
if (!$this->ldap_add($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
$xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
$this->ldap_add($xdn, $xf);
}
$dn = self::dn_encode($dn);
// add new contact to the selected group
if ($this->group_id)
$this->add_to_group($this->group_id, $dn);
return $dn;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Hash array with save data
*
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
$record = $this->get_record($id, true);
$newdata = array();
$replacedata = array();
$deletedata = array();
$subdata = array();
$subdeldata = array();
$subnewdata = array();
$ldap_data = $this->_map_data($save_cols);
$old_data = $record['_raw_attrib'];
// special handling of photo col
if ($photo_fld = $this->fieldmap['photo']) {
// undefined means keep old photo
if (!array_key_exists('photo', $save_cols)) {
$ldap_data[$photo_fld] = $record['photo'];
}
}
foreach ($this->fieldmap as $col => $fld) {
if ($fld) {
$val = $ldap_data[$fld];
$old = $old_data[$fld];
// remove empty array values
if (is_array($val))
$val = array_filter($val);
// $this->_map_data() result and _raw_attrib use different format
// make sure comparing array with one element with a string works as expected
if (is_array($old) && count($old) == 1 && !is_array($val)) {
$old = array_pop($old);
}
if (is_array($val) && count($val) == 1 && !is_array($old)) {
$val = array_pop($val);
}
// Subentries must be handled separately
if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
if ($old != $val) {
if ($old !== null) {
$subdeldata[$fld] = $old;
}
if ($val) {
$subnewdata[$fld] = $val;
}
}
else if ($old !== null) {
$subdata[$fld] = $old;
}
continue;
}
// The field does exist compare it to the ldap record.
if ($old != $val) {
// Changed, but find out how.
if ($old === null) {
// Field was not set prior, need to add it.
$newdata[$fld] = $val;
}
else if ($val == '') {
// Field supplied is empty, verify that it is not required.
if (!in_array($fld, $this->prop['required_fields'])) {
// ...It is not, safe to clear.
// #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
// jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
$deletedata[$fld] = array();
//$deletedata[$fld] = $old_data[$fld];
}
}
else {
// The data was modified, save it out.
$replacedata[$fld] = $val;
}
} // end if
} // end if
} // end foreach
// console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
$dn = self::dn_decode($id);
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
if (!$this->ldap_mod_del($dn, $deletedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
if (!empty($replacedata)) {
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
.$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->base_dn;
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
.$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
if (!$this->ldap_mod_replace($dn, $replacedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
} // end if
// RDN change, we need to remove all sub-entries
if (!empty($newrdn)) {
$subdeldata = array_merge($subdeldata, $subdata);
$subnewdata = array_merge($subnewdata, $subdata);
}
// remove sub-entries
if (!empty($subdeldata)) {
foreach ($subdeldata as $fld => $val) {
$subdn = $fld.'='.$this->_quote_string($val).','.$dn;
if (!$this->ldap_delete($subdn)) {
return false;
}
}
}
if (!empty($newdata)) {
// Add the fields.
if (!$this->ldap_mod_add($dn, $newdata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
// Handle RDN change
if (!empty($newrdn)) {
if (!$this->ldap_rename($dn, $newrdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$dn = self::dn_encode($dn);
$newdn = self::dn_encode($newdn);
// change the group membership of the contact
if ($this->groups) {
$group_ids = $this->get_record_groups($dn);
foreach ($group_ids as $group_id)
{
$this->remove_from_group($group_id, $dn);
$this->add_to_group($group_id, $newdn);
}
}
$dn = self::dn_decode($newdn);
}
// add sub-entries
if (!empty($subnewdata)) {
foreach ($subnewdata as $fld => $val) {
$subdn = $fld.'='.$this->_quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
$this->ldap_add($subdn, $xf);
}
}
return $newdn ? $newdn : true;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (unsupported)
*
* @return boolean True on success, False on error
*/
function delete($ids, $force=true)
{
if (!is_array($ids)) {
// Not an array, break apart the encoded DNs.
$ids = explode(',', $ids);
} // end if
foreach ($ids as $id) {
$dn = self::dn_decode($id);
// Need to delete all sub-entries first
if ($this->sub_filter) {
if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
if (!$this->ldap_delete($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
}
}
// Delete the record.
if (!$this->ldap_delete($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
// remove contact from all groups where he was member
if ($this->groups) {
$dn = self::dn_encode($dn);
$group_ids = $this->get_record_groups($dn);
foreach ($group_ids as $group_id) {
$this->remove_from_group($group_id, $dn);
}
}
} // end foreach
return count($ids);
}
/**
* Remove all contact records
*/
function delete_all()
{
//searching for contact entries
$dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
$dn_list[$idx] = self::dn_encode($entry['dn']);
}
$this->delete($dn_list);
}
}
/**
* Execute the LDAP search based on the stored credentials
*/
private function _exec_search($count = false)
{
if ($this->ready)
{
$filter = $this->filter ? $this->filter : '(objectclass=*)';
$function = $this->_scope2func($this->prop['scope'], $ns_function);
$this->_debug("C: Search [$filter][dn: $this->base_dn]");
// when using VLV, we get the total count by...
if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
// ...either reading numSubOrdinates attribute
if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
$counts = ldap_get_entries($this->conn, $result_count);
for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
$this->vlv_count += $counts[$j]['numsubordinates'][0];
$this->_debug("D: total numsubordinates = " . $this->vlv_count);
}
else if (!function_exists('ldap_parse_virtuallist_control')) // ...or by fetching all records dn and count them
$this->vlv_count = $this->_exec_search(true);
$this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
}
// only fetch dn for count (should keep the payload low)
$attrs = $count ? array('dn') : array_values($this->fieldmap);
if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
$attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
) {
// when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
if (ldap_parse_result($this->conn, $this->ldap_result,
$errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
) {
ldap_parse_virtuallist_control($this->conn, $serverctrls,
$last_offset, $this->vlv_count, $vresult);
$this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
}
else {
$this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
}
}
$entries_count = ldap_count_entries($this->conn, $this->ldap_result);
$this->_debug("S: $entries_count record(s)");
return $count ? $entries_count : true;
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
}
return false;
}
/**
* Choose the right PHP function according to scope property
*/
private function _scope2func($scope, &$ns_function = null)
{
switch ($scope) {
case 'sub':
$function = $ns_function = 'ldap_search';
break;
case 'base':
$function = $ns_function = 'ldap_read';
break;
default:
$function = 'ldap_list';
$ns_function = 'ldap_read';
break;
}
return $function;
}
/**
* Set server controls for Virtual List View (paginated listing)
*/
private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
{
$sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort']));
$vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
$sort = (array)$prop['sort'];
$this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
. " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
$this->_debug("S: ".ldap_error($this->conn));
$this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
return false;
}
return true;
}
/**
* Converts LDAP entry into an array
*/
private function _ldap2result($rec)
{
$out = array();
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
foreach ($this->fieldmap as $rf => $lf)
{
for ($i=0; $i < $rec[$lf]['count']; $i++) {
if (!($value = $rec[$lf][$i]))
continue;
list($col, $subtype) = explode(':', $rf);
$out['_raw_attrib'][$lf][$i] = $value;
if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
$out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
else if (in_array($col, array('street','zipcode','locality','country','region')))
$out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
else if ($col == 'address' && strpos($value, '$') !== false) // address data is represented as string separated with $
list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $value);
else if ($rec[$lf]['count'] > 1)
$out[$rf][] = $value;
else
$out[$rf] = $value;
}
// Make sure name fields aren't arrays (#1488108)
if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
$out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
}
}
return $out;
}
/**
* Return real field name (from fields map)
*/
private function _map_field($field)
{
return $this->fieldmap[$field];
}
/**
* Convert a record data set into LDAP field attributes
*/
private function _map_data($save_cols)
{
// flatten composite fields first
foreach ($this->coltypes as $col => $colprop) {
if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
foreach ($values as $subtype => $childs) {
$subtype = $subtype ? ':'.$subtype : '';
foreach ($childs as $i => $child_values) {
foreach ((array)$child_values as $childcol => $value) {
$save_cols[$childcol.$subtype][$i] = $value;
}
}
}
}
// if addresses are to be saved as serialized string, do so
if (is_array($colprop['serialized'])) {
foreach ($colprop['serialized'] as $subtype => $delim) {
$key = $col.':'.$subtype;
foreach ((array)$save_cols[$key] as $i => $val)
$save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country']));
}
}
}
$ldap_data = array();
foreach ($this->fieldmap as $col => $fld) {
$val = $save_cols[$col];
if (is_array($val))
$val = array_filter($val); // remove empty entries
if ($fld && $val) {
// The field does exist, add it to the entry.
$ldap_data[$fld] = $val;
}
}
return $ldap_data;
}
/**
* Returns unified attribute name (resolving aliases)
*/
private static function _attr_name($namev)
{
// list of known attribute aliases
static $aliases = array(
'gn' => 'givenname',
'rfc822mailbox' => 'email',
'userid' => 'uid',
'emailaddress' => 'email',
'pkcs9email' => 'email',
);
list($name, $limit) = explode(':', $namev, 2);
$suffix = $limit ? ':'.$limit : '';
return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
}
/**
* Prints debug info to the log
*/
private function _debug($str)
{
if ($this->debug) {
rcube::write_log('ldap', $str);
}
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if LDAP commands should be logged
* @access public
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
}
/**
* Quotes attribute value string
*
* @param string $str Attribute value
* @param bool $dn True if the attribute is a DN
*
* @return string Quoted string
*/
private static function _quote_string($str, $dn=false)
{
// take firt entry if array given
if (is_array($str))
$str = reset($str);
if ($dn)
$replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
'>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
else
$replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
'/'=>'\2f');
return strtr($str, $replace);
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($group_id)
{
if ($group_id)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$this->group_id = $group_id;
$this->group_data = $group_cache[$group_id];
}
else
{
$this->group_id = 0;
$this->group_data = null;
}
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
if (!$this->groups)
return array();
// use cached list for searching
$this->cache->expunge();
if (!$search || ($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$groups = array();
if ($search) {
$search = mb_strtolower($search);
foreach ($group_cache as $group) {
if (strpos(mb_strtolower($group['name']), $search) !== false)
$groups[] = $group;
}
}
else
$groups = $group_cache;
return array_values($groups);
}
/**
* Fetch groups from server
*/
private function _fetch_groups($vlv_page = 0)
{
$base_dn = $this->groups_base_dn;
$filter = $this->prop['groups']['filter'];
$name_attr = $this->prop['groups']['name_attr'];
$email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
$sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
$sort_attr = $sort_attrs[0];
$this->_debug("C: Search [$filter][dn: $base_dn]");
// use vlv to list groups
if ($this->prop['groups']['vlv']) {
$page_size = 200;
if (!$this->prop['groups']['sort'])
$this->prop['groups']['sort'] = $sort_attrs;
$vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
}
$function = $this->_scope2func($this->prop['groups']['scope'], $ns_function);
$res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
if ($res === false)
{
$this->_debug("S: ".ldap_error($this->conn));
return array();
}
$ldap_data = ldap_get_entries($this->conn, $res);
$this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
$groups = array();
$group_sortnames = array();
$group_count = $ldap_data["count"];
for ($i=0; $i < $group_count; $i++)
{
$group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
$group_id = self::dn_encode($group_name);
$groups[$group_id]['ID'] = $group_id;
$groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
$groups[$group_id]['name'] = $group_name;
$groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
// list email attributes of a group
for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
$groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
}
$group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
}
// recursive call can exit here
if ($vlv_page > 0)
return $groups;
// call recursively until we have fetched all groups
while ($vlv_active && $group_count == $page_size)
{
$next_page = $this->_fetch_groups(++$vlv_page);
$groups = array_merge($groups, $next_page);
$group_count = count($next_page);
}
// when using VLV the list of groups is already sorted
if (!$this->prop['groups']['vlv'])
array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
// cache this
$this->cache->set('groups', $groups);
return $groups;
}
/**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
* @return array Group properties as hash array
*/
function get_group($group_id)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$group_data = $group_cache[$group_id];
unset($group_data['dn'], $group_data['member_attr']);
return $group_data;
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($group_name)
{
$base_dn = $this->groups_base_dn;
$new_dn = "cn=$group_name,$base_dn";
$new_gid = self::dn_encode($group_name);
$member_attr = $this->get_group_member_attr();
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
$new_entry = array(
'objectClass' => $this->prop['groups']['object_classes'],
$name_attr => $group_name,
$member_attr => '',
);
if (!$this->ldap_add($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->cache->remove('groups');
return array('id' => $new_gid, 'name' => $group_name);
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($group_id)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$del_dn = "cn=$group_name,$base_dn";
if (!$this->ldap_delete($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->cache->remove('groups');
return true;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
* @return boolean New name on success, false if no data was changed
*/
function rename_group($group_id, $new_name, &$new_gid)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$old_dn = "cn=$group_name,$base_dn";
$new_rdn = "cn=$new_name";
$new_gid = self::dn_encode($new_name);
if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->cache->remove('groups');
return $new_name;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($group_id, $contact_ids)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = "cn=$group_name,$base_dn";
$new_attrs = array();
foreach ($contact_ids as $id)
$new_attrs[$member_attr][] = self::dn_decode($id);
if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
$this->cache->remove('groups');
return count($new_attrs['member']);
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $contact_ids)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = "cn=$group_name,$base_dn";
$del_attrs = array();
foreach (explode(",", $contact_ids) as $id)
$del_attrs[$member_attr][] = self::dn_decode($id);
if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
$this->cache->remove('groups');
return count($del_attrs['member']);
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array List of assigned groups as ID=>Name pairs
* @since 0.5-beta
*/
function get_record_groups($contact_id)
{
if (!$this->groups)
return array();
$base_dn = $this->groups_base_dn;
$contact_dn = self::dn_decode($contact_id);
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
$member_attr = $this->get_group_member_attr();
$add_filter = '';
if ($member_attr != 'member' && $member_attr != 'uniqueMember')
$add_filter = "($member_attr=$contact_dn)";
$filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
$this->_debug("C: Search [$filter][dn: $base_dn]");
$res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
if ($res === false)
{
$this->_debug("S: ".ldap_error($this->conn));
return array();
}
$ldap_data = ldap_get_entries($this->conn, $res);
$this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
$groups = array();
for ($i=0; $i<$ldap_data["count"]; $i++)
{
$group_name = $ldap_data[$i][$name_attr][0];
$group_id = self::dn_encode($group_name);
$groups[$group_id] = $group_id;
}
return $groups;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array())
{
if (empty($object_classes)) {
$object_classes = $this->prop['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
switch (strtolower($oc)) {
case 'group':
case 'groupofnames':
case 'kolabgroupofnames':
$member_attr = 'member';
break;
case 'groupofuniquenames':
case 'kolabgroupofuniquenames':
$member_attr = 'uniqueMember';
break;
}
}
}
if (!empty($member_attr)) {
return $member_attr;
}
if (!empty($this->prop['groups']['member_attr'])) {
return $this->prop['groups']['member_attr'];
}
return 'member';
}
/**
* Generate BER encoded string for Virtual List View option
*
* @param integer List offset (first record)
* @param integer Records per page
* @return string BER encoded option value
*/
private function _vlv_ber_encode($offset, $rpp, $search = '')
{
# this string is ber-encoded, php will prefix this value with:
# 04 (octet string) and 10 (length of 16 bytes)
# the code behind this string is broken down as follows:
# 30 = ber sequence with a length of 0e (14) bytes following
# 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
# 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
# a0 = type context-specific/constructed with a length of 06 (6) bytes following
# 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
# 02 = type integer with 2 bytes following (contentCount): 01 00
# whith a search string present:
# 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
# 81 indicates a user string is present where as a a0 indicates just a offset search
# 81 = type context-specific/constructed with a length of 06 (6) bytes following
# the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
# encoding of integer values (note: these values are in
# two-complement form so since offset will never be negative bit 8 of the
# leftmost octet should never by set to 1):
# 8.3.2: If the contents octets of an integer value encoding consist
# of more than one octet, then the bits of the first octet (rightmost) and bit 8
# of the second (to the left of first octet) octet:
# a) shall not all be ones; and
# b) shall not all be zero
if ($search)
{
$search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
$ber_val = self::_string2hex($search);
$str = self::_ber_addseq($ber_val, '81');
}
else
{
# construct the string from right to left
$str = "020100"; # contentCount
$ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format
// calculate octet length of $ber_val
$str = self::_ber_addseq($ber_val, '02') . $str;
// now compute length over $str
$str = self::_ber_addseq($str, 'a0');
}
// now tack on records per page
$str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
// now tack on sequence identifier and length
$str = self::_ber_addseq($str, '30');
return pack('H'.strlen($str), $str);
}
/**
* create ber encoding for sort control
*
* @param array List of cols to sort by
* @return string BER encoded option value
*/
private function _sort_ber_encode($sortcols)
{
$str = '';
foreach (array_reverse((array)$sortcols) as $col) {
$ber_val = self::_string2hex($col);
# 30 = ber sequence with a length of octet value
# 04 = octet string with a length of the ascii value
$oct = self::_ber_addseq($ber_val, '04');
$str = self::_ber_addseq($oct, '30') . $str;
}
// now tack on sequence identifier and length
$str = self::_ber_addseq($str, '30');
return pack('H'.strlen($str), $str);
}
/**
* Add BER sequence with correct length and the given identifier
*/
private static function _ber_addseq($str, $identifier)
{
$len = dechex(strlen($str)/2);
if (strlen($len) % 2 != 0)
$len = '0'.$len;
return $identifier . $len . $str;
}
/**
* Returns BER encoded integer value in hex format
*/
private static function _ber_encode_int($offset)
{
$val = dechex($offset);
$prefix = '';
// check if bit 8 of high byte is 1
if (preg_match('/^[89abcdef]/', $val))
$prefix = '00';
if (strlen($val)%2 != 0)
$prefix .= '0';
return $prefix . $val;
}
/**
* Returns ascii string encoded in hex
*/
private static function _string2hex($str)
{
$hex = '';
for ($i=0; $i < strlen($str); $i++)
$hex .= dechex(ord($str[$i]));
return $hex;
}
/**
* HTML-safe DN string encoding
*
* @param string $str DN string
*
* @return string Encoded HTML identifier string
*/
static function dn_encode($str)
{
// @TODO: to make output string shorter we could probably
// remove dc=* items from it
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
/**
* Decodes DN string encoded with _dn_encode()
*
* @param string $str Encoded HTML identifier string
*
* @return string DN string
*/
static function dn_decode($str)
{
$str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
return base64_decode($str);
}
/**
* Wrapper for ldap_add()
*/
protected function ldap_add($dn, $entry)
{
$this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
$res = ldap_add($this->conn, $dn, $entry);
if ($res === false) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_delete()
*/
protected function ldap_delete($dn)
{
$this->_debug("C: Delete [dn: $dn]");
$res = ldap_delete($this->conn, $dn);
if ($res === false) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_replace()
*/
protected function ldap_mod_replace($dn, $entry)
{
$this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
if (!ldap_mod_replace($this->conn, $dn, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_add()
*/
protected function ldap_mod_add($dn, $entry)
{
$this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
if (!ldap_mod_add($this->conn, $dn, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_del()
*/
protected function ldap_mod_del($dn, $entry)
{
$this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
if (!ldap_mod_del($this->conn, $dn, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_rename()
*/
protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
{
$this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_list()
*/
protected function ldap_list($dn, $filter, $attrs = array(''))
{
$list = array();
$this->_debug("C: List [dn: $dn] [{$filter}]");
if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
$list = ldap_get_entries($this->conn, $result);
if ($list === false) {
$this->_debug("S: ".ldap_error($this->conn));
return array();
}
$count = $list['count'];
unset($list['count']);
$this->_debug("S: $count record(s)");
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
return $list;
}
}
diff --git a/lib/ext/Roundcube/rcube_output_html.php b/lib/ext/Roundcube/rcube_output_html.php
index 30201fd..3bc93db 100644
--- a/lib/ext/Roundcube/rcube_output_html.php
+++ b/lib/ext/Roundcube/rcube_output_html.php
@@ -1,1622 +1,1635 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcubeoutput_html.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to handle HTML page output using a skin template. |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create HTML page output using a skin template
*
* @package View
*/
class rcube_output_html extends rcube_output
{
public $type = 'html';
protected $message = null;
protected $js_env = array();
protected $js_labels = array();
protected $js_commands = array();
protected $plugin_skin_path;
protected $template_name;
protected $scripts_path = '';
protected $script_files = array();
protected $css_files = array();
protected $scripts = array();
protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
protected $header = '';
protected $footer = '';
protected $body = '';
protected $base_path = '';
// deprecated names of templates used before 0.5
protected $deprecated_templates = array(
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
);
/**
* Constructor
*
* @todo Replace $this->config with the real rcube_config object
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
//$this->framed = $framed;
$this->set_env('task', $task);
$this->set_env('x_frame_options', $this->config->get('x_frame_options', 'sameorigin'));
// load the correct skin (in case user-defined)
$skin = $this->config->get('skin');
$this->set_skin($skin);
$this->set_env('skin', $skin);
// add common javascripts
$this->add_script('var '.rcmail::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
// don't wait for page onload. Call init at the bottom of the page (delayed)
$this->add_script(rcmail::JS_OBJECT_NAME.'.init();', 'docready');
$this->scripts_path = 'program/js/';
$this->include_script('jquery.min.js');
$this->include_script('common.js');
$this->include_script('app.js');
// register common UI objects
$this->add_handlers(array(
'loginform' => array($this, 'login_form'),
'preloader' => array($this, 'preloader'),
'username' => array($this, 'current_username'),
'message' => array($this, 'message_container'),
'charsetselector' => array($this, 'charset_selector'),
'aboutcontent' => array($this, 'about_content'),
));
}
/**
* Set environment variable
*
* @param string Property name
* @param mixed Property value
* @param boolean True if this property should be added to client environment
*/
public function set_env($name, $value, $addtojs = true)
{
$this->env[$name] = $value;
if ($addtojs || isset($this->js_env[$name])) {
$this->js_env[$name] = $value;
}
}
/**
* Getter for the current page title
*
* @return string The page title
*/
protected function get_pagetitle()
{
if (!empty($this->pagetitle)) {
$title = $this->pagetitle;
}
else if ($this->env['task'] == 'login') {
$title = $this->app->gettext(array(
'name' => 'welcome',
'vars' => array('product' => $this->config->get('product_name')
)));
}
else {
$title = ucfirst($this->env['task']);
}
return $title;
}
/**
* Set skin
*/
public function set_skin($skin)
{
$valid = false;
if (!empty($skin) && is_dir('skins/'.$skin) && is_readable('skins/'.$skin)) {
$skin_path = 'skins/'.$skin;
$valid = true;
}
else {
$skin_path = $this->config->get('skin_path');
if (!$skin_path) {
$skin_path = 'skins/default';
}
$valid = !$skin;
}
$this->config->set('skin_path', $skin_path);
return $valid;
}
/**
* Check if a specific template exists
*
* @param string Template name
* @return boolean True if template exists
*/
public function template_exists($name)
{
$filename = $this->config->get('skin_path') . '/templates/' . $name . '.html';
return (is_file($filename) && is_readable($filename)) || ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name]));
}
/**
* Register a GUI object to the client script
*
* @param string Object name
* @param string Object ID
* @return void
*/
public function add_gui_object($obj, $id)
{
$this->add_script(rcmail::JS_OBJECT_NAME.".gui_object('$obj', '$id');");
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
public function command()
{
$cmd = func_get_args();
if (strpos($cmd[0], 'plugin.') !== false)
$this->js_commands[] = array('triggerEvent', $cmd[0], $cmd[1]);
else
$this->js_commands[] = $cmd;
}
/**
* Add a localized label to the client environment
*/
public function add_label()
{
$args = func_get_args();
if (count($args) == 1 && is_array($args[0]))
$args = $args[0];
foreach ($args as $name) {
$this->js_labels[$name] = $this->app->gettext($name);
}
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message display time in seconds
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
{
if ($override || !$this->message) {
if ($this->app->text_exists($message)) {
if (!empty($vars))
$vars = array_map('Q', $vars);
$msgtext = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
else
$msgtext = $message;
$this->message = $message;
$this->command('display_message', $msgtext, $type, $timeout * 1000);
}
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
parent::reset();
$this->js_env = array();
$this->js_labels = array();
$this->js_commands = array();
$this->script_files = array();
$this->scripts = array();
$this->header = '';
$this->footer = '';
$this->body = '';
}
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
*/
public function redirect($p = array(), $delay = 1)
{
$location = $this->app->url($p);
header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin tempalte or send an AJAX response
*
* @param string Template name
* @param boolean True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
if ($templ != 'iframe') {
// prevent from endless loops
if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {
rcube::raise_error(array('code' => 505, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => 'Recursion alert: ignoring output->send()'), true, false);
return;
}
$this->parse($templ, false);
}
else {
$this->framed = $templ == 'iframe' ? true : $this->framed;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
// unlock interface after iframe load
$unlock = preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']);
if ($this->framed) {
array_unshift($this->js_commands, array('set_busy', false, null, $unlock));
}
else if ($unlock) {
array_unshift($this->js_commands, array('hide_message', $unlock));
}
if (!empty($this->script_files))
$this->set_env('request_token', $this->app->get_request_token());
// write all env variables to client
$js = $this->framed ? "if(window.parent) {\n" : '';
$js .= $this->get_js_commands() . ($this->framed ? ' }' : '');
$this->add_script($js, 'head_top');
// send clickjacking protection headers
$iframe = $this->framed || !empty($_REQUEST['_framed']);
if (!headers_sent() && ($xframe = $this->app->config->get('x_frame_options', 'sameorigin')))
header('X-Frame-Options: ' . ($iframe && $xframe == 'deny' ? 'sameorigin' : $xframe));
// call super method
$this->_write($template, $this->config->get('skin_path'));
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string Template name
* @param boolean Exit script
* @param boolean Don't write to stdout, return parsed content instead
*
* @link http://php.net/manual/en/function.exit.php
*/
function parse($name = 'main', $exit = true, $write = true)
{
$skin_path = $this->config->get('skin_path');
$plugin = false;
$realname = $name;
$temp = explode('.', $name, 2);
$this->plugin_skin_path = null;
$this->template_name = $realname;
if (count($temp) > 1) {
$plugin = $temp[0];
$name = $temp[1];
$skin_dir = $plugin . '/skins/' . $this->config->get('skin');
$skin_path = $this->plugin_skin_path = $this->app->plugins->dir . $skin_dir;
// fallback to default skin
if (!is_dir($skin_path)) {
$skin_dir = $plugin . '/skins/default';
$skin_path = $this->plugin_skin_path = $this->app->plugins->dir . $skin_dir;
}
}
$path = "$skin_path/templates/$name.html";
if (!is_readable($path) && $this->deprecated_templates[$realname]) {
$path = "$skin_path/templates/".$this->deprecated_templates[$realname].".html";
if (is_readable($path))
rcube::raise_error(array('code' => 502, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Using deprecated template '".$this->deprecated_templates[$realname]
."' in $skin_path/templates. Please rename to '".$realname."'"),
true, false);
}
// read template file
if (($templ = @file_get_contents($path)) === false) {
rcube::raise_error(array(
'code' => 501,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$realname
), true, true);
return false;
}
// replace all path references to plugins/... with the configured plugins dir
// and /this/ to the current plugin skin directory
if ($plugin) {
$templ = preg_replace(array('/\bplugins\//', '/(["\']?)\/this\//'), array($this->app->plugins->url, '\\1'.$this->app->plugins->url.$skin_dir.'/'), $templ);
}
// parse for specialtags
$output = $this->parse_conditions($templ);
$output = $this->parse_xml($output);
// trigger generic hook where plugins can put additional content to the page
$hook = $this->app->plugins->exec_hook("render_page", array('template' => $realname, 'content' => $output));
// save some memory
$output = $hook['content'];
unset($hook['content']);
$output = $this->parse_with_globals($output);
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
$this->footer = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $this->footer);
if ($write) {
// add debug console
if ($realname != 'error' && ($this->config->get('debug_level') & 8)) {
$this->add_footer('<div id="console" style="position:absolute;top:5px;left:5px;width:405px;padding:2px;background:white;z-index:9000;display:none">
<a href="#toggle" onclick="con=$(\'#dbgconsole\');con[con.is(\':visible\')?\'hide\':\'show\']();return false">console</a>
<textarea name="console" id="dbgconsole" rows="20" cols="40" style="display:none;width:400px;border:none;font-size:10px" spellcheck="false"></textarea></div>'
);
$this->add_script(
"if (!window.console || !window.console.log) {\n".
" window.console = new rcube_console();\n".
" $('#console').show();\n".
"}", 'foot');
}
$this->write(trim($output));
}
else {
return $output;
}
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*
* @return string $out
*/
protected function get_js_commands()
{
$out = '';
if (!$this->framed && !empty($this->js_env)) {
$out .= rcmail::JS_OBJECT_NAME . '.set_env('.self::json_serialize($this->js_env).");\n";
}
if (!empty($this->js_labels)) {
$this->command('add_label', $this->js_labels);
}
foreach ($this->js_commands as $i => $args) {
$method = array_shift($args);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg);
}
$parent = $this->framed || preg_match('/^parent\./', $method);
$out .= sprintf(
"%s.%s(%s);\n",
($parent ? 'if(window.parent && parent.'.rcmail::JS_OBJECT_NAME.') parent.' : '') . rcmail::JS_OBJECT_NAME,
preg_replace('/^parent\./', '', $method),
implode(',', $args)
);
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string Input string
* @return string
*/
public function abs_url($str)
{
if ($str[0] == '/')
return $this->config->get('skin_path') . $str;
else
return $str;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
global $__page_content, $ERROR_CODE, $ERROR_MESSAGE;
$ERROR_CODE = $code;
$ERROR_MESSAGE = $message;
include INSTALL_PATH . 'program/steps/utils/error.inc';
exit;
}
/***** Template parsing methods *****/
/**
* Replace all strings ($varname)
* with the content of the according global variable.
*/
protected function parse_with_globals($input)
{
$GLOBALS['__version'] = html::quote(RCMAIL_VERSION);
$GLOBALS['__comm_path'] = html::quote($this->app->comm_path);
$GLOBALS['__skin_path'] = Q($this->config->get('skin_path'));
return preg_replace_callback('/\$(__[a-z0-9_\-]+)/',
array($this, 'globals_callback'), $input);
}
/**
* Callback funtion for preg_replace_callback() in parse_with_globals()
*/
protected function globals_callback($matches)
{
return $GLOBALS[$matches[1]];
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input
* @return string
* @uses rcube_output_html::parse_xml()
* @since 0.1-rc1
*/
public function just_parse($input)
{
return $this->parse_xml($input);
}
/**
* Parse for conditional tags
*
* @param string $input
* @return string
*/
protected function parse_conditions($input)
{
$matches = preg_split('/<roundcube:(if|elseif|else|endif)\s+([^>]+)>\n?/is', $input, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($matches && count($matches) == 4) {
if (preg_match('/^(else|endif)$/i', $matches[1])) {
return $matches[0] . $this->parse_conditions($matches[3]);
}
$attrib = html::parse_attrib_string($matches[2]);
if (isset($attrib['condition'])) {
$condmet = $this->check_condition($attrib['condition']);
$submatches = preg_split('/<roundcube:(elseif|else|endif)\s+([^>]+)>\n?/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE);
if ($condmet) {
$result = $submatches[0];
$result.= ($submatches[1] != 'endif' ? preg_replace('/.*<roundcube:endif\s+[^>]+>\n?/Uis', '', $submatches[3], 1) : $submatches[3]);
}
else {
$result = "<roundcube:$submatches[1] $submatches[2]>" . $submatches[3];
}
return $matches[0] . $this->parse_conditions($result);
}
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @todo Get rid off eval() once I understand what this does.
* @todo Extend this to allow real conditions, not just "set"
* @param string Condition statement
* @return boolean True if condition is met, False if not
*/
protected function check_condition($condition)
{
return eval("return (".$this->parse_expression($condition).");");
}
/**
* Inserts hidden field with CSRF-prevention-token into POST forms
*/
protected function alter_form_tag($matches)
{
$out = $matches[0];
$attrib = html::parse_attrib_string($matches[1]);
if (strtolower($attrib['method']) == 'post') {
$hidden = new html_hiddenfield(array('name' => '_token', 'value' => $this->app->get_request_token()));
$out .= "\n" . $hidden->show();
}
return $out;
}
/**
* Parses expression and replaces variables
*
* @param string Expression statement
* @return string Expression value
*/
protected function parse_expression($expression)
{
return preg_replace(
array(
'/session:([a-z0-9_]+)/i',
'/config:([a-z0-9_]+)(:([a-z0-9_]+))?/i',
'/env:([a-z0-9_]+)/i',
'/request:([a-z0-9_]+)/i',
'/cookie:([a-z0-9_]+)/i',
'/browser:([a-z0-9_]+)/i',
'/template:name/i',
),
array(
"\$_SESSION['\\1']",
"\$this->app->config->get('\\1',get_boolean('\\3'))",
"\$this->env['\\1']",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"\$_COOKIE['\\1']",
"\$this->browser->{'\\1'}",
$this->template_name,
),
$expression);
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string Input string to parse
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
protected function parse_xml($input)
{
return preg_replace_callback('/<roundcube:([-_a-z]+)\s+((?:[^>]|\\\\>)+)(?<!\\\\)>/Ui', array($this, 'xml_command'), $input);
}
/**
* Callback function for parsing an xml command tag
* and turn it into real html content
*
* @param array Matches array of preg_replace_callback
* @return string Tag/Object content
*/
protected function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = html::parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// execute command
switch ($command) {
// return a button
case 'button':
if ($attrib['name'] || $attrib['command']) {
return $this->button($attrib);
}
break;
// show a label
case 'label':
if ($attrib['name'] || $attrib['command']) {
+ // @FIXME: 'noshow' is useless, remove?
+ if ($attrib['noshow']) {
+ return '';
+ }
+
$vars = $attrib + array('product' => $this->config->get('product_name'));
unset($vars['name'], $vars['command']);
- $label = $this->app->gettext($attrib + array('vars' => $vars));
+
+ $label = $this->app->gettext($attrib + array('vars' => $vars));
$quoting = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (get_boolean((string)$attrib['html']) ? 'no' : '');
+
switch ($quoting) {
case 'no':
- case 'raw': break;
+ case 'raw':
+ break;
case 'javascript':
- case 'js': $label = rcmail::JQ($label); break;
- default: $label = html::quote($label); break;
+ case 'js':
+ $label = rcmail::JQ($label);
+ break;
+ default:
+ $label = html::quote($label);
+ break;
}
- return !$attrib['noshow'] ? $label : '';
+
+ return $label;
}
break;
// include a file
case 'include':
if (!$this->plugin_skin_path || !is_file($path = realpath($this->plugin_skin_path . $attrib['file'])))
$path = realpath(($attrib['skin_path'] ? $attrib['skin_path'] : $this->config->get('skin_path')).$attrib['file']);
if (is_readable($path)) {
if ($this->config->get('skin_include_php')) {
$incl = $this->include_php($path);
}
else {
$incl = file_get_contents($path);
}
$incl = $this->parse_conditions($incl);
return $this->parse_xml($incl);
}
break;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib);
return $hook['content'];
break;
// define a container block
case 'container':
if ($attrib['name'] && $attrib['id']) {
$this->command('gui_container', $attrib['name'], $attrib['id']);
// let plugins insert some content here
$hook = $this->app->plugins->exec_hook("template_container", $attrib);
return $hook['content'];
}
break;
// return code for a specific application object
case 'object':
$object = strtolower($attrib['name']);
$content = '';
// we are calling a class/method
if (($handler = $this->object_handlers[$object]) && is_array($handler)) {
if ((is_object($handler[0]) && method_exists($handler[0], $handler[1])) ||
(is_string($handler[0]) && class_exists($handler[0])))
$content = call_user_func($handler, $attrib);
}
// execute object handler function
else if (function_exists($handler)) {
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"')));
if ($logo = $this->config->get('skin_logo'))
$attrib['src'] = $logo;
$content = html::img($attrib);
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'pagetitle') {
if ($this->config->get('devel_mode') && !empty($_SESSION['username']))
$title = $_SESSION['username'].' :: ';
else if ($prod_name = $this->config->get('product_name'))
$title = $prod_name . ' :: ';
else
$title = '';
$title .= $this->get_pagetitle();
$content = html::quote($title);
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
return $hook['content'];
// return code for a specified eval expression
case 'exp':
$value = $this->parse_expression($attrib['expression']);
return eval("return html::quote($value);");
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$name = $var[1];
$value = '';
switch ($var[0]) {
case 'env':
$value = $this->env[$name];
break;
case 'config':
$value = $this->config->get($name);
if (is_array($value) && $value[$_SESSION['storage_host']]) {
$value = $value[$_SESSION['storage_host']];
}
break;
case 'request':
$value = rcube_utils::get_input_value($name, rcube_utils::INPUT_GPC);
break;
case 'session':
$value = $_SESSION[$name];
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name]);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
break;
}
return '';
}
/**
* Include a specific file and return it's contents
*
* @param string File path
* @return string Contents of the processed file
*/
protected function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Create and register a button
*
* @param array Named button attributes
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $s_button_count = 100;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (!($attrib['command'] || $attrib['name'])) {
return '';
}
// try to find out the button type
if ($attrib['type']) {
$attrib['type'] = strtolower($attrib['type']);
}
else {
$attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'link';
}
$command = $attrib['command'];
if ($attrib['task'])
$command = $attrib['task'] . '.' . $command;
if (!$attrib['image']) {
$attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact'];
}
if (!$attrib['id']) {
$attrib['id'] = sprintf('rcmbtn%d', $s_button_count++);
}
// get localized text for labels and titles
if ($attrib['title']) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $attrib['domain']));
}
if ($attrib['label']) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $attrib['domain']));
}
if ($attrib['alt']) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $attrib['domain']));
}
// set title to alt attribute for IE browsers
if ($this->browser->ie && !$attrib['title'] && $attrib['alt']) {
$attrib['title'] = $attrib['alt'];
}
// add empty alt attribute for XHTML compatibility
if (!isset($attrib['alt'])) {
$attrib['alt'] = '';
}
// register button in the system
if ($attrib['command']) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
rcmail::JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
$attrib['imageact'] ? $this->abs_url($attrib['imageact']) : $attrib['classact'],
$attrib['imagesel'] ? $this->abs_url($attrib['imagesel']) : $attrib['classsel'],
$attrib['imageover'] ? $this->abs_url($attrib['imageover']) : ''
));
// make valid href to specific buttons
if (in_array($attrib['command'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('task' => $attrib['command']));
$attrib['onclick'] = sprintf("%s.command('switch-task','%s',null,event); return false", rcmail::JS_OBJECT_NAME, $attrib['command']);
}
else if ($attrib['task'] && in_array($attrib['task'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task']));
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command']));
}
else if ($attrib['command'] == 'permaurl' && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (!$attrib['href']) {
$attrib['href'] = '#';
}
if ($attrib['task']) {
if ($attrib['classact'])
$attrib['class'] = $attrib['classact'];
}
else if ($command && !$attrib['onclick']) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
rcmail::JS_OBJECT_NAME,
$command,
$attrib['prop']
);
}
$out = '';
// generate image tag
if ($attrib['type'] == 'image') {
$attrib_str = html::attrib_string(
$attrib,
array(
'style', 'class', 'id', 'width', 'height', 'border', 'hspace',
'vspace', 'align', 'alt', 'tabindex', 'title'
)
);
$btn_content = sprintf('<img src="%s"%s />', $this->abs_url($attrib['image']), $attrib_str);
if ($attrib['label']) {
$btn_content .= ' '.$attrib['label'];
}
$link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'target');
}
else if ($attrib['type'] == 'link') {
$btn_content = isset($attrib['content']) ? $attrib['content'] : ($attrib['label'] ? $attrib['label'] : $attrib['command']);
$link_attrib = array('href', 'onclick', 'title', 'id', 'class', 'style', 'tabindex', 'target');
if ($attrib['innerclass'])
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($attrib['command']) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, $link_attrib);
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
return $out;
}
/**
* Link an external script file
*
* @param string File URL
* @param string Target position [head|foot]
*/
public function include_script($file, $position='head')
{
static $sa_files = array();
if (!preg_match('|^https?://|i', $file) && $file[0] != '/') {
$file = $this->scripts_path . $file;
if ($fs = @filemtime($file)) {
$file .= '?s=' . $fs;
}
}
if (in_array($file, $sa_files)) {
return;
}
$sa_files[] = $file;
if (!is_array($this->script_files[$position])) {
$this->script_files[$position] = array();
}
$this->script_files[$position][] = $file;
}
/**
* Add inline javascript code
*
* @param string JS code snippet
* @param string Target position [head|head_top|foot]
*/
public function add_script($script, $position='head')
{
if (!isset($this->scripts[$position])) {
$this->scripts[$position] = "\n" . rtrim($script);
}
else {
$this->scripts[$position] .= "\n" . rtrim($script);
}
}
/**
* Link an external css file
*
* @param string File URL
*/
public function include_css($file)
{
$this->css_files[] = $file;
}
/**
* Add HTML code to the page header
*
* @param string $str HTML code
*/
public function add_header($str)
{
$this->header .= "\n" . $str;
}
/**
* Add HTML code to the page footer
* To be added right befor </body>
*
* @param string $str HTML code
*/
public function add_footer($str)
{
$this->footer .= "\n" . $str;
}
/**
* Process template and write to stdOut
*
* @param string HTML template
* @param string Base for absolute paths
*/
public function _write($templ = '', $base_path = '')
{
$output = empty($templ) ? $this->default_template : trim($templ);
// set default page title
if (empty($this->pagetitle)) {
$this->pagetitle = 'Roundcube Mail';
}
// replace specialchars in content
$page_title = html::quote($this->pagetitle);
$page_header = '';
$page_footer = '';
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
header('Content-Type: text/html; charset=' . $this->charset);
}
$page_header = '<meta http-equiv="content-type"';
$page_header.= ' content="text/html; charset=';
$page_header.= $this->charset . '" />'."\n";
}
// definition of the code to be placed in the document header and footer
if (is_array($this->script_files['head'])) {
foreach ($this->script_files['head'] as $file) {
$page_header .= html::script($file);
}
}
$head_script = $this->scripts['head_top'] . $this->scripts['head'];
if (!empty($head_script)) {
$page_header .= html::script(array(), $head_script);
}
if (!empty($this->header)) {
$page_header .= $this->header;
}
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script('$(document).ready(function(){ ' . $this->scripts['docready'] . "\n});", 'foot');
}
if (is_array($this->script_files['foot'])) {
foreach ($this->script_files['foot'] as $file) {
$page_footer .= html::script($file);
}
}
if (!empty($this->footer)) {
$page_footer .= $this->footer . "\n";
}
if (!empty($this->scripts['foot'])) {
$page_footer .= html::script(array(), $this->scripts['foot']);
}
// find page header
if ($hpos = stripos($output, '</head>')) {
$page_header .= "\n";
}
else {
if (!is_numeric($hpos)) {
$hpos = stripos($output, '<body');
}
if (!is_numeric($hpos) && ($hpos = stripos($output, '<html'))) {
while ($output[$hpos] != '>') {
$hpos++;
}
$hpos++;
}
$page_header = "<head>\n<title>$page_title</title>\n$page_header\n</head>\n";
}
// add page hader
if ($hpos) {
$output = substr_replace($output, $page_header, $hpos, 0);
}
else {
$output = $page_header . $output;
}
// add page footer
if (($fpos = strripos($output, '</body>')) || ($fpos = strripos($output, '</html>'))) {
$output = substr_replace($output, $page_footer."\n", $fpos, 0);
}
else {
$output .= "\n".$page_footer;
}
// add css files in head, before scripts, for speed up with parallel downloads
if (!empty($this->css_files) &&
(($pos = stripos($output, '<script ')) || ($pos = stripos($output, '</head>')))
) {
$css = '';
foreach ($this->css_files as $file) {
$css .= html::tag('link', array('rel' => 'stylesheet',
'type' => 'text/css', 'href' => $file, 'nl' => true));
}
$output = substr_replace($output, $css, $pos, 0);
}
$this->base_path = $base_path;
// correct absolute paths in images and other tags
// add timestamp to .js and .css filename
$output = preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
array($this, 'file_callback'), $output);
// trigger hook with final HTML content to be sent
$hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
if (!$hook['abort']) {
if ($this->charset != RCMAIL_CHARSET) {
echo rcube_charset::convert($hook['content'], RCMAIL_CHARSET, $this->charset);
}
else {
echo $hook['content'];
}
}
}
/**
* Callback function for preg_replace_callback in write()
*
* @return string Parsed string
*/
protected function file_callback($matches)
{
$file = $matches[3];
// correct absolute paths
if ($file[0] == '/') {
$file = $this->base_path . $file;
}
// add file modification timestamp
if (preg_match('/\.(js|css)$/', $file)) {
if ($fs = @filemtime($file)) {
$file .= '?s=' . $fs;
}
}
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array Named tag parameters
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
if ($this->framed || !empty($_REQUEST['_framed'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
$hidden = $hiddenfield->show();
}
if (!$content)
$attrib['noclose'] = true;
return html::tag('form',
$attrib + array('action' => "./", 'method' => "get"),
$hidden . $content,
array('id','class','style','name','method','action','enctype','onsubmit'));
}
/**
* Build a form tag with a unique request token
*
* @param array Named tag parameters including 'action' and 'task' values which will be put into hidden fields
* @param string Form content
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if ($attrib['task']) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if ($attrib['action']) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
// we already have a <form> tag
if ($attrib['form']) {
if ($this->framed || !empty($_REQUEST['_framed']))
$hidden->add(array('name' => '_framed', 'value' => '1'));
return $hidden->show() . $content;
}
else
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array Named tag parameters (currently not used)
* @return string HTML code for the gui object
*/
public function current_username($attrib)
{
static $username;
// alread fetched
if (!empty($username)) {
return $username;
}
// Current username is an e-mail address
if (strpos($_SESSION['username'], '@')) {
$username = $_SESSION['username'];
}
// get e-mail address from default identity
else if ($sql_arr = $this->app->user->get_identity()) {
$username = $sql_arr['email'];
}
else {
$username = $this->app->user->get_username();
}
return rcube_utils::idn_to_utf8($username);
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array Named parameters
* @return string HTML code for the gui object
*/
protected function login_form($attrib)
{
$default_host = $this->config->get('default_host');
$autocomplete = (int) $this->config->get('login_autocomplete');
$_SESSION['temp'] = true;
// save original url
$url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))
$url = $_SERVER['QUERY_STRING'];
// set atocomplete attribute
$user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
$input_dst = new html_hiddenfield(array('name' => '_dstactive', 'id' => 'rcmlogindst', 'value' => '_default_'));
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
$input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser')
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd')
+ $attrib + $pass_attrib);
$input_host = null;
if (is_array($default_host) && count($default_host) > 1) {
$input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost'));
foreach ($default_host as $key => $value) {
if (!is_array($value)) {
$input_host->add($value, (is_numeric($key) ? $value : $key));
}
else {
$input_host = null;
break;
}
}
}
else if (is_array($default_host) && ($host = array_pop($default_host))) {
$hide_host = true;
$input_host = new html_hiddenfield(array(
'name' => '_host', 'id' => 'rcmloginhost', 'value' => $host) + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost')
+ $attrib + $host_attrib);
}
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', html::quote($this->app->gettext('username'))));
$table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
$table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
$table->add('input', $input_pass->show());
// add host selection row
if (is_object($input_host) && !$hide_host) {
$table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
$table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_dst->show();
$out .= $input_url->show();
$out .= $table->show();
if ($hide_host) {
$out .= $input_host->show();
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
}
return $out;
}
/**
* GUI object 'preloader'
* Loads javascript code for images preloading
*
* @param array Named parameters
* @return void
*/
protected function preloader($attrib)
{
$images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
$images = array_map(array($this, 'abs_url'), $images);
if (empty($images) || $this->app->task == 'logout')
return;
$this->add_script('var images = ' . self::json_serialize($images) .';
for (var i=0; i<images.length; i++) {
img = new Image();
img.src = images[i];
}', 'docready');
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array Named parameters
* @return string HTML code for the gui object
*/
protected function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if ($attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
$input_q = new html_inputfield($attrib);
$out = $input_q->show();
$this->add_gui_object('qsearchbox', $attrib['id']);
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->form_tag(array(
'name' => "rcmqsearchform",
'onsubmit' => rcmail::JS_OBJECT_NAME . ".command('search'); return false",
'style' => "display:inline"),
$out);
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
protected function message_container($attrib)
{
if (isset($attrib['id']) === false) {
$attrib['id'] = 'rcmMessageContainer';
}
$this->add_gui_object('message', $attrib['id']);
return html::div($attrib, '');
}
/**
* GUI object 'charsetselector'
*
* @param array Named parameters for the select tag
* @return string HTML code for the gui object
*/
public function charset_selector($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_charset');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'name', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
$charsets = array(
'UTF-8' => 'UTF-8 ('.$this->app->gettext('unicode').')',
'US-ASCII' => 'ASCII ('.$this->app->gettext('english').')',
'ISO-8859-1' => 'ISO-8859-1 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-2' => 'ISO-8859-2 ('.$this->app->gettext('easterneuropean').')',
'ISO-8859-4' => 'ISO-8859-4 ('.$this->app->gettext('baltic').')',
'ISO-8859-5' => 'ISO-8859-5 ('.$this->app->gettext('cyrillic').')',
'ISO-8859-6' => 'ISO-8859-6 ('.$this->app->gettext('arabic').')',
'ISO-8859-7' => 'ISO-8859-7 ('.$this->app->gettext('greek').')',
'ISO-8859-8' => 'ISO-8859-8 ('.$this->app->gettext('hebrew').')',
'ISO-8859-9' => 'ISO-8859-9 ('.$this->app->gettext('turkish').')',
'ISO-8859-10' => 'ISO-8859-10 ('.$this->app->gettext('nordic').')',
'ISO-8859-11' => 'ISO-8859-11 ('.$this->app->gettext('thai').')',
'ISO-8859-13' => 'ISO-8859-13 ('.$this->app->gettext('baltic').')',
'ISO-8859-14' => 'ISO-8859-14 ('.$this->app->gettext('celtic').')',
'ISO-8859-15' => 'ISO-8859-15 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-16' => 'ISO-8859-16 ('.$this->app->gettext('southeasterneuropean').')',
'WINDOWS-1250' => 'Windows-1250 ('.$this->app->gettext('easterneuropean').')',
'WINDOWS-1251' => 'Windows-1251 ('.$this->app->gettext('cyrillic').')',
'WINDOWS-1252' => 'Windows-1252 ('.$this->app->gettext('westerneuropean').')',
'WINDOWS-1253' => 'Windows-1253 ('.$this->app->gettext('greek').')',
'WINDOWS-1254' => 'Windows-1254 ('.$this->app->gettext('turkish').')',
'WINDOWS-1255' => 'Windows-1255 ('.$this->app->gettext('hebrew').')',
'WINDOWS-1256' => 'Windows-1256 ('.$this->app->gettext('arabic').')',
'WINDOWS-1257' => 'Windows-1257 ('.$this->app->gettext('baltic').')',
'WINDOWS-1258' => 'Windows-1258 ('.$this->app->gettext('vietnamese').')',
'ISO-2022-JP' => 'ISO-2022-JP ('.$this->app->gettext('japanese').')',
'ISO-2022-KR' => 'ISO-2022-KR ('.$this->app->gettext('korean').')',
'ISO-2022-CN' => 'ISO-2022-CN ('.$this->app->gettext('chinese').')',
'EUC-JP' => 'EUC-JP ('.$this->app->gettext('japanese').')',
'EUC-KR' => 'EUC-KR ('.$this->app->gettext('korean').')',
'EUC-CN' => 'EUC-CN ('.$this->app->gettext('chinese').')',
'BIG5' => 'BIG5 ('.$this->app->gettext('chinese').')',
'GB2312' => 'GB2312 ('.$this->app->gettext('chinese').')',
);
if (!empty($_POST['_charset'])) {
$set = $_POST['_charset'];
}
else if (!empty($attrib['selected'])) {
$set = $attrib['selected'];
}
else {
$set = $this->get_charset();
}
$set = strtoupper($set);
if (!isset($charsets[$set])) {
$charsets[$set] = $set;
}
$select = new html_select($field_attrib);
$select->add(array_values($charsets), array_keys($charsets));
return $select->show($set);
}
/**
* Include content from config/about.<LANG>.html if available
*/
protected function about_content($attrib)
{
$content = '';
$filenames = array(
'about.' . $_SESSION['language'] . '.html',
'about.' . substr($_SESSION['language'], 0, 2) . '.html',
'about.html',
);
foreach ($filenames as $file) {
$fn = RCMAIL_CONFIG_DIR . '/' . $file;
if (is_readable($fn)) {
$content = file_get_contents($fn);
$content = $this->parse_conditions($content);
$content = $this->parse_xml($content);
break;
}
}
return $content;
}
}
diff --git a/lib/ext/Roundcube/rcube_plugin.php b/lib/ext/Roundcube/rcube_plugin.php
index b91355c..b1ec32a 100644
--- a/lib/ext/Roundcube/rcube_plugin.php
+++ b/lib/ext/Roundcube/rcube_plugin.php
@@ -1,356 +1,359 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_plugin.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2009, 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: |
| Abstract plugins interface/class |
| All plugins need to extend this class |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Plugin interface class
*
* @package PluginAPI
*/
abstract class rcube_plugin
{
/**
* Class name of the plugin instance
*
* @var string
*/
public $ID;
/**
* Instance of Plugin API
*
* @var rcube_plugin_api
*/
public $api;
/**
* Regular expression defining task(s) to bind with
*
* @var string
*/
public $task;
/**
* Disables plugin in AJAX requests
*
* @var boolean
*/
public $noajax = false;
/**
* Disables plugin in framed mode
*
* @var boolean
*/
public $noframe = false;
protected $home;
protected $urlbase;
private $mytask;
/**
* Default constructor.
*
* @param rcube_plugin_api $api Plugin API
*/
public function __construct($api)
{
$this->ID = get_class($this);
$this->api = $api;
$this->home = $api->dir . $this->ID;
$this->urlbase = $api->url . $this->ID . '/';
}
/**
* Initialization method, needs to be implemented by the plugin itself
*/
abstract function init();
/**
* Attempt to load the given plugin which is required for the current plugin
*
* @param string Plugin name
* @return boolean True on success, false on failure
*/
public function require_plugin($plugin_name)
{
return $this->api->load_plugin($plugin_name);
}
/**
* Load local config file from plugins directory.
* The loaded values are patched over the global configuration.
*
* @param string $fname Config file name relative to the plugin's folder
* @return boolean True on success, false on failure
*/
public function load_config($fname = 'config.inc.php')
{
$fpath = $this->home.'/'.$fname;
$rcube = rcube::get_instance();
if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) {
rcube::raise_error(array(
'code' => 527, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load config from $fpath"), true, false);
return false;
}
return true;
}
/**
* Register a callback function for a specific (server-side) hook
*
* @param string $hook Hook name
* @param mixed $callback Callback function as string or array with object reference and method name
*/
public function add_hook($hook, $callback)
{
$this->api->register_hook($hook, $callback);
}
/**
* Unregister a callback function for a specific (server-side) hook.
*
* @param string $hook Hook name
* @param mixed $callback Callback function as string or array with object reference and method name
*/
public function remove_hook($hook, $callback)
{
$this->api->unregister_hook($hook, $callback);
}
/**
* Load localized texts from the plugins dir
*
* @param string $dir Directory to search in
* @param mixed $add2client Make texts also available on the client (array with list or true for all)
*/
public function add_texts($dir, $add2client = false)
{
$domain = $this->ID;
$lang = $_SESSION['language'];
$langs = array_unique(array('en_US', $lang));
$locdir = slashify(realpath(slashify($this->home) . $dir));
$texts = array();
// Language aliases used to find localization in similar lang, see below
$aliases = array(
'de_CH' => 'de_DE',
'es_AR' => 'es_ES',
'fa_AF' => 'fa_IR',
'nl_BE' => 'nl_NL',
'pt_BR' => 'pt_PT',
'zh_CN' => 'zh_TW',
);
// use buffering to handle empty lines/spaces after closing PHP tag
ob_start();
foreach ($langs as $lng) {
$fpath = $locdir . $lng . '.inc';
if (is_file($fpath) && is_readable($fpath)) {
include $fpath;
$texts = (array)$labels + (array)$messages + (array)$texts;
}
else if ($lng != 'en_US') {
// Find localization in similar language (#1488401)
$alias = null;
if (!empty($aliases[$lng])) {
$alias = $aliases[$lng];
}
else if ($key = array_search($lng, $aliases)) {
$alias = $key;
}
if (!empty($alias)) {
$fpath = $locdir . $alias . '.inc';
if (is_file($fpath) && is_readable($fpath)) {
include $fpath;
$texts = (array)$labels + (array)$messages + (array)$texts;
}
}
}
}
ob_end_clean();
// prepend domain to text keys and add to the application texts repository
if (!empty($texts)) {
$add = array();
foreach ($texts as $key => $value)
$add[$domain.'.'.$key] = $value;
$rcmail = rcmail::get_instance();
$rcmail->load_language($lang, $add);
// add labels to client
if ($add2client) {
$js_labels = is_array($add2client) ? array_map(array($this, 'label_map_callback'), $add2client) : array_keys($add);
$rcmail->output->add_label($js_labels);
}
}
}
/**
* Wrapper for rcmail::gettext() adding the plugin ID as domain
*
* @param string $p Message identifier
* @return string Localized text
* @see rcmail::gettext()
*/
public function gettext($p)
{
return rcube::get_instance()->gettext($p, $this->ID);
}
/**
* Register this plugin to be responsible for a specific task
*
* @param string $task Task name (only characters [a-z0-9_.-] are allowed)
*/
public function register_task($task)
{
if ($this->api->register_task($task, $this->ID))
$this->mytask = $task;
}
/**
* Register a handler for a specific client-request action
*
* The callback will be executed upon a request like /?_task=mail&_action=plugin.myaction
*
* @param string $action Action name (should be unique)
* @param mixed $callback Callback function as string or array with object reference and method name
*/
public function register_action($action, $callback)
{
$this->api->register_action($action, $this->ID, $callback, $this->mytask);
}
/**
* Register a handler function for a template object
*
* When parsing a template for display, tags like <roundcube:object name="plugin.myobject" />
* will be replaced by the return value if the registered callback function.
*
* @param string $name Object name (should be unique and start with 'plugin.')
* @param mixed $callback Callback function as string or array with object reference and method name
*/
public function register_handler($name, $callback)
{
$this->api->register_handler($name, $this->ID, $callback);
}
/**
* Make this javascipt file available on the client
*
* @param string $fn File path; absolute or relative to the plugin directory
*/
public function include_script($fn)
{
$this->api->include_script($this->resource_url($fn));
}
/**
* Make this stylesheet available on the client
*
* @param string $fn File path; absolute or relative to the plugin directory
*/
public function include_stylesheet($fn)
{
$this->api->include_stylesheet($this->resource_url($fn));
}
/**
* Append a button to a certain container
*
* @param array $p Hash array with named parameters (as used in skin templates)
* @param string $container Container name where the buttons should be added to
* @see rcube_remplate::button()
*/
public function add_button($p, $container)
{
if ($this->api->output->type == 'html') {
// fix relative paths
foreach (array('imagepas', 'imageact', 'imagesel') as $key)
if ($p[$key])
$p[$key] = $this->api->url . $this->resource_url($p[$key]);
$this->api->add_content($this->api->output->button($p), $container);
}
}
/**
* Generate an absolute URL to the given resource within the current
* plugin directory
*
* @param string $fn The file name
* @return string Absolute URL to the given resource
*/
public function url($fn)
{
return $this->api->url . $this->resource_url($fn);
}
/**
* Make the given file name link into the plugin directory
*
* @param string $fn Filename
*/
private function resource_url($fn)
{
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
return $this->ID . '/' . $fn;
else
return $fn;
}
/**
* Provide path to the currently selected skin folder within the plugin directory
* with a fallback to the default skin folder.
*
* @return string Skin path relative to plugins directory
*/
public function local_skin_path()
{
$rcmail = rcube::get_instance();
- $skin_path = 'skins/' . $rcmail->config->get('skin');
- if (!is_dir(realpath(slashify($this->home) . $skin_path)))
- $skin_path = 'skins/default';
+ foreach (array($rcmail->config->get('skin'),'default') as $skin) {
+ $skin_path = 'skins/' . $skin;
+ if (is_dir(realpath(slashify($this->home) . $skin_path)))
+ break;
+ }
+
return $skin_path;
}
/**
* Callback function for array_map
*
* @param string $key Array key.
* @return string
*/
private function label_map_callback($key)
{
return $this->ID.'.'.$key;
}
}
diff --git a/lib/ext/Roundcube/rcube_shared.inc b/lib/ext/Roundcube/rcube_shared.inc
index 08fca48..85f2784 100644
--- a/lib/ext/Roundcube/rcube_shared.inc
+++ b/lib/ext/Roundcube/rcube_shared.inc
@@ -1,438 +1,438 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_shared.inc |
| |
| This file is part of the Roundcube PHP suite |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Shared functions used by Roundcube Framework |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Roundcube shared functions
*
* @package Core
*/
/**
* Similar function as in_array() but case-insensitive
*
* @param string $needle Needle value
* @param array $heystack Array to search in
*
* @return boolean True if found, False if not
*/
function in_array_nocase($needle, $haystack)
{
$needle = mb_strtolower($needle);
foreach ((array)$haystack as $value) {
if ($needle === mb_strtolower($value)) {
return true;
}
}
return false;
}
/**
* Find out if the string content means true or false
*
* @param string $str Input value
*
* @return boolean Boolean value
*/
function get_boolean($str)
{
$str = strtolower($str);
return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true);
}
/**
* Parse a human readable string for a number of bytes.
*
* @param string $str Input string
*
* @return float Number of bytes
*/
function parse_bytes($str)
{
if (is_numeric($str)) {
return floatval($str);
}
if (preg_match('/([0-9\.]+)\s*([a-z]*)/i', $str, $regs)) {
$bytes = floatval($regs[1]);
switch (strtolower($regs[2])) {
case 'g':
case 'gb':
$bytes *= 1073741824;
break;
case 'm':
case 'mb':
$bytes *= 1048576;
break;
case 'k':
case 'kb':
$bytes *= 1024;
break;
}
}
return floatval($bytes);
}
/**
* Make sure the string ends with a slash
*/
function slashify($str)
{
return unslashify($str).'/';
}
/**
* Remove slash at the end of the string
*/
function unslashify($str)
{
return preg_replace('/\/$/', '', $str);
}
/**
* Delete all files within a folder
*
* @param string Path to directory
*
* @return boolean True on success, False if directory was not found
*/
function clear_directory($dir_path)
{
$dir = @opendir($dir_path);
if (!$dir) {
return false;
}
while ($file = readdir($dir)) {
if (strlen($file) > 2) {
unlink("$dir_path/$file");
}
}
closedir($dir);
return true;
}
/**
* Returns number of seconds for a specified offset string.
*
* @param string $str String representation of the offset (e.g. 20min, 5h, 2days, 1week)
*
* @return int Number of seconds
*/
function get_offset_sec($str)
{
if (preg_match('/^([0-9]+)\s*([smhdw])/i', $str, $regs)) {
$amount = (int) $regs[1];
$unit = strtolower($regs[2]);
}
else {
$amount = (int) $str;
$unit = 's';
}
switch ($unit) {
case 'w':
$amount *= 7;
case 'd':
$amount *= 24;
case 'h':
$amount *= 60;
case 'm':
$amount *= 60;
}
return $amount;
}
/**
* Create a unix timestamp with a specified offset from now.
*
* @param string $offset_str String representation of the offset (e.g. 20min, 5h, 2days)
* @param int $factor Factor to multiply with the offset
*
* @return int Unix timestamp
*/
function get_offset_time($offset_str, $factor=1)
{
return time() + get_offset_sec($offset_str) * $factor;
}
/**
* Truncate string if it is longer than the allowed length.
* Replace the middle or the ending part of a string with a placeholder.
*
* @param string $str Input string
* @param int $maxlength Max. length
* @param string $placeholder Replace removed chars with this
* @param bool $ending Set to True if string should be truncated from the end
*
* @return string Abbreviated string
*/
function abbreviate_string($str, $maxlength, $placeholder='...', $ending=false)
{
$length = mb_strlen($str);
if ($length > $maxlength) {
if ($ending) {
return mb_substr($str, 0, $maxlength) . $placeholder;
}
$placeholder_length = mb_strlen($placeholder);
$first_part_length = floor(($maxlength - $placeholder_length)/2);
$second_starting_location = $length - $maxlength + $first_part_length + $placeholder_length;
$str = mb_substr($str, 0, $first_part_length) . $placeholder . mb_substr($str, $second_starting_location);
}
return $str;
}
/**
* Get all keys from array (recursive).
*
* @param array $array Input array
*
* @return array List of array keys
*/
function array_keys_recursive($array)
{
$keys = array();
if (!empty($array)) {
foreach ($array as $key => $child) {
$keys[] = $key;
foreach (array_keys_recursive($child) as $val) {
$keys[] = $val;
}
}
}
return $keys;
}
/**
* Remove all non-ascii and non-word chars except ., -, _
*/
function asciiwords($str, $css_id = false, $replace_with = '')
{
$allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
return preg_replace("/[^$allowed]/i", $replace_with, $str);
}
/**
* Remove single and double quotes from given string
*
* @param string Input value
*
* @return string Dequoted string
*/
function strip_quotes($str)
{
return str_replace(array("'", '"'), '', $str);
}
/**
* Remove new lines characters from given string
*
* @param string $str Input value
*
* @return string Stripped string
*/
function strip_newlines($str)
{
return preg_replace('/[\r\n]/', '', $str);
}
/**
* Compose a valid representation of name and e-mail address
*
* @param string $email E-mail address
* @param string $name Person name
*
* @return string Formatted string
*/
function format_email_recipient($email, $name = '')
{
$email = trim($email);
if ($name && $name != $email) {
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
if (preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name)) {
$name = '"'.addcslashes($name, '"').'"';
}
return "$name <$email>";
}
return $email;
}
/**
* mbstring replacement functions
*/
if (!extension_loaded('mbstring'))
{
function mb_strlen($str)
{
return strlen($str);
}
function mb_strtolower($str)
{
return strtolower($str);
}
function mb_strtoupper($str)
{
return strtoupper($str);
}
function mb_substr($str, $start, $len=null)
{
return substr($str, $start, $len);
}
function mb_strpos($haystack, $needle, $offset=0)
{
return strpos($haystack, $needle, $offset);
}
function mb_strrpos($haystack, $needle, $offset=0)
{
return strrpos($haystack, $needle, $offset);
}
}
/**
* intl replacement functions
*/
if (!function_exists('idn_to_utf8'))
{
function idn_to_utf8($domain, $flags=null)
{
static $idn, $loaded;
if (!$loaded) {
$idn = new Net_IDNA2();
$loaded = true;
}
if ($idn && $domain && preg_match('/(^|\.)xn--/i', $domain)) {
try {
$domain = $idn->decode($domain);
}
catch (Exception $e) {
}
}
return $domain;
}
}
if (!function_exists('idn_to_ascii'))
{
function idn_to_ascii($domain, $flags=null)
{
static $idn, $loaded;
if (!$loaded) {
$idn = new Net_IDNA2();
$loaded = true;
}
if ($idn && $domain && preg_match('/[^\x20-\x7E]/', $domain)) {
try {
$domain = $idn->encode($domain);
}
catch (Exception $e) {
}
}
return $domain;
}
}
/**
* Use PHP5 autoload for dynamic class loading
*
* @todo Make Zend, PEAR etc play with this
* @todo Make our classes conform to a more straight forward CS.
*/
function rcube_autoload($classname)
{
$filename = preg_replace(
array(
'/MDB2_(.+)/',
'/Mail_(.+)/',
'/Net_(.+)/',
'/Auth_(.+)/',
'/^html_.+/',
'/^utf8$/',
),
array(
- 'MDB2/\\1',
+ 'Mail/\\1',
'Mail/\\1',
'Net/\\1',
'Auth/\\1',
'html',
'utf8.class',
),
$classname
);
if ($fp = @fopen("$filename.php", 'r', true)) {
fclose($fp);
include_once "$filename.php";
return true;
}
return false;
}
/**
* Local callback function for PEAR errors
*/
function rcube_pear_error($err)
{
error_log(sprintf("%s (%s): %s",
$err->getMessage(),
$err->getCode(),
$err->getUserinfo()), 0);
}
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index f38720b..219dca7 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -1,616 +1,620 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_spellchecker.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011, Kolab Systems AG |
| Copyright (C) 2008-2011, The Roundcube Dev Team |
| |
| 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 using different backends |
| |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Helper class for spellchecking with Googielspell and PSpell support.
*
* @package Core
*/
class rcube_spellchecker
{
private $matches = array();
private $engine;
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
*
* @param string $lang Language code
*/
function __construct($lang = 'en')
{
$this->rc = rcube::get_instance();
$this->engine = $this->rc->config->get('spellcheck_engine', 'googie');
$this->lang = $lang ? $lang : 'en';
- if ($this->engine == 'pspell' && !extension_loaded('pspell')) {
- rcube::raise_error(array(
- 'code' => 500, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Pspell extension not available"), true, true);
- }
-
$this->options = array(
'ignore_syms' => $this->rc->config->get('spellcheck_ignore_syms'),
'ignore_nums' => $this->rc->config->get('spellcheck_ignore_nums'),
'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'),
'dictionary' => $this->rc->config->get('spellcheck_dictionary'),
);
}
/**
* Set content and check spelling
*
* @param string $text Text content for spellchecking
* @param bool $is_html Enables HTML-to-Text conversion
*
* @return bool True when no mispelling found, otherwise false
*/
function check($text, $is_html = false)
{
// convert to plain text
if ($is_html) {
$this->content = $this->html2text($text);
}
else {
$this->content = $text;
}
if ($this->engine == 'pspell') {
$this->matches = $this->_pspell_check($this->content);
}
else {
$this->matches = $this->_googie_check($this->content);
}
return $this->found() == 0;
}
/**
* Number of mispellings found (after check)
*
* @return int Number of mispellings
*/
function found()
{
return count($this->matches);
}
/**
* Returns suggestions for the specified word
*
* @param string $word The word
*
* @return array Suggestions list
*/
function get_suggestions($word)
{
if ($this->engine == 'pspell') {
return $this->_pspell_suggestions($word);
}
return $this->_googie_suggestions($word);
}
/**
- * Returns mispelled words
+ * Returns misspelled words
*
* @param string $text The content for spellchecking. If empty content
* used for check() method will be used.
*
- * @return array List of mispelled words
+ * @return array List of misspelled words
*/
function get_words($text = null, $is_html=false)
{
if ($this->engine == 'pspell') {
return $this->_pspell_words($text, $is_html);
}
return $this->_googie_words($text, $is_html);
}
/**
* Returns checking result in XML (Googiespell) format
*
* @return string XML content
*/
function get_xml()
{
// send output
$out = '<?xml version="1.0" encoding="'.RCMAIL_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
foreach ($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>';
}
$out .= '</spellresult>';
return $out;
}
/**
- * Returns checking result (mispelled words with suggestions)
+ * Returns checking result (misspelled words with suggestions)
*
* @return array Spellchecking result. An array indexed by word.
*/
function get()
{
$result = array();
foreach ($this->matches as $item) {
if ($this->engine == 'pspell') {
$word = $item[0];
}
else {
$word = mb_substr($this->content, $item[1], $item[2], RCMAIL_CHARSET);
}
$result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
}
return $result;
}
/**
* Returns error message
*
* @return string Error message
*/
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);
+ 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 mispelled words
+ * 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 mispelled words
+ // 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 mispelled word
+ * 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";
+ rcube::raise_error(array(
+ 'code' => 500, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => $this->error), true, false);
+
+ return;
+ }
+
$this->plink = pspell_new($this->lang, null, null, RCMAIL_CHARSET, PSPELL_FAST);
}
if (!$this->plink) {
$this->error = "Unable to load Pspell engine for selected language";
}
}
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);
}
if (!$store) {
$this->error = "Empty result from spelling engine";
}
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], RCMAIL_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], RCMAIL_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();
}
private function html2text($text)
{
$h2t = new html2text($text, false, true, 0);
return $h2t->get_text();
}
/**
* Check if the specified word is an exception accoring to
* spellcheck options.
*
* @param string $word The word
*
* @return bool True if the word is an exception, False otherwise
*/
public function is_exception($word)
{
// Contain only symbols (e.g. "+9,0", "2:2")
if (!$word || preg_match('/^[0-9@#$%^&_+~*=:;?!,.-]+$/', $word))
return true;
// Contain symbols (e.g. "g@@gle"), all symbols excluding separators
if (!empty($this->options['ignore_syms']) && preg_match('/[@#$%^&_+~*=-]/', $word))
return true;
// Contain numbers (e.g. "g00g13")
if (!empty($this->options['ignore_nums']) && preg_match('/[0-9]/', $word))
return true;
// Blocked caps (e.g. "GOOGLE")
if (!empty($this->options['ignore_caps']) && $word == mb_strtoupper($word))
return true;
// Use exceptions from dictionary
if (!empty($this->options['dictionary'])) {
$this->load_dict();
// @TODO: should dictionary be case-insensitive?
if (!empty($this->dict) && in_array($word, $this->dict))
return true;
}
return false;
}
/**
* Add a word to dictionary
*
* @param string $word The word to add
*/
public function add_word($word)
{
$this->load_dict();
foreach (explode(' ', $word) as $word) {
// sanity check
if (strlen($word) < 512) {
$this->dict[] = $word;
$valid = true;
}
}
if ($valid) {
$this->dict = array_unique($this->dict);
$this->update_dict();
}
}
/**
* Remove a word from dictionary
*
* @param string $word The word to remove
*/
public function remove_word($word)
{
$this->load_dict();
if (($key = array_search($word, $this->dict)) !== false) {
unset($this->dict[$key]);
$this->update_dict();
}
}
/**
* Update dictionary row in DB
*/
private function update_dict()
{
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
$userid = $this->rc->get_user_id();
}
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_save', array(
'userid' => $userid, 'language' => $this->lang, 'dictionary' => $this->dict));
if (!empty($plugin['abort'])) {
return;
}
if ($this->have_dict) {
if (!empty($this->dict)) {
$this->rc->db->query(
"UPDATE ".$this->rc->db->table_name('dictionary')
." SET data = ?"
." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
implode(' ', $plugin['dictionary']), $plugin['language']);
}
// don't store empty dict
else {
$this->rc->db->query(
"DELETE FROM " . $this->rc->db->table_name('dictionary')
." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
$plugin['language']);
}
}
else if (!empty($this->dict)) {
$this->rc->db->query(
"INSERT INTO " .$this->rc->db->table_name('dictionary')
." (user_id, " . $this->rc->db->quoteIdentifier('language') . ", data) VALUES (?, ?, ?)",
$plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary']));
}
}
/**
* Get dictionary from DB
*/
private function load_dict()
{
if (is_array($this->dict)) {
return $this->dict;
}
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
$userid = $this->rc->get_user_id();
}
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_get', array(
'userid' => $userid, 'language' => $this->lang, 'dictionary' => array()));
if (empty($plugin['abort'])) {
$dict = array();
$this->rc->db->query(
"SELECT data FROM ".$this->rc->db->table_name('dictionary')
." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
$plugin['language']);
if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) {
$this->have_dict = true;
if (!empty($sql_arr['data'])) {
$dict = explode(' ', $sql_arr['data']);
}
}
$plugin['dictionary'] = array_merge((array)$plugin['dictionary'], $dict);
}
if (!empty($plugin['dictionary']) && is_array($plugin['dictionary'])) {
$this->dict = $plugin['dictionary'];
}
else {
$this->dict = array();
}
return $this->dict;
}
}
diff --git a/lib/ext/Roundcube/rcube_user.php b/lib/ext/Roundcube/rcube_user.php
index 76f74da..644d24b 100644
--- a/lib/ext/Roundcube/rcube_user.php
+++ b/lib/ext/Roundcube/rcube_user.php
@@ -1,678 +1,684 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_user.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2010, 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: |
| This class represents a system user linked and provides access |
| to the related database records. |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class representing a system user
*
* @package Core
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_user
{
public $ID;
public $data;
public $language;
/**
* Holds database connection.
*
- * @var rcube_mdb2
+ * @var rcube_db
*/
private $db;
/**
* Framework object.
*
* @var rcube
*/
private $rc;
const SEARCH_ADDRESSBOOK = 1;
const SEARCH_MAIL = 2;
/**
* Object constructor
*
* @param int $id User id
* @param array $sql_arr SQL result set
*/
function __construct($id = null, $sql_arr = null)
{
$this->rc = rcube::get_instance();
$this->db = $this->rc->get_dbh();
if ($id && !$sql_arr) {
$sql_result = $this->db->query(
"SELECT * FROM ".$this->db->table_name('users')." WHERE user_id = ?", $id);
$sql_arr = $this->db->fetch_assoc($sql_result);
}
if (!empty($sql_arr)) {
$this->ID = $sql_arr['user_id'];
$this->data = $sql_arr;
$this->language = $sql_arr['language'];
}
}
/**
* Build a user name string (as e-mail address)
*
* @param string $part Username part (empty or 'local' or 'domain')
* @return string Full user name or its part
*/
function get_username($part = null)
{
if ($this->data['username']) {
list($local, $domain) = explode('@', $this->data['username']);
// at least we should always have the local part
if ($part == 'local') {
return $local;
}
// if no domain was provided...
if (empty($domain)) {
$domain = $this->rc->config->mail_domain($this->data['mail_host']);
}
if ($part == 'domain') {
return $domain;
}
if (!empty($domain))
return $local . '@' . $domain;
else
return $local;
}
return false;
}
/**
* Get the preferences saved for this user
*
* @return array Hash array with prefs
*/
function get_prefs()
{
if (!empty($this->language))
$prefs = array('language' => $this->language);
if ($this->ID) {
// Preferences from session (write-master is unavailable)
if (!empty($_SESSION['preferences'])) {
// Check last write attempt time, try to write again (every 5 minutes)
if ($_SESSION['preferences_time'] < time() - 5 * 60) {
$saved_prefs = unserialize($_SESSION['preferences']);
$this->rc->session->remove('preferences');
$this->rc->session->remove('preferences_time');
$this->save_prefs($saved_prefs);
}
else {
$this->data['preferences'] = $_SESSION['preferences'];
}
}
if ($this->data['preferences']) {
$prefs += (array)unserialize($this->data['preferences']);
}
}
return $prefs;
}
/**
* Write the given user prefs to the user's record
*
* @param array $a_user_prefs User prefs to save
* @return boolean True on success, False on failure
*/
function save_prefs($a_user_prefs)
{
if (!$this->ID)
return false;
$config = $this->rc->config;
$old_prefs = (array)$this->get_prefs();
// merge (partial) prefs array with existing settings
$save_prefs = $a_user_prefs + $old_prefs;
unset($save_prefs['language']);
// don't save prefs with default values if they haven't been changed yet
foreach ($a_user_prefs as $key => $value) {
if (!isset($old_prefs[$key]) && ($value == $config->get($key)))
unset($save_prefs[$key]);
}
$save_prefs = serialize($save_prefs);
$this->db->query(
"UPDATE ".$this->db->table_name('users').
" SET preferences = ?".
", language = ?".
" WHERE user_id = ?",
$save_prefs,
$_SESSION['language'],
$this->ID);
$this->language = $_SESSION['language'];
// Update success
if ($this->db->affected_rows() !== false) {
$config->set_user_prefs($a_user_prefs);
$this->data['preferences'] = $save_prefs;
if (isset($_SESSION['preferences'])) {
$this->rc->session->remove('preferences');
$this->rc->session->remove('preferences_time');
}
return true;
}
// Update error, but we are using replication (we have read-only DB connection)
// and we are storing session not in the SQL database
// we can store preferences in session and try to write later (see get_prefs())
else if ($this->db->is_replicated() && $config->get('session_storage', 'db') != 'db') {
$_SESSION['preferences'] = $save_prefs;
$_SESSION['preferences_time'] = time();
$config->set_user_prefs($a_user_prefs);
$this->data['preferences'] = $save_prefs;
}
return false;
}
/**
* Get default identity of this user
*
* @param int $id Identity ID. If empty, the default identity is returned
* @return array Hash array with all cols of the identity record
*/
function get_identity($id = null)
{
$result = $this->list_identities($id ? sprintf('AND identity_id = %d', $id) : '');
return $result[0];
}
/**
* Return a list of all identities linked with this user
*
* @param string $sql_add Optional WHERE clauses
* @return array List of identities
*/
function list_identities($sql_add = '')
{
$result = array();
$sql_result = $this->db->query(
"SELECT * FROM ".$this->db->table_name('identities').
" WHERE del <> 1 AND user_id = ?".
($sql_add ? " ".$sql_add : "").
" ORDER BY ".$this->db->quoteIdentifier('standard')." DESC, name ASC, identity_id ASC",
$this->ID);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$result[] = $sql_arr;
}
return $result;
}
/**
* Update a specific identity record
*
* @param int $iid Identity ID
* @param array $data Hash array with col->value pairs to save
* @return boolean True if saved successfully, false if nothing changed
*/
function update_identity($iid, $data)
{
if (!$this->ID)
return false;
$query_cols = $query_params = array();
foreach ((array)$data as $col => $value) {
$query_cols[] = $this->db->quoteIdentifier($col) . ' = ?';
$query_params[] = $value;
}
$query_params[] = $iid;
$query_params[] = $this->ID;
$sql = "UPDATE ".$this->db->table_name('identities').
" SET changed = ".$this->db->now().", ".join(', ', $query_cols).
" WHERE identity_id = ?".
" AND user_id = ?".
" AND del <> 1";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $query_params));
return $this->db->affected_rows();
}
/**
* Create a new identity record linked with this user
*
* @param array $data Hash array with col->value pairs to save
* @return int The inserted identity ID or false on error
*/
function insert_identity($data)
{
if (!$this->ID)
return false;
unset($data['user_id']);
$insert_cols = $insert_values = array();
foreach ((array)$data as $col => $value) {
$insert_cols[] = $this->db->quoteIdentifier($col);
$insert_values[] = $value;
}
$insert_cols[] = 'user_id';
$insert_values[] = $this->ID;
$sql = "INSERT INTO ".$this->db->table_name('identities').
" (changed, ".join(', ', $insert_cols).")".
" VALUES (".$this->db->now().", ".join(', ', array_pad(array(), sizeof($insert_values), '?')).")";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $insert_values));
return $this->db->insert_id('identities');
}
/**
* Mark the given identity as deleted
*
* @param int $iid Identity ID
* @return boolean True if deleted successfully, false if nothing changed
*/
function delete_identity($iid)
{
if (!$this->ID)
return false;
$sql_result = $this->db->query(
"SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities').
" WHERE user_id = ? AND del <> 1",
$this->ID);
$sql_arr = $this->db->fetch_assoc($sql_result);
// we'll not delete last identity
if ($sql_arr['ident_count'] <= 1)
return -1;
$this->db->query(
"UPDATE ".$this->db->table_name('identities').
" SET del = 1, changed = ".$this->db->now().
" WHERE user_id = ?".
" AND identity_id = ?",
$this->ID,
$iid);
return $this->db->affected_rows();
}
/**
* Make this identity the default one for this user
*
* @param int $iid The identity ID
*/
function set_default($iid)
{
if ($this->ID && $iid) {
$this->db->query(
"UPDATE ".$this->db->table_name('identities').
" SET ".$this->db->quoteIdentifier('standard')." = '0'".
" WHERE user_id = ?".
" AND identity_id <> ?".
" AND del <> 1",
$this->ID,
$iid);
}
}
/**
* Update user's last_login timestamp
*/
function touch()
{
if ($this->ID) {
$this->db->query(
"UPDATE ".$this->db->table_name('users').
" SET last_login = ".$this->db->now().
" WHERE user_id = ?",
$this->ID);
}
}
/**
* Clear the saved object state
*/
function reset()
{
$this->ID = null;
$this->data = null;
}
/**
* Find a user record matching the given name and host
*
* @param string $user IMAP user name
* @param string $host IMAP host name
* @return rcube_user New user instance
*/
static function query($user, $host)
{
- $dbh = rcube::get_instance()->get_dbh();
+ $dbh = rcube::get_instance()->get_dbh();
+ $config = rcube::get_instance()->config;
// query for matching user name
- $query = "SELECT * FROM ".$dbh->table_name('users')." WHERE mail_host = ? AND %s = ?";
- $sql_result = $dbh->query(sprintf($query, 'username'), $host, $user);
+ $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users')
+ ." WHERE mail_host = ? AND username = ?", $host, $user);
+
+ $sql_arr = $dbh->fetch_assoc($sql_result);
+
+ // username not found, try aliases from identities
+ if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) {
+ $sql_result = $dbh->limitquery("SELECT u.*"
+ ." FROM " . $dbh->table_name('users') . " u"
+ ." JOIN " . $dbh->table_name('identities') . " i ON (i.user_id = u.user_id)"
+ ." WHERE email = ? AND del <> 1", 0, 1, $user);
- // query for matching alias
- if (!($sql_arr = $dbh->fetch_assoc($sql_result))) {
- $sql_result = $dbh->query(sprintf($query, 'alias'), $host, $user);
$sql_arr = $dbh->fetch_assoc($sql_result);
}
// user already registered -> overwrite username
if ($sql_arr)
return new rcube_user($sql_arr['user_id'], $sql_arr);
else
return false;
}
/**
* Create a new user record and return a rcube_user instance
*
* @param string $user IMAP user name
* @param string $host IMAP host
* @return rcube_user New user instance
*/
static function create($user, $host)
{
$user_name = '';
$user_email = '';
$rcube = rcube::get_instance();
// try to resolve user in virtuser table and file
if ($email_list = self::user2email($user, false, true)) {
$user_email = is_array($email_list[0]) ? $email_list[0]['email'] : $email_list[0];
}
$data = $rcube->plugins->exec_hook('user_create',
array('user'=>$user, 'user_name'=>$user_name, 'user_email'=>$user_email, 'host'=>$host));
// plugin aborted this operation
if ($data['abort'])
return false;
$user_name = $data['user_name'];
$user_email = $data['user_email'];
$dbh = $rcube->get_dbh();
$dbh->query(
"INSERT INTO ".$dbh->table_name('users').
- " (created, last_login, username, mail_host, alias, language)".
- " VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?, ?)",
+ " (created, last_login, username, mail_host, language)".
+ " VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)",
strip_newlines($user),
strip_newlines($host),
- strip_newlines($data['alias'] ? $data['alias'] : $user_email),
strip_newlines($data['language'] ? $data['language'] : $_SESSION['language']));
if ($user_id = $dbh->insert_id('users')) {
// create rcube_user instance to make plugin hooks work
$user_instance = new rcube_user($user_id);
$rcube->user = $user_instance;
$mail_domain = $rcube->config->mail_domain($host);
if ($user_email == '') {
$user_email = strpos($user, '@') ? $user : sprintf('%s@%s', $user, $mail_domain);
}
if ($user_name == '') {
$user_name = $user != $user_email ? $user : '';
}
if (empty($email_list))
$email_list[] = strip_newlines($user_email);
// identities_level check
else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1)
$email_list = array($email_list[0]);
// create new identities records
$standard = 1;
foreach ($email_list as $row) {
- $record = array();
+ $record = array();
if (is_array($row)) {
- $record = $row;
+ $record = $row;
}
else {
$record['email'] = $row;
}
- if (empty($record['name']))
- $record['name'] = $user_name;
+ if (empty($record['name']))
+ $record['name'] = $user_name;
$record['name'] = strip_newlines($record['name']);
$record['user_id'] = $user_id;
$record['standard'] = $standard;
$plugin = $rcube->plugins->exec_hook('identity_create',
- array('login' => true, 'record' => $record));
+ array('login' => true, 'record' => $record));
if (!$plugin['abort'] && $plugin['record']['email']) {
$rcube->user->insert_identity($plugin['record']);
}
$standard = 0;
}
}
else {
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Failed to create new user"), true, false);
}
return $user_id ? $user_instance : false;
}
/**
* Resolve username using a virtuser plugins
*
* @param string $email E-mail address to resolve
* @return string Resolved IMAP username
*/
static function email2user($email)
{
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('email2user',
array('email' => $email, 'user' => NULL));
return $plugin['user'];
}
/**
* Resolve e-mail address from virtuser plugins
*
* @param string $user User name
* @param boolean $first If true returns first found entry
* @param boolean $extended If true returns email as array (email and name for identity)
* @return mixed Resolved e-mail address string or array of strings
*/
static function user2email($user, $first=true, $extended=false)
{
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('user2email',
array('email' => NULL, 'user' => $user,
'first' => $first, 'extended' => $extended));
return empty($plugin['email']) ? NULL : $plugin['email'];
}
/**
* Return a list of saved searches linked with this user
*
* @param int $type Search type
*
* @return array List of saved searches indexed by search ID
*/
function list_searches($type)
{
$plugin = $this->rc->plugins->exec_hook('saved_search_list', array('type' => $type));
if ($plugin['abort']) {
return (array) $plugin['result'];
}
$result = array();
$sql_result = $this->db->query(
"SELECT search_id AS id, ".$this->db->quoteIdentifier('name')
." FROM ".$this->db->table_name('searches')
." WHERE user_id = ?"
." AND ".$this->db->quoteIdentifier('type')." = ?"
." ORDER BY ".$this->db->quoteIdentifier('name'),
(int) $this->ID, (int) $type);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$sql_arr['data'] = unserialize($sql_arr['data']);
$result[$sql_arr['id']] = $sql_arr;
}
return $result;
}
/**
* Return saved search data.
*
* @param int $id Row identifier
*
* @return array Data
*/
function get_search($id)
{
$plugin = $this->rc->plugins->exec_hook('saved_search_get', array('id' => $id));
if ($plugin['abort']) {
return $plugin['result'];
}
$sql_result = $this->db->query(
"SELECT ".$this->db->quoteIdentifier('name')
.", ".$this->db->quoteIdentifier('data')
.", ".$this->db->quoteIdentifier('type')
." FROM ".$this->db->table_name('searches')
." WHERE user_id = ?"
." AND search_id = ?",
(int) $this->ID, (int) $id);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
return array(
'id' => $id,
'name' => $sql_arr['name'],
'type' => $sql_arr['type'],
'data' => unserialize($sql_arr['data']),
);
}
return null;
}
/**
* Deletes given saved search record
*
* @param int $sid Search ID
*
* @return boolean True if deleted successfully, false if nothing changed
*/
function delete_search($sid)
{
if (!$this->ID)
return false;
$this->db->query(
"DELETE FROM ".$this->db->table_name('searches')
." WHERE user_id = ?"
." AND search_id = ?",
(int) $this->ID, $sid);
return $this->db->affected_rows();
}
/**
* Create a new saved search record linked with this user
*
* @param array $data Hash array with col->value pairs to save
*
* @return int The inserted search ID or false on error
*/
function insert_search($data)
{
if (!$this->ID)
return false;
$insert_cols[] = 'user_id';
$insert_values[] = (int) $this->ID;
$insert_cols[] = $this->db->quoteIdentifier('type');
$insert_values[] = (int) $data['type'];
$insert_cols[] = $this->db->quoteIdentifier('name');
$insert_values[] = $data['name'];
$insert_cols[] = $this->db->quoteIdentifier('data');
$insert_values[] = serialize($data['data']);
$sql = "INSERT INTO ".$this->db->table_name('searches')
." (".join(', ', $insert_cols).")"
." VALUES (".join(', ', array_pad(array(), sizeof($insert_values), '?')).")";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $insert_values));
return $this->db->insert_id('searches');
}
}
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index 7f20dbb..9344a92 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -1,831 +1,833 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_utils.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| Copyright (C) 2011-2012, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Utility class providing common functions |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Utility class providing common functions
*
* @package Core
*/
class rcube_utils
{
// define constants for input reading
const INPUT_GET = 0x0101;
const INPUT_POST = 0x0102;
const INPUT_GPC = 0x0103;
/**
* Helper method to set a cookie with the current path and host settings
*
* @param string Cookie name
* @param string Cookie value
* @param string Expiration time
*/
public static function setcookie($name, $value, $exp = 0)
{
if (headers_sent()) {
return;
}
$cookie = session_get_cookie_params();
- $secure = self::https_check();
+ $secure = $cookie['secure'] || self::https_check();
setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'], $secure, true);
}
/**
* E-mail address validation.
*
* @param string $email Email address
* @param boolean $dns_check True to check dns
*
* @return boolean True on success, False if address is invalid
*/
public static function check_email($email, $dns_check=true)
{
// Check for invalid characters
if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email)) {
return false;
}
// Check for length limit specified by RFC 5321 (#1486453)
if (strlen($email) > 254) {
return false;
}
$email_array = explode('@', $email);
// Check that there's one @ symbol
if (count($email_array) < 2) {
return false;
}
$domain_part = array_pop($email_array);
$local_part = implode('@', $email_array);
// from PEAR::Validate
$regexp = '&^(?:
("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) #2 OR dot-atom (RFC5322)
$&xi';
if (!preg_match($regexp, $local_part)) {
return false;
}
// Check domain part
if (preg_match('/^\[*(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]*$/', $domain_part)) {
return true; // IP address
}
else {
// If not an IP address
$domain_array = explode('.', $domain_part);
// Not enough parts to be a valid domain
if (sizeof($domain_array) < 2) {
return false;
}
foreach ($domain_array as $part) {
if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
return false;
}
}
$rcube = rcube::get_instance();
if (!$dns_check || !$rcube->config->get('email_dns_check')) {
return true;
}
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
$lookup = array();
@exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
foreach ($lookup as $line) {
if (strpos($line, 'MX preference')) {
return true;
}
}
return false;
}
// find MX record(s)
if (getmxrr($domain_part, $mx_records)) {
return true;
}
// find any DNS record
if (checkdnsrr($domain_part, 'ANY')) {
return true;
}
}
return false;
}
/**
* Check whether the HTTP referer matches the current request
*
* @return boolean True if referer is the same host+path, false if not
*/
public static function check_referer()
{
$uri = parse_url($_SERVER['REQUEST_URI']);
$referer = parse_url(rcube_request_header('Referer'));
return $referer['host'] == rcube_request_header('Host') && $referer['path'] == $uri['path'];
}
/**
* Replacing specials characters to a specific encoding type
*
* @param string Input string
* @param string Encoding type: text|html|xml|js|url
* @param string Replace mode for tags: show|replace|remove
* @param boolean Convert newlines
*
* @return string The quoted string
*/
public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true)
{
static $html_encode_arr = false;
static $js_rep_table = false;
static $xml_rep_table = false;
// encode for HTML output
if ($enctype == 'html') {
if (!$html_encode_arr) {
$html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
unset($html_encode_arr['?']);
}
$encode_arr = $html_encode_arr;
// don't replace quotes and html tags
if ($mode == 'show' || $mode == '') {
$ltpos = strpos($str, '<');
if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) {
unset($encode_arr['"']);
unset($encode_arr['<']);
unset($encode_arr['>']);
unset($encode_arr['&']);
}
}
else if ($mode == 'remove') {
$str = strip_tags($str);
}
$out = strtr($str, $encode_arr);
// avoid douple quotation of &
$out = preg_replace('/&amp;([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
return $newlines ? nl2br($out) : $out;
}
// if the replace tables for XML and JS are not yet defined
if ($js_rep_table === false) {
$js_rep_table = $xml_rep_table = array();
$xml_rep_table['&'] = '&amp;';
// can be increased to support more charsets
for ($c=160; $c<256; $c++) {
$xml_rep_table[chr($c)] = "&#$c;";
}
$xml_rep_table['"'] = '&quot;';
$js_rep_table['"'] = '\\"';
$js_rep_table["'"] = "\\'";
$js_rep_table["\\"] = "\\\\";
// Unicode line and paragraph separators (#1486310)
$js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '&#8232;';
$js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '&#8233;';
}
// encode for javascript use
if ($enctype == 'js') {
return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
}
// encode for plaintext
if ($enctype == 'text') {
return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
}
if ($enctype == 'url') {
return rawurlencode($str);
}
// encode for XML
if ($enctype == 'xml') {
return strtr($str, $xml_rep_table);
}
// no encoding given -> return original string
return $str;
}
/**
* Read input value and convert it for internal use
* Performs stripslashes() and charset conversion if necessary
*
* @param string Field name to read
* @param int Source to get value from (GPC)
* @param boolean Allow HTML tags in field value
* @param string Charset to convert into
*
* @return string Field value or NULL if not available
*/
public static function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
{
$value = NULL;
if ($source == self::INPUT_GET) {
if (isset($_GET[$fname])) {
$value = $_GET[$fname];
}
}
else if ($source == self::INPUT_POST) {
if (isset($_POST[$fname])) {
$value = $_POST[$fname];
}
}
else if ($source == self::INPUT_GPC) {
if (isset($_POST[$fname])) {
$value = $_POST[$fname];
}
else if (isset($_GET[$fname])) {
$value = $_GET[$fname];
}
else if (isset($_COOKIE[$fname])) {
$value = $_COOKIE[$fname];
}
}
return self::parse_input_value($value, $allow_html, $charset);
}
/**
* Parse/validate input value. See self::get_input_value()
* Performs stripslashes() and charset conversion if necessary
*
* @param string Input value
* @param boolean Allow HTML tags in field value
* @param string Charset to convert into
*
* @return string Parsed value
*/
public static function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
{
global $OUTPUT;
if (empty($value)) {
return $value;
}
if (is_array($value)) {
foreach ($value as $idx => $val) {
$value[$idx] = self::parse_input_value($val, $allow_html, $charset);
}
return $value;
}
// strip single quotes if magic_quotes_sybase is enabled
if (ini_get('magic_quotes_sybase')) {
$value = str_replace("''", "'", $value);
}
// strip slashes if magic_quotes enabled
else if (get_magic_quotes_gpc() || get_magic_quotes_runtime()) {
$value = stripslashes($value);
}
// remove HTML tags if not allowed
if (!$allow_html) {
$value = strip_tags($value);
}
$output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null;
// remove invalid characters (#1488124)
if ($output_charset == 'UTF-8') {
$value = rcube_charset::clean($value);
}
// convert to internal charset
if ($charset && $output_charset) {
$value = rcube_charset::convert($value, $output_charset, $charset);
}
return $value;
}
/**
* 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
*
* @return array Hash array with all request parameters
*/
public static function request2param($mode = null, $ignore = 'task|action')
{
$out = array();
$src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
foreach ($src as $key => $value) {
$fname = $key[0] == '_' ? substr($key, 1) : $key;
if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
$out[$fname] = self::get_input_value($key, $mode);
}
}
return $out;
}
/**
* Convert the given string into a valid HTML identifier
* Same functionality as done in app.js with rcube_webmail.html_identifier()
*/
public static function html_identifier($str, $encode=false)
{
if ($encode) {
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
else {
return asciiwords($str, true, '_');
}
}
/**
* Create an edit field for inclusion on a form
*
* @param string col field name
* @param string value field value
* @param array attrib HTML element attributes for field
* @param string type HTML element type (default 'text')
*
* @return string HTML field definition
*/
public static function get_edit_field($col, $value, $attrib, $type = 'text')
{
static $colcounts = array();
$fname = '_'.$col;
$attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
$attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
if ($type == 'checkbox') {
$attrib['value'] = '1';
$input = new html_checkbox($attrib);
}
else if ($type == 'textarea') {
$attrib['cols'] = $attrib['size'];
$input = new html_textarea($attrib);
}
else if ($type == 'select') {
$input = new html_select($attrib);
$input->add('---', '');
$input->add(array_values($attrib['options']), array_keys($attrib['options']));
}
else if ($attrib['type'] == 'password') {
$input = new html_passwordfield($attrib);
}
else {
if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden') {
$attrib['type'] = 'text';
}
$input = new html_inputfield($attrib);
}
// use value from post
if (isset($_POST[$fname])) {
$postvalue = self::get_input_value($fname, self::INPUT_POST, true);
$value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
}
$out = $input->show($value);
return $out;
}
/**
* Replace all css definitions with #container [def]
* and remove css-inlined scripting
*
* @param string CSS source code
* @param string Container ID to use as prefix
*
* @return string Modified CSS source
*/
public static function mod_css_styles($source, $container_id, $allow_remote=false)
{
$last_pos = 0;
$replacements = new rcube_string_replacer;
// ignore the whole block if evil styles are detected
$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! */';
}
// cut out all contents between { and }
while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
$styles = substr($source, $pos+1, $pos2-($pos+1));
// 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)) {
$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;
}
// 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(
array(
'/(^\s*<!--)|(-->\s*$)/',
'/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
'/'.preg_quote($container_id, '/').'\s+body/i',
),
array(
'',
"\\1#$container_id \\2",
$container_id,
),
$source);
// put block contents back in
$styles = $replacements->resolve($styles);
return $styles;
}
/**
* Generate CSS classes from mimetype and filename extension
*
* @param string $mimetype Mimetype
* @param string $filename Filename
*
* @return string CSS classes separated by space
*/
public static function file2class($mimetype, $filename)
{
list($primary, $secondary) = explode('/', $mimetype);
$classes = array($primary ? $primary : 'unknown');
if ($secondary) {
$classes[] = $secondary;
}
if (preg_match('/\.([a-z0-9]+)$/i', $filename, $m)) {
$classes[] = $m[1];
}
return strtolower(join(" ", $classes));
}
/**
* Decode escaped entities used by known XSS exploits.
* See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
*
* @param string CSS content to decode
*
* @return string Decoded string
*/
public static function xss_entity_decode($content)
{
$out = html_entity_decode(html_entity_decode($content));
$out = preg_replace_callback('/\\\([0-9a-f]{4})/i',
array(self, 'xss_entity_decode_callback'), $out);
$out = preg_replace('#/\*.*\*/#Ums', '', $out);
return $out;
}
/**
* preg_replace_callback callback for xss_entity_decode
*
* @param array $matches Result from preg_replace_callback
*
* @return string Decoded entity
*/
public static function xss_entity_decode_callback($matches)
{
return chr(hexdec($matches[1]));
}
/**
* Check if we can process not exceeding memory_limit
*
* @param integer Required amount of memory
*
* @return boolean True if memory won't be exceeded, False otherwise
*/
public static function mem_check($need)
{
$mem_limit = parse_bytes(ini_get('memory_limit'));
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true;
}
/**
* Check if working in SSL mode
*
* @param integer $port HTTPS port number
* @param boolean $use_https Enables 'use_https' option checking
*
* @return boolean
*/
public static function https_check($port=null, $use_https=true)
{
global $RCMAIL;
if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') {
return true;
}
if ($port && $_SERVER['SERVER_PORT'] == $port) {
return true;
}
if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https')) {
return true;
}
return false;
}
/**
* Replaces hostname variables.
*
* @param string $name Hostname
* @param string $host Optional IMAP hostname
*
* @return string Hostname
*/
public static function parse_host($name, $host = '')
{
// %n - host
$n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
- // %d - domain name without first part, e.g. %n=mail.domain.tld, %d=domain.tld
- $d = preg_replace('/^[^\.]+\./', '', $n);
+ // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
+ $t = preg_replace('/^[^\.]+\./', '', $n);
+ // %d - domain name without first part
+ $d = preg_replace('/^[^\.]+\./', '', $_SERVER['HTTP_HOST']);
// %h - IMAP host
$h = $_SESSION['storage_host'] ? $_SESSION['storage_host'] : $host;
// %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
$z = preg_replace('/^[^\.]+\./', '', $h);
// %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided
if (strpos($name, '%s') !== false) {
$user_email = self::get_input_value('_user', self::INPUT_POST);
$user_email = rcube_utils::idn_convert($user_email, true);
$matches = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s);
if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) {
return false;
}
}
- $name = str_replace(array('%n', '%d', '%h', '%z', '%s'), array($n, $d, $h, $z, $s[2]), $name);
+ $name = str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
return $name;
}
/**
* Returns remote IP address and forwarded addresses if found
*
* @return string Remote IP address(es)
*/
public static function remote_ip()
{
$address = $_SERVER['REMOTE_ADDR'];
// append the NGINX X-Real-IP header, if set
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
}
// append the X-Forwarded-For header, if set
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if (!empty($remote_ip)) {
$address .= '(' . implode(',', $remote_ip) . ')';
}
return $address;
}
/**
* Read a specific HTTP request header.
*
* @param string $name Header name
*
* @return mixed Header value or null if not available
*/
public static function request_header($name)
{
if (function_exists('getallheaders')) {
$hdrs = array_change_key_case(getallheaders(), CASE_UPPER);
$key = strtoupper($name);
}
else {
$key = 'HTTP_' . strtoupper(strtr($name, '-', '_'));
$hdrs = array_change_key_case($_SERVER, CASE_UPPER);
}
return $hdrs[$key];
}
/**
* Explode quoted string
*
* @param string Delimiter expression string for preg_match()
* @param string Input string
*
* @return array String items
*/
public static function explode_quoted_string($delimiter, $string)
{
$result = array();
$strlen = strlen($string);
for ($q=$p=$i=0; $i < $strlen; $i++) {
if ($string[$i] == "\"" && $string[$i-1] != "\\") {
$q = $q ? false : true;
}
else if (!$q && preg_match("/$delimiter/", $string[$i])) {
$result[] = substr($string, $p, $i - $p);
$p = $i + 1;
}
}
$result[] = substr($string, $p);
return $result;
}
/**
* Improved equivalent to strtotime()
*
* @param string $date Date string
*
* @return int Unix timestamp
*/
public static function strtotime($date)
{
// check for MS Outlook vCard date format YYYYMMDD
if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) {
return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
}
else if (is_numeric($date)) {
return $date;
}
// support non-standard "GMTXXXX" literal
$date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
// if date parsing fails, we have a date in non-rfc format.
// remove token from the end and try again
while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
$d = explode(' ', $date);
array_pop($d);
if (!$d) {
break;
}
$date = implode(' ', $d);
}
return $ts;
}
/*
* Idn_to_ascii wrapper.
* Intl/Idn modules version of this function doesn't work with e-mail address
*/
public static function idn_to_ascii($str)
{
return self::idn_convert($str, true);
}
/*
* Idn_to_ascii wrapper.
* Intl/Idn modules version of this function doesn't work with e-mail address
*/
public static function idn_to_utf8($str)
{
return self::idn_convert($str, false);
}
public static function idn_convert($input, $is_utf=false)
{
if ($at = strpos($input, '@')) {
$user = substr($input, 0, $at);
$domain = substr($input, $at+1);
}
else {
$domain = $input;
}
$domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
if ($domain === false) {
return '';
}
return $at ? $user . '@' . $domain : $domain;
}
/**
* Split the given string into word tokens
*
* @param string Input to tokenize
* @return array List of tokens
*/
public static function tokenize_string($str)
{
return explode(" ", preg_replace(
array('/[\s;\/+-]+/i', '/(\d)[-.\s]+(\d)/', '/\s\w{1,3}\s/u'),
array(' ', '\\1\\2', ' '),
$str));
}
/**
* Normalize the given string for fulltext search.
* Currently only optimized for Latin-1 characters; to be extended
*
* @param string Input string (UTF-8)
* @param boolean True to return list of words as array
* @return mixed Normalized string or a list of normalized tokens
*/
public static function normalize_string($str, $as_array = false)
{
// split by words
$arr = self::tokenize_string($str);
foreach ($arr as $i => $part) {
if (utf8_encode(utf8_decode($part)) == $part) { // is latin-1 ?
$arr[$i] = utf8_encode(strtr(strtolower(strtr(utf8_decode($part),
'ÇçäâàåéêëèïîìÅÉöôòüûùÿøØáíóúñÑÁÂÀãÃÊËÈÍÎÏÓÔõÕÚÛÙýÝ',
'ccaaaaeeeeiiiaeooouuuyooaiounnaaaaaeeeiiioooouuuyy')),
array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')));
}
else
$arr[$i] = mb_strtolower($part);
}
return $as_array ? $arr : join(" ", $arr);
}
}
diff --git a/lib/ext/Roundcube/rcube_vcard.php b/lib/ext/Roundcube/rcube_vcard.php
index 52545a0..37cd3ab 100644
--- a/lib/ext/Roundcube/rcube_vcard.php
+++ b/lib/ext/Roundcube/rcube_vcard.php
@@ -1,810 +1,810 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_vcard.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2011, 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: |
| Logical representation of a vcard address record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a vcard-based address record
* Provides functions to parse and export vCard data format
*
* @package Addressbook
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_vcard
{
private static $values_decoded = false;
private $raw = array(
'FN' => array(),
'N' => array(array('','','','','')),
);
private static $fieldmap = array(
'phone' => 'TEL',
'birthday' => 'BDAY',
'website' => 'URL',
'notes' => 'NOTE',
'email' => 'EMAIL',
'address' => 'ADR',
'jobtitle' => 'TITLE',
'department' => 'X-DEPARTMENT',
'gender' => 'X-GENDER',
'maidenname' => 'X-MAIDENNAME',
'anniversary' => 'X-ANNIVERSARY',
'assistant' => 'X-ASSISTANT',
'manager' => 'X-MANAGER',
'spouse' => 'X-SPOUSE',
'edit' => 'X-AB-EDIT',
);
private $typemap = array('iPhone' => 'mobile', 'CELL' => 'mobile', 'WORK,FAX' => 'workfax');
private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'BUSINESSFAX' => 'WORK,FAX');
private $addresstypemap = array('BUSINESS' => 'WORK');
private $immap = array('X-JABBER' => 'jabber', 'X-ICQ' => 'icq', 'X-MSN' => 'msn', 'X-AIM' => 'aim', 'X-YAHOO' => 'yahoo', 'X-SKYPE' => 'skype', 'X-SKYPE-USERNAME' => 'skype');
public $business = false;
public $displayname;
public $surname;
public $firstname;
public $middlename;
public $nickname;
public $organization;
public $notes;
public $email = array();
public static $eol = "\r\n";
/**
* Constructor
*/
public function __construct($vcard = null, $charset = RCMAIL_CHARSET, $detect = false, $fieldmap = array())
{
if (!empty($fielmap))
$this->extend_fieldmap($fieldmap);
if (!empty($vcard))
$this->load($vcard, $charset, $detect);
}
/**
* Load record from (internal, unfolded) vcard 3.0 format
*
* @param string vCard string to parse
* @param string Charset of string values
* @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required
*/
public function load($vcard, $charset = RCMAIL_CHARSET, $detect = false)
{
self::$values_decoded = false;
$this->raw = self::vcard_decode($vcard);
// resolve charset parameters
if ($charset == null) {
$this->raw = self::charset_convert($this->raw);
}
// vcard has encoded values and charset should be detected
else if ($detect && self::$values_decoded &&
($detected_charset = self::detect_encoding(self::vcard_encode($this->raw))) && $detected_charset != RCMAIL_CHARSET) {
$this->raw = self::charset_convert($this->raw, $detected_charset);
}
// consider FN empty if the same as the primary e-mail address
if ($this->raw['FN'][0][0] == $this->raw['EMAIL'][0][0])
$this->raw['FN'][0][0] = '';
// find well-known address fields
$this->displayname = $this->raw['FN'][0][0];
$this->surname = $this->raw['N'][0][0];
$this->firstname = $this->raw['N'][0][1];
$this->middlename = $this->raw['N'][0][2];
$this->nickname = $this->raw['NICKNAME'][0][0];
$this->organization = $this->raw['ORG'][0][0];
$this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization));
foreach ((array)$this->raw['EMAIL'] as $i => $raw_email)
$this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email;
// make the pref e-mail address the first entry in $this->email
$pref_index = $this->get_type_index('EMAIL', 'pref');
if ($pref_index > 0) {
$tmp = $this->email[0];
$this->email[0] = $this->email[$pref_index];
$this->email[$pref_index] = $tmp;
}
}
/**
* Return vCard data as associative array to be unsed in Roundcube address books
*
* @return array Hash array with key-value pairs
*/
public function get_assoc()
{
$out = array('name' => $this->displayname);
$typemap = $this->typemap;
// copy name fields to output array
foreach (array('firstname','surname','middlename','nickname','organization') as $col) {
if (strlen($this->$col))
$out[$col] = $this->$col;
}
if ($this->raw['N'][0][3])
$out['prefix'] = $this->raw['N'][0][3];
if ($this->raw['N'][0][4])
$out['suffix'] = $this->raw['N'][0][4];
// convert from raw vcard data into associative data for Roundcube
foreach (array_flip(self::$fieldmap) as $tag => $col) {
foreach ((array)$this->raw[$tag] as $i => $raw) {
if (is_array($raw)) {
$k = -1;
$key = $col;
$subtype = '';
if (!empty($raw['type'])) {
$combined = join(',', self::array_filter((array)$raw['type'], 'internet,pref', true));
$subtype = $typemap[$combined] ? $typemap[$combined] : ($typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]));
while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref'))
$subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
}
// read vcard 2.1 subtype
if (!$subtype) {
foreach ($raw as $k => $v) {
if (!is_numeric($k) && $v === true && !in_array(strtolower($k), array('pref','internet','voice','base64'))) {
$subtype = $typemap[$k] ? $typemap[$k] : strtolower($k);
break;
}
}
}
// force subtype if none set
if (!$subtype && preg_match('/^(email|phone|address|website)/', $key))
$subtype = 'other';
if ($subtype)
$key .= ':' . $subtype;
// split ADR values into assoc array
if ($tag == 'ADR') {
list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
$out[$key][] = $value;
}
else
$out[$key][] = $raw[0];
}
else {
$out[$col][] = $raw;
}
}
}
// handle special IM fields as used by Apple
foreach ($this->immap as $tag => $type) {
foreach ((array)$this->raw[$tag] as $i => $raw) {
$out['im:'.$type][] = $raw[0];
}
}
// copy photo data
if ($this->raw['PHOTO'])
$out['photo'] = $this->raw['PHOTO'][0][0];
return $out;
}
/**
* Convert the data structure into a vcard 3.0 string
*/
public function export($folded = true)
{
$vcard = self::vcard_encode($this->raw);
return $folded ? self::rfc2425_fold($vcard) : $vcard;
}
/**
* Clear the given fields in the loaded vcard data
*
* @param array List of field names to be reset
*/
public function reset($fields = null)
{
if (!$fields)
$fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
foreach ($fields as $f)
unset($this->raw[$f]);
if (!$this->raw['N'])
$this->raw['N'] = array(array('','','','',''));
if (!$this->raw['FN'])
$this->raw['FN'] = array();
$this->email = array();
}
/**
* Setter for address record fields
*
* @param string Field name
* @param string Field value
* @param string Type/section name
*/
public function set($field, $value, $type = 'HOME')
{
$field = strtolower($field);
$type_uc = strtoupper($type);
$typemap = array_flip($this->typemap);
switch ($field) {
case 'name':
case 'displayname':
$this->raw['FN'][0][0] = $value;
break;
case 'surname':
$this->raw['N'][0][0] = $value;
break;
case 'firstname':
$this->raw['N'][0][1] = $value;
break;
case 'middlename':
$this->raw['N'][0][2] = $value;
break;
case 'prefix':
$this->raw['N'][0][3] = $value;
break;
case 'suffix':
$this->raw['N'][0][4] = $value;
break;
case 'nickname':
$this->raw['NICKNAME'][0][0] = $value;
break;
case 'organization':
$this->raw['ORG'][0][0] = $value;
break;
case 'photo':
if (strpos($value, 'http:') === 0) {
// TODO: fetch file from URL and save it locally?
$this->raw['PHOTO'][0] = array(0 => $value, 'url' => true);
}
else {
$this->raw['PHOTO'][0] = array(0 => $value, 'base64' => (bool) preg_match('![^a-z0-9/=+-]!i', $value));
}
break;
case 'email':
$this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc)));
$this->email[] = $value;
break;
case 'im':
// save IM subtypes into extension fields
$typemap = array_flip($this->immap);
if ($field = $typemap[strtolower($type)])
$this->raw[$field][] = array(0 => $value);
break;
case 'birthday':
case 'anniversary':
if (($val = rcube_strtotime($value)) && ($fn = self::$fieldmap[$field]))
$this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
break;
case 'address':
if ($this->addresstypemap[$type_uc])
$type = $this->addresstypemap[$type_uc];
$value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
// fall through if not empty
if (!strlen(join('', $value)))
break;
default:
if ($field == 'phone' && $this->phonetypemap[$type_uc])
$type = $this->phonetypemap[$type_uc];
if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) {
$index = count($this->raw[$tag]);
$this->raw[$tag][$index] = (array)$value;
if ($type)
$this->raw[$tag][$index]['type'] = explode(',', ($typemap[$type] ? $typemap[$type] : $type));
}
break;
}
}
/**
* Setter for individual vcard properties
*
* @param string VCard tag name
* @param array Value-set of this vcard property
* @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set
*/
public function set_raw($tag, $value, $append = false)
{
$index = $append ? count($this->raw[$tag]) : 0;
$this->raw[$tag][$index] = (array)$value;
}
/**
* Find index with the '$type' attribute
*
* @param string Field name
* @return int Field index having $type set
*/
private function get_type_index($field, $type = 'pref')
{
$result = 0;
if ($this->raw[$field]) {
foreach ($this->raw[$field] as $i => $data) {
if (is_array($data['type']) && in_array_nocase('pref', $data['type']))
$result = $i;
}
}
return $result;
}
/**
* Convert a whole vcard (array) to UTF-8.
* If $force_charset is null, each member value that has a charset parameter will be converted
*/
private static function charset_convert($card, $force_charset = null)
{
foreach ($card as $key => $node) {
foreach ($node as $i => $subnode) {
if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) {
foreach ($subnode as $j => $value) {
if (is_numeric($j) && is_string($value))
$card[$key][$i][$j] = rcube_charset::convert($value, $charset);
}
unset($card[$key][$i]['charset']);
}
}
}
return $card;
}
/**
* Extends fieldmap definition
*/
public function extend_fieldmap($map)
{
if (is_array($map))
self::$fieldmap = array_merge($map, self::$fieldmap);
}
/**
* Factory method to import a vcard file
*
* @param string vCard file content
* @return array List of rcube_vcard objects
*/
public static function import($data)
{
$out = array();
// check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
if (preg_match('/charset=/i', substr($data, 0, 2048)))
$charset = null;
// detect charset and convert to utf-8
else if (($charset = self::detect_encoding($data)) && $charset != RCMAIL_CHARSET) {
$data = rcube_charset::convert($data, $charset);
$data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM
$charset = RCMAIL_CHARSET;
}
$vcard_block = '';
$in_vcard_block = false;
foreach (preg_split("/[\r\n]+/", $data) as $i => $line) {
if ($in_vcard_block && !empty($line))
$vcard_block .= $line . "\n";
$line = trim($line);
if (preg_match('/^END:VCARD$/i', $line)) {
// parse vcard
$obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap);
if (!empty($obj->displayname) || !empty($obj->email))
$out[] = $obj;
$in_vcard_block = false;
}
else if (preg_match('/^BEGIN:VCARD$/i', $line)) {
$vcard_block = $line . "\n";
$in_vcard_block = true;
}
}
return $out;
}
/**
* Normalize vcard data for better parsing
*
* @param string vCard block
* @return string Cleaned vcard block
*/
private static function cleanup($vcard)
{
// Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
$vcard = preg_replace(
'/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
'\2;type=\5\3:\4',
$vcard);
// convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
$vcard = preg_replace_callback(
'/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
array('self', 'x_abrelatednames_callback'),
$vcard);
// Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
$vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
// convert X-WAB-GENDER to X-GENDER
if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
$value = $matches[1] == '2' ? 'male' : 'female';
$vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
}
// if N doesn't have any semicolons, add some
$vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
return $vcard;
}
private static function x_abrelatednames_callback($matches)
{
return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4];
}
private static function rfc2425_fold_callback($matches)
{
// chunk_split string and avoid lines breaking multibyte characters
$c = 71;
$out .= substr($matches[1], 0, $c);
for ($n = $c; $c < strlen($matches[1]); $c++) {
// break if length > 75 or mutlibyte character starts after position 71
if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) {
$out .= "\r\n ";
$n = 0;
}
$out .= $matches[1][$c];
$n++;
}
return $out;
}
public static function rfc2425_fold($val)
{
return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val);
}
/**
* Decodes a vcard block (vcard 3.0 format, unfolded)
* into an array structure
*
* @param string vCard block to parse
* @return array Raw data structure
*/
private static function vcard_decode($vcard)
{
// Perform RFC2425 line unfolding and split lines
$vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
$lines = explode("\n", $vcard);
$data = array();
for ($i=0; $i < count($lines); $i++) {
if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line))
continue;
if (preg_match('/^(BEGIN|END)$/i', $line[1]))
continue;
// convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
if (($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) {
$line[1] = $regs2[1];
foreach (explode(';', $regs2[2]) as $prop)
$line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
}
if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) {
$entry = array();
$field = strtoupper($regs2[1][0]);
$enc = null;
foreach($regs2[1] as $attrid => $attr) {
if ((list($key, $value) = explode('=', $attr)) && $value) {
$value = trim($value);
if ($key == 'ENCODING') {
// add next line(s) to value string if QP line end detected
if ($value == 'QUOTED-PRINTABLE') {
while (preg_match('/=$/', $lines[$i]))
$line[2] .= "\n" . $lines[++$i];
}
$enc = $value;
}
else {
$lc_key = strtolower($key);
$entry[$lc_key] = array_merge((array)$entry[$lc_key], (array)self::vcard_unquote($value, ','));
}
}
else if ($attrid > 0) {
$entry[strtolower($key)] = true; // true means attr without =value
}
}
// decode value
if ($enc || !empty($entry['base64'])) {
// save encoding type (#1488432)
if ($enc == 'B') {
$entry['encoding'] = 'B';
// should we use vCard 3.0 instead?
// $entry['base64'] = true;
}
$line[2] = self::decode_value($line[2], $enc ? $enc : 'base64');
}
if ($enc != 'B' && empty($entry['base64'])) {
$line[2] = self::vcard_unquote($line[2]);
}
$entry = array_merge($entry, (array) $line[2]);
$data[$field][] = $entry;
}
}
unset($data['VERSION']);
return $data;
}
/**
* Decode a given string with the encoding rule from ENCODING attributes
*
* @param string String to decode
* @param string Encoding type (quoted-printable and base64 supported)
* @return string Decoded 8bit value
*/
private static function decode_value($value, $encoding)
{
switch (strtolower($encoding)) {
case 'quoted-printable':
self::$values_decoded = true;
return quoted_printable_decode($value);
case 'base64':
case 'b':
self::$values_decoded = true;
return base64_decode($value);
default:
return $value;
}
}
/**
* Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
*
* @param array Raw data structure to encode
* @return string vCard encoded string
*/
static function vcard_encode($data)
{
foreach((array)$data as $type => $entries) {
/* valid N has 5 properties */
while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5)
$entries[0][] = "";
// make sure FN is not empty (required by RFC2426)
if ($type == "FN" && empty($entries))
$entries[0] = $data['EMAIL'][0][0];
foreach((array)$entries as $entry) {
$attr = '';
if (is_array($entry)) {
$value = array();
foreach($entry as $attrname => $attrvalues) {
if (is_int($attrname)) {
if (!empty($entry['base64']) || $entry['encoding'] == 'B') {
$attrvalues = base64_encode($attrvalues);
}
$value[] = $attrvalues;
}
else if (is_bool($attrvalues)) {
if ($attrvalues) {
$attr .= strtoupper(";$attrname"); // true means just tag, not tag=value, as in PHOTO;BASE64:...
}
}
else {
foreach((array)$attrvalues as $attrvalue)
$attr .= strtoupper(";$attrname=") . self::vcard_quote($attrvalue, ',');
}
}
}
else {
$value = $entry;
}
// skip empty entries
if (self::is_empty($value))
continue;
$vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol;
}
}
return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD';
}
/**
* Join indexed data array to a vcard quoted string
*
* @param array Field data
* @param string Separator
* @return string Joined and quoted string
*/
private static function vcard_quote($s, $sep = ';')
{
if (is_array($s)) {
foreach($s as $part) {
$r[] = self::vcard_quote($part, $sep);
}
return(implode($sep, (array)$r));
}
else {
return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;'));
}
}
/**
* Split quoted string
*
* @param string vCard string to split
* @param string Separator char/string
- * @return array List with splitted values
+ * @return array List with splited values
*/
private static function vcard_unquote($s, $sep = ';')
{
// break string into parts separated by $sep, but leave escaped $sep alone
if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) {
foreach($parts as $s) {
$result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep);
}
return $result;
}
else {
return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';', '\:' => ':'));
}
}
/**
* Check if vCard entry is empty: empty string or an array with
* all entries empty.
*
* @param mixed $value Attribute value (string or array)
*
* @return bool True if the value is empty, False otherwise
*/
private static function is_empty($value)
{
foreach ((array)$value as $v) {
if (((string)$v) !== '') {
return false;
}
}
return true;
}
/**
* Extract array values by a filter
*
* @param array Array to filter
* @param keys Array or comma separated list of values to keep
* @param boolean Invert key selection: remove the listed values
* @return array The filtered array
*/
private static function array_filter($arr, $values, $inverse = false)
{
if (!is_array($values))
$values = explode(',', $values);
$result = array();
$keep = array_flip((array)$values);
foreach ($arr as $key => $val)
if ($inverse != isset($keep[strtolower($val)]))
$result[$key] = $val;
return $result;
}
/**
* Returns UNICODE type based on BOM (Byte Order Mark)
*
* @param string Input string to test
* @return string Detected encoding
*/
private static function detect_encoding($string)
{
if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian
if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian
if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE'; // Big Endian
if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE'; // Little Endian
if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';
// heuristics
if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE';
if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE';
if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE';
if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE';
// use mb_detect_encoding()
$encodings = array('UTF-8', 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3',
'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9',
'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16',
'WINDOWS-1252', 'WINDOWS-1251', 'BIG5', 'GB2312');
if (function_exists('mb_detect_encoding') && ($enc = mb_detect_encoding($string, $encodings)))
return $enc;
// No match, check for UTF-8
// from http://w3.org/International/questions/qa-forms-utf-8.html
if (preg_match('/\A(
[\x09\x0A\x0D\x20-\x7E]
| [\xC2-\xDF][\x80-\xBF]
| \xE0[\xA0-\xBF][\x80-\xBF]
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
| \xED[\x80-\x9F][\x80-\xBF]
| \xF0[\x90-\xBF][\x80-\xBF]{2}
| [\xF1-\xF3][\x80-\xBF]{3}
| \xF4[\x80-\x8F][\x80-\xBF]{2}
)*\z/xs', substr($string, 0, 2048)))
return 'UTF-8';
return rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); # fallback to Latin-1
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 19, 9:39 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
436223
Default Alt Text
(444 KB)

Event Timeline