Page MenuHomePhorge

No OneTemporary

Size
1 MB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
This document is not UTF8. It was detected as ISO-8859-1 (Latin 1) and converted to UTF8 for display.
diff --git a/lib/ext/Roundcube/bootstrap.php b/lib/ext/Roundcube/bootstrap.php
index 27c124a..fe9c389 100644
--- a/lib/ext/Roundcube/bootstrap.php
+++ b/lib/ext/Roundcube/bootstrap.php
@@ -1,499 +1,497 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | Copyright (C) 2005-2014, 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: |
| Roundcube Framework Initialization |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Roundcube Framework Initialization
*
* @package Framework
* @subpackage Core
*/
$config = array(
'error_reporting' => E_ALL & ~E_NOTICE & ~E_STRICT,
// Some users are not using Installer, so we'll check some
// critical PHP settings here. Only these, which doesn't provide
// an error/warning in the logs later. See (#1486307).
'mbstring.func_overload' => 0,
'magic_quotes_runtime' => false,
'magic_quotes_sybase' => false, // #1488506
);
// check these additional ini settings if not called via CLI
if (php_sapi_name() != 'cli') {
$config += array(
'suhosin.session.encrypt' => false,
'file_uploads' => true,
);
}
foreach ($config as $optname => $optval) {
$ini_optval = filter_var(ini_get($optname), is_bool($optval) ? FILTER_VALIDATE_BOOLEAN : FILTER_VALIDATE_INT);
if ($optval != $ini_optval && @ini_set($optname, $optval) === false) {
$error = "ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n"
. "Check your PHP configuration (including php_admin_flag).";
if (defined('STDERR')) fwrite(STDERR, $error); else echo $error;
exit(1);
}
}
// framework constants
-define('RCUBE_VERSION', '1.0.2');
+define('RCUBE_VERSION', '1.1-git');
define('RCUBE_CHARSET', 'UTF-8');
if (!defined('RCUBE_LIB_DIR')) {
- define('RCUBE_LIB_DIR', dirname(__FILE__).DIRECTORY_SEPARATOR);
+ define('RCUBE_LIB_DIR', __DIR__ . '/');
}
if (!defined('RCUBE_INSTALL_PATH')) {
define('RCUBE_INSTALL_PATH', RCUBE_LIB_DIR);
}
if (!defined('RCUBE_CONFIG_DIR')) {
define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
}
if (!defined('RCUBE_PLUGINS_DIR')) {
define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/');
}
if (!defined('RCUBE_LOCALIZATION_DIR')) {
define('RCUBE_LOCALIZATION_DIR', RCUBE_INSTALL_PATH . 'localization/');
}
// set internal encoding for mbstring extension
if (extension_loaded('mbstring')) {
mb_internal_encoding(RCUBE_CHARSET);
@mb_regex_encoding(RCUBE_CHARSET);
}
// make sure the Roundcube lib directory is in the include_path
$rcube_path = realpath(RCUBE_LIB_DIR . '..');
$sep = PATH_SEPARATOR;
$regexp = "!(^|$sep)" . preg_quote($rcube_path, '!') . "($sep|\$)!";
$path = ini_get('include_path');
if (!preg_match($regexp, $path)) {
set_include_path($path . PATH_SEPARATOR . $rcube_path);
}
// Register autoloader
spl_autoload_register('rcube_autoload');
// set PEAR error handling (will also load the PEAR main class)
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'rcube_pear_error');
/**
* 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;
}
/**
* 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 slashes at the end of the string
*/
function unslashify($str)
{
return preg_replace('/\/+$/', '', $str);
}
/**
* 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) && is_array($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);
}
/**
* Check if a string contains only ascii characters
*
* @param string $str String to check
* @param bool $control_chars Includes control characters
*
* @return bool
*/
function is_ascii($str, $control_chars = true)
{
$regexp = $control_chars ? '/[^\x00-\x7F]/' : '/[^\x20-\x7E]/';
return preg_match($regexp, $str) ? false : true;
}
/**
* 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;
}
/**
* Format e-mail address
*
* @param string $email E-mail address
*
* @return string Formatted e-mail address
*/
function format_email($email)
{
$email = trim($email);
$parts = explode('@', $email);
$count = count($parts);
if ($count > 1) {
$parts[$count-1] = mb_strtolower($parts[$count-1]);
$email = implode('@', $parts);
}
return $email;
}
/**
* Fix version number so it can be used correctly in version_compare()
*
* @param string $version Version number string
*
* @param return Version number string
*/
function version_parse($version)
{
return str_replace(
array('-stable', '-git'),
array('.0', '.99'),
$version);
}
/**
* 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(
'/Mail_(.+)/',
'/Net_(.+)/',
'/Auth_(.+)/',
'/^html_.+/',
- '/^rcube(.*)/',
- '/^utf8$/',
+ '/^rcube(.*)/'
),
array(
'Mail/\\1',
'Net/\\1',
'Auth/\\1',
'Roundcube/html',
- 'Roundcube/rcube\\1',
- 'utf8.class',
+ 'Roundcube/rcube\\1'
),
$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/html.php b/lib/ext/Roundcube/html.php
index 31bacbf..f18cad0 100644
--- a/lib/ext/Roundcube/html.php
+++ b/lib/ext/Roundcube/html.php
@@ -1,904 +1,914 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Helper class to create valid XHTML code |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class for HTML code creation
*
* @package Framework
* @subpackage View
*/
class html
{
protected $tagname;
protected $attrib = array();
protected $allowed = array();
protected $content;
public static $doctype = 'xhtml';
public static $lc_tags = true;
- public static $common_attrib = array('id','class','style','title','align','unselectable');
+ public static $common_attrib = array('id','class','style','title','align','unselectable','tabindex','role');
public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script');
/**
* Constructor
*
* @param array $attrib Hash array with tag attributes
*/
public function __construct($attrib = array())
{
if (is_array($attrib)) {
$this->attrib = $attrib;
}
}
/**
* Return the tag code
*
* @return string The finally composed HTML tag
*/
public function show()
{
return self::tag($this->tagname, $this->attrib, $this->content, array_merge(self::$common_attrib, $this->allowed));
}
/****** STATIC METHODS *******/
/**
* Generic method to create a HTML tag
*
* @param string $tagname Tag name
* @param array $attrib Tag attributes as key/value pairs
* @param string $content Optinal Tag content (creates a container tag)
* @param array $allowed_attrib List with allowed attributes, omit to allow all
* @return string The XHTML tag
*/
public static function tag($tagname, $attrib = array(), $content = null, $allowed_attrib = null)
{
if (is_string($attrib))
$attrib = array('class' => $attrib);
$inline_tags = array('a','span','img');
$suffix = $attrib['nl'] || ($content && $attrib['nl'] !== false && !in_array($tagname, $inline_tags)) ? "\n" : '';
$tagname = self::$lc_tags ? strtolower($tagname) : $tagname;
if (isset($content) || in_array($tagname, self::$containers)) {
$suffix = $attrib['noclose'] ? $suffix : '</' . $tagname . '>' . $suffix;
unset($attrib['noclose'], $attrib['nl']);
return '<' . $tagname . self::attrib_string($attrib, $allowed_attrib) . '>' . $content . $suffix;
}
else {
return '<' . $tagname . self::attrib_string($attrib, $allowed_attrib) . '>' . $suffix;
}
}
/**
*
*/
public static function doctype($type)
{
$doctypes = array(
'html5' => '<!DOCTYPE html>',
'xhtml' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
);
if ($doctypes[$type]) {
self::$doctype = preg_replace('/-\w+$/', '', $type);
return $doctypes[$type];
}
return '';
}
/**
* Derrived method for <div> containers
*
* @param mixed $attr Hash array with tag attributes or string with class name
* @param string $cont Div content
* @return string HTML code
* @see html::tag()
*/
public static function div($attr = null, $cont = null)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
return self::tag('div', $attr, $cont, array_merge(self::$common_attrib, array('onclick')));
}
/**
* Derrived method for <p> blocks
*
* @param mixed $attr Hash array with tag attributes or string with class name
* @param string $cont Paragraph content
* @return string HTML code
* @see html::tag()
*/
public static function p($attr = null, $cont = null)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
return self::tag('p', $attr, $cont, self::$common_attrib);
}
/**
* Derrived method to create <img />
*
* @param mixed $attr Hash array with tag attributes or string with image source (src)
* @return string HTML code
* @see html::tag()
*/
public static function img($attr = null)
{
if (is_string($attr)) {
$attr = array('src' => $attr);
}
return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib,
array('src','alt','width','height','border','usemap','onclick','onerror')));
}
/**
* Derrived method for link tags
*
* @param mixed $attr Hash array with tag attributes or string with link location (href)
* @param string $cont Link content
* @return string HTML code
* @see html::tag()
*/
public static function a($attr, $cont)
{
if (is_string($attr)) {
$attr = array('href' => $attr);
}
return self::tag('a', $attr, $cont, array_merge(self::$common_attrib,
array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
}
/**
* Derrived method for inline span tags
*
* @param mixed $attr Hash array with tag attributes or string with class name
* @param string $cont Tag content
* @return string HTML code
* @see html::tag()
*/
public static function span($attr, $cont)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
return self::tag('span', $attr, $cont, self::$common_attrib);
}
/**
* Derrived method for form element labels
*
* @param mixed $attr Hash array with tag attributes or string with 'for' attrib
* @param string $cont Tag content
* @return string HTML code
* @see html::tag()
*/
public static function label($attr, $cont)
{
if (is_string($attr)) {
$attr = array('for' => $attr);
}
return self::tag('label', $attr, $cont, array_merge(self::$common_attrib, array('for')));
}
/**
* Derrived method to create <iframe></iframe>
*
* @param mixed $attr Hash array with tag attributes or string with frame source (src)
* @return string HTML code
* @see html::tag()
*/
public static function iframe($attr = null, $cont = null)
{
if (is_string($attr)) {
$attr = array('src' => $attr);
}
return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib,
- array('src','name','width','height','border','frameborder','onload')));
+ array('src','name','width','height','border','frameborder','onload','allowfullscreen')));
}
/**
* Derrived method to create <script> tags
*
* @param mixed $attr Hash array with tag attributes or string with script source (src)
* @param string $cont Javascript code to be placed as tag content
* @return string HTML code
* @see html::tag()
*/
public static function script($attr, $cont = null)
{
if (is_string($attr)) {
$attr = array('src' => $attr);
}
if ($cont) {
if (self::$doctype == 'xhtml')
$cont = "\n/* <![CDATA[ */\n" . $cont . "\n/* ]]> */\n";
else
$cont = "\n" . $cont . "\n";
}
return self::tag('script', $attr + array('type' => 'text/javascript', 'nl' => true),
$cont, array_merge(self::$common_attrib, array('src','type','charset')));
}
/**
* Derrived method for line breaks
*
* @return string HTML code
* @see html::tag()
*/
public static function br($attrib = array())
{
return self::tag('br', $attrib);
}
/**
* Create string with attributes
*
* @param array $attrib Associative arry with tag attributes
* @param array $allowed List of allowed attributes
* @return string Valid attribute string
*/
public static function attrib_string($attrib = array(), $allowed = null)
{
if (empty($attrib)) {
return '';
}
$allowed_f = array_flip((array)$allowed);
$attrib_arr = array();
foreach ($attrib as $key => $value) {
// skip size if not numeric
if ($key == 'size' && !is_numeric($value)) {
continue;
}
// ignore "internal" or empty attributes
if ($key == 'nl' || $value === null) {
continue;
}
- // ignore not allowed attributes
+ // ignore not allowed attributes, except aria-* and data-*
if (!empty($allowed)) {
$is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0;
- if (!isset($allowed_f[$key]) && (!$is_data_attr || !isset($allowed_f['data-*']))) {
+ $is_aria_attr = @substr_compare($key, 'aria-', 0, 5) === 0;
+ if (!$is_aria_attr && !$is_data_attr && !isset($allowed_f[$key])) {
continue;
}
}
// skip empty eventhandlers
if (preg_match('/^on[a-z]+/', $key) && !$value) {
continue;
}
// attributes with no value
if (in_array($key, array('checked', 'multiple', 'disabled', 'selected', 'autofocus'))) {
if ($value) {
$attrib_arr[] = $key . '="' . $key . '"';
}
}
else {
$attrib_arr[] = $key . '="' . self::quote($value) . '"';
}
}
return count($attrib_arr) ? ' '.implode(' ', $attrib_arr) : '';
}
/**
* Convert a HTML attribute string attributes to an associative array (name => value)
*
* @param string Input string
* @return array Key-value pairs of parsed attributes
*/
public static function parse_attrib_string($str)
{
$attrib = array();
$regexp = '/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui';
preg_match_all($regexp, stripslashes($str), $regs, PREG_SET_ORDER);
// convert attributes to an associative array (name => value)
if ($regs) {
foreach ($regs as $attr) {
$attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
}
}
return $attrib;
}
/**
* Replacing specials characters in html attribute value
*
* @param string $str Input string
*
* @return string The quoted string
*/
public static function quote($str)
{
static $flags;
if (!$flags) {
$flags = ENT_COMPAT;
if (defined('ENT_SUBSTITUTE')) {
$flags |= ENT_SUBSTITUTE;
}
}
return @htmlspecialchars($str, $flags, RCUBE_CHARSET);
}
}
/**
* Class to create an HTML input field
*
* @package Framework
* @subpackage View
*/
class html_inputfield extends html
{
protected $tagname = 'input';
protected $type = 'text';
protected $allowed = array(
'type','name','value','size','tabindex','autocapitalize','required',
'autocomplete','checked','onchange','onclick','disabled','readonly',
'spellcheck','results','maxlength','src','multiple','accept',
'placeholder','autofocus',
);
/**
* Object constructor
*
* @param array $attrib Associative array with tag attributes
*/
public function __construct($attrib = array())
{
if (is_array($attrib)) {
$this->attrib = $attrib;
}
if ($attrib['type']) {
$this->type = $attrib['type'];
}
}
/**
* Compose input tag
*
* @param string $value Field value
* @param array $attrib Additional attributes to override
* @return string HTML output
*/
public function show($value = null, $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// set value attribute
if ($value !== null) {
$this->attrib['value'] = $value;
}
// set type
$this->attrib['type'] = $this->type;
return parent::show();
}
}
/**
* Class to create an HTML password field
*
* @package Framework
* @subpackage View
*/
class html_passwordfield extends html_inputfield
{
protected $type = 'password';
}
/**
* Class to create an hidden HTML input field
*
* @package Framework
* @subpackage View
*/
class html_hiddenfield extends html
{
protected $tagname = 'input';
protected $type = 'hidden';
protected $fields_arr = array();
protected $allowed = array('type','name','value','onchange','disabled','readonly');
/**
* Constructor
*
* @param array $attrib Named tag attributes
*/
public function __construct($attrib = null)
{
if (is_array($attrib)) {
$this->add($attrib);
}
}
/**
* Add a hidden field to this instance
*
* @param array $attrib Named tag attributes
*/
public function add($attrib)
{
$this->fields_arr[] = $attrib;
}
/**
* Create HTML code for the hidden fields
*
* @return string Final HTML code
*/
public function show()
{
$out = '';
foreach ($this->fields_arr as $attrib) {
$out .= self::tag($this->tagname, array('type' => $this->type) + $attrib);
}
return $out;
}
}
/**
* Class to create HTML radio buttons
*
* @package Framework
* @subpackage View
*/
class html_radiobutton extends html_inputfield
{
protected $type = 'radio';
/**
* Get HTML code for this object
*
* @param string $value Value of the checked field
* @param array $attrib Additional attributes to override
* @return string HTML output
*/
public function show($value = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// set value attribute
$this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']);
return parent::show();
}
}
/**
* Class to create HTML checkboxes
*
* @package Framework
* @subpackage View
*/
class html_checkbox extends html_inputfield
{
protected $type = 'checkbox';
/**
* Get HTML code for this object
*
* @param string $value Value of the checked field
* @param array $attrib Additional attributes to override
* @return string HTML output
*/
public function show($value = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// set value attribute
$this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']);
return parent::show();
}
}
/**
* Class to create an HTML textarea
*
* @package Framework
* @subpackage View
*/
class html_textarea extends html
{
protected $tagname = 'textarea';
protected $allowed = array('name','rows','cols','wrap','tabindex',
'onchange','disabled','readonly','spellcheck');
/**
* Get HTML code for this object
*
* @param string $value Textbox value
* @param array $attrib Additional attributes to override
* @return string HTML output
*/
public function show($value = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// take value attribute as content
if (empty($value) && !empty($this->attrib['value'])) {
$value = $this->attrib['value'];
}
// make shure we don't print the value attribute
if (isset($this->attrib['value'])) {
unset($this->attrib['value']);
}
if (!empty($value) && empty($this->attrib['is_escaped'])) {
$value = self::quote($value);
}
return self::tag($this->tagname, $this->attrib, $value,
array_merge(self::$common_attrib, $this->allowed));
}
}
/**
* Builder for HTML drop-down menus
* Syntax:<pre>
* // create instance. arguments are used to set attributes of select-tag
* $select = new html_select(array('name' => 'fieldname'));
*
* // add one option
* $select->add('Switzerland', 'CH');
*
* // add multiple options
* $select->add(array('Switzerland','Germany'), array('CH','DE'));
*
* // generate pulldown with selection 'Switzerland' and return html-code
* // as second argument the same attributes available to instanciate can be used
* print $select->show('CH');
* </pre>
*
* @package Framework
* @subpackage View
*/
class html_select extends html
{
protected $tagname = 'select';
protected $options = array();
protected $allowed = array('name','size','tabindex','autocomplete',
'multiple','onchange','disabled','rel');
/**
* Add a new option to this drop-down
*
* @param mixed $names Option name or array with option names
* @param mixed $values Option value or array with option values
* @param array $attrib Additional attributes for the option entry
*/
public function add($names, $values = null, $attrib = array())
{
if (is_array($names)) {
foreach ($names as $i => $text) {
$this->options[] = array('text' => $text, 'value' => $values[$i]) + $attrib;
}
}
else {
$this->options[] = array('text' => $names, 'value' => $values) + $attrib;
}
}
/**
* Get HTML code for this object
*
* @param string $select Value of the selection option
* @param array $attrib Additional attributes to override
* @return string HTML output
*/
public function show($select = array(), $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
$this->content = "\n";
$select = (array)$select;
foreach ($this->options as $option) {
$attr = array(
'value' => $option['value'],
'selected' => (in_array($option['value'], $select, true) ||
in_array($option['text'], $select, true)) ? 1 : null);
$option_content = $option['text'];
if (empty($this->attrib['is_escaped'])) {
$option_content = self::quote($option_content);
}
$this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected'));
}
return parent::show();
}
}
/**
* Class to build an HTML table
*
* @package Framework
* @subpackage View
*/
class html_table extends html
{
protected $tagname = 'table';
protected $allowed = array('id','class','style','width','summary',
'cellpadding','cellspacing','border');
private $header = array();
private $rows = array();
private $rowindex = 0;
private $colindex = 0;
/**
* Constructor
*
* @param array $attrib Named tag attributes
*/
public function __construct($attrib = array())
{
$default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => '0') : array();
$this->attrib = array_merge($attrib, $default_attrib);
if (!empty($attrib['tagname']) && $attrib['tagname'] != 'table') {
$this->tagname = $attrib['tagname'];
$this->allowed = self::$common_attrib;
}
}
/**
* Add a table cell
*
* @param array $attr Cell attributes
* @param string $cont Cell content
*/
public function add($attr, $cont)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
$cell = new stdClass;
$cell->attrib = $attr;
$cell->content = $cont;
$this->rows[$this->rowindex]->cells[$this->colindex] = $cell;
$this->colindex += max(1, intval($attr['colspan']));
if ($this->attrib['cols'] && $this->colindex >= $this->attrib['cols']) {
$this->add_row();
}
}
/**
* Add a table header cell
*
* @param array $attr Cell attributes
* @param string $cont Cell content
*/
public function add_header($attr, $cont)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
$cell = new stdClass;
$cell->attrib = $attr;
$cell->content = $cont;
$this->header[] = $cell;
}
/**
* Remove a column from a table
* Useful for plugins making alterations
*
* @param string $class
*/
public function remove_column($class)
{
// Remove the header
foreach ($this->header as $index=>$header){
if ($header->attrib['class'] == $class){
unset($this->header[$index]);
break;
}
}
// Remove cells from rows
foreach ($this->rows as $i=>$row){
foreach ($row->cells as $j=>$cell){
if ($cell->attrib['class'] == $class){
unset($this->rows[$i]->cells[$j]);
break;
}
}
}
}
/**
* Jump to next row
*
* @param array $attr Row attributes
*/
public function add_row($attr = array())
{
$this->rowindex++;
$this->colindex = 0;
$this->rows[$this->rowindex] = new stdClass;
$this->rows[$this->rowindex]->attrib = $attr;
$this->rows[$this->rowindex]->cells = array();
}
/**
* Set row attributes
*
* @param array $attr Row attributes
* @param int $index Optional row index (default current row index)
*/
public function set_row_attribs($attr = array(), $index = null)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
if ($index === null) {
$index = $this->rowindex;
}
// make sure row object exists (#1489094)
if (!$this->rows[$index]) {
$this->rows[$index] = new stdClass;
}
$this->rows[$index]->attrib = $attr;
}
/**
* Get row attributes
*
* @param int $index Row index
*
* @return array Row attributes
*/
public function get_row_attribs($index = null)
{
if ($index === null) {
$index = $this->rowindex;
}
return $this->rows[$index] ? $this->rows[$index]->attrib : null;
}
/**
* Build HTML output of the table data
*
* @param array $attrib Table attributes
* @return string The final table HTML code
*/
public function show($attrib = null)
{
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
$thead = $tbody = "";
// include <thead>
if (!empty($this->header)) {
$rowcontent = '';
foreach ($this->header as $c => $col) {
- $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content);
+ $rowcontent .= self::tag($this->_head_tagname(), $col->attrib, $col->content);
}
$thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) :
self::tag($this->_row_tagname(), array('class' => 'thead'), $rowcontent, parent::$common_attrib);
}
foreach ($this->rows as $r => $row) {
$rowcontent = '';
foreach ($row->cells as $c => $col) {
$rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content);
}
if ($r < $this->rowindex || count($row->cells)) {
$tbody .= self::tag($this->_row_tagname(), $row->attrib, $rowcontent, parent::$common_attrib);
}
}
if ($this->attrib['rowsonly']) {
return $tbody;
}
// add <tbody>
$this->content = $thead . ($this->tagname == 'table' ? self::tag('tbody', null, $tbody) : $tbody);
unset($this->attrib['cols'], $this->attrib['rowsonly']);
return parent::show();
}
/**
* Count number of rows
*
* @return The number of rows
*/
public function size()
{
return count($this->rows);
}
/**
* Remove table body (all rows)
*/
public function remove_body()
{
$this->rows = array();
$this->rowindex = 0;
}
/**
* Getter for the corresponding tag name for table row elements
*/
private function _row_tagname()
{
static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div');
- return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*'];
+ return $row_tagnames[$this->tagname] ?: $row_tagnames['*'];
+ }
+
+ /**
+ * Getter for the corresponding tag name for table row elements
+ */
+ private function _head_tagname()
+ {
+ static $head_tagnames = array('table' => 'th', '*' => 'span');
+ return $head_tagnames[$this->tagname] ?: $head_tagnames['*'];
}
/**
* Getter for the corresponding tag name for table cell elements
*/
private function _col_tagname()
{
static $col_tagnames = array('table' => 'td', '*' => 'span');
- return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*'];
+ return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];
}
}
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index 87103be..03f4963 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -1,1610 +1,1704 @@
<?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 |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | Copyright (C) 2011-2014, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| 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 Framework
* @subpackage Core
*/
class rcube
{
const INIT_WITH_DB = 1;
const INIT_WITH_PLUGINS = 2;
/**
* Singleton instace of rcube
*
* @var rcube
*/
static protected $instance;
/**
* Stores instance of rcube_config.
*
* @var rcube_config
*/
public $config;
/**
* Instace of database class.
*
* @var rcube_db
*/
public $db;
/**
* Instace of Memcache class.
*
* @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;
+ /**
+ * Instance of rcube_user class.
+ *
+ * @var rcube_user
+ */
+ public $user;
+
/* private/protected vars */
protected $texts;
protected $caches = array();
protected $shutdown_functions = array();
/**
* This implements the 'singleton' design pattern
*
* @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants
* @param string Environment name to run (e.g. live, dev, test)
*
* @return rcube The one and only instance
*/
static function get_instance($mode = 0, $env = '')
{
if (!self::$instance) {
self::$instance = new rcube($env);
self::$instance->init($mode);
}
return self::$instance;
}
/**
* Private constructor
*/
protected function __construct($env = '')
{
// load configuration
$this->config = new rcube_config($env);
$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_db Database object
*/
public function get_dbh()
{
if (!$this->db) {
- $config_all = $this->config->all();
- $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']);
+ $this->db = rcube_db::factory(
+ $this->config->get('db_dsnw'),
+ $this->config->get('db_dsnr'),
+ $this->config->get('db_persistent')
+ );
+
+ $this->db->set_debug((bool)$this->config->get('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 all configured hosts to pool
$pconnect = $this->config->get('memcache_pconnect', true);
foreach ($this->config->get('memcache_hosts', array()) as $host) {
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];
}
/**
* Initialize and get shared cache object
*
* @param string $name Cache identifier
* @param bool $packed Enables/disables data serialization
*
* @return rcube_cache_shared Cache object
*/
public function get_cache_shared($name, $packed=true)
{
$shared_name = "shared_$name";
if (!array_key_exists($shared_name, $this->caches)) {
$opt = strtolower($name) . '_cache';
$type = $this->config->get($opt);
$ttl = $this->config->get($opt . '_ttl');
if (!$type) {
// cache is disabled
return $this->caches[$shared_name] = null;
}
if ($ttl === null) {
$ttl = $this->config->get('shared_cache_ttl', '10d');
}
$this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed);
}
return $this->caches[$shared_name];
}
/**
* Create SMTP object and connect to server
*
* @param boolean True if connection should be established
*/
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"),
- 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
- 'timeout' => (int) $this->config->get("{$driver}_timeout"),
- 'skip_deleted' => (bool) $this->config->get('skip_deleted'),
- 'driver' => $driver,
+ '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"),
+ 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
+ 'socket_options' => $this->config->get("{$driver}_conn_options"),
+ '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();
- }
+ // subscribe to 'storage_connected' hook for session logging
+ if ($this->config->get('imap_log_session', false)) {
+ $this->plugins->register_hook('storage_connected', array($this, 'storage_log_session'));
+ }
+ }
/**
* Set storage parameters.
- * This must be done AFTER connecting to the server!
*/
protected function set_storage_prop()
{
$storage = $this->get_storage();
+ // set pagesize from config
+ $pagesize = $this->config->get('mail_pagesize');
+ if (!$pagesize) {
+ $pagesize = $this->config->get('pagesize', 50);
+ }
+
+ $storage->set_pagesize($pagesize);
$storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
- if ($default_folders = $this->config->get('default_folders')) {
- $storage->set_default_folders($default_folders);
+ // enable caching of mail data
+ $driver = $this->config->get('storage_driver', 'imap');
+ $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) {
+ $storage->set_caching($storage_cache);
+ }
+ if ($messages_cache) {
+ $storage->set_messages_caching(true);
+ }
+ }
+
+
+ /**
+ * Set special folders type association.
+ * This must be done AFTER connecting to the server!
+ */
+ protected function set_special_folders()
+ {
+ $storage = $this->get_storage();
+ $folders = $storage->get_special_folders(true);
+ $prefs = array();
+
+ // check SPECIAL-USE flags on IMAP folders
+ foreach ($folders as $type => $folder) {
+ $idx = $type . '_mbox';
+ if ($folder !== $this->config->get($idx)) {
+ $prefs[$idx] = $folder;
+ }
}
- if (isset($_SESSION['mbox'])) {
- $storage->set_folder($_SESSION['mbox']);
+
+ // Some special folders differ, update user preferences
+ if (!empty($prefs) && $this->user) {
+ $this->user->save_prefs($prefs);
}
- if (isset($_SESSION['page'])) {
- $storage->set_page($_SESSION['page']);
+
+ // create default folders (on login)
+ if ($this->config->get('create_default_folders')) {
+ $storage->create_default_folders();
}
}
+ /**
+ * Callback for IMAP connection events to log session identifiers
+ */
+ public function storage_log_session($args)
+ {
+ if (!empty($args['session']) && session_id()) {
+ $this->write_log('imap_session', $args['session']);
+ }
+ }
+
/**
* 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');
$sess_path = $this->config->get('session_path');
$lifetime = $this->config->get('session_lifetime', 0) * 60;
$is_secure = $this->config->get('use_https') || rcube_utils::https_check();
// set session domain
if ($sess_domain) {
ini_set('session.cookie_domain', $sess_domain);
}
// set session path
if ($sess_path) {
ini_set('session.cookie_path', $sess_path);
}
// set session garbage collecting time according to session_lifetime
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
ini_set('session.cookie_secure', $is_secure);
ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.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, 'gc'));
$this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->config->get('ip_check'));
if ($this->config->get('session_auth_name')) {
$this->session->set_cookiename($this->config->get('session_auth_name'));
}
// start PHP session (if not in CLI mode)
if ($_SERVER['REMOTE_ADDR']) {
$this->session->start();
}
}
/**
* Garbage collector - cache/temp cleaner
*/
public function gc()
{
rcube_cache::gc();
rcube_cache_shared::gc();
$this->get_storage()->cache_gc();
$this->gc_temp();
}
/**
* Garbage collector function for temp files.
* Remove temp files older than two days
*/
public function gc_temp()
{
$tmp = unslashify($this->config->get('temp_dir'));
// expire in 48 hours by default
$temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
$temp_dir_ttl = get_offset_sec($temp_dir_ttl);
if ($temp_dir_ttl < 6*3600)
$temp_dir_ttl = 6*3600; // 6 hours sensible lower bound.
$expire = time() - $temp_dir_ttl;
if ($tmp && ($dir = opendir($tmp))) {
while (($fname = readdir($dir)) !== false) {
if ($fname[0] == '.') {
continue;
}
if (@filemtime($tmp.'/'.$fname) < $expire) {
@unlink($tmp.'/'.$fname);
}
}
closedir($dir);
}
}
/**
* Runs garbage collector with probability based on
* session settings. This is intended for environments
* without a session.
*/
public function gc_run()
{
$probability = (int) ini_get('session.gc_probability');
$divisor = (int) ini_get('session.gc_divisor');
if ($divisor > 0 && $probability > 0) {
$random = mt_rand(1, $divisor);
if ($random <= $probability) {
$this->gc();
}
}
}
/**
* 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 $lang Language ID
* @param array $add Additional text labels/messages
* @param array $merge Additional text labels/messages to merge
*/
public function load_language($lang = null, $add = array(), $merge = 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(RCUBE_LOCALIZATION_DIR . 'en_US/labels.inc');
@include(RCUBE_LOCALIZATION_DIR . '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(RCUBE_LOCALIZATION_DIR . $lang)) {
include_once(RCUBE_LOCALIZATION_DIR . $lang . '/labels.inc');
include_once(RCUBE_LOCALIZATION_DIR . $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;
}
// merge additional texts (from plugin)
if (is_array($merge) && !empty($merge)) {
$this->texts = array_merge($this->texts, $merge);
}
}
/**
* 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 = $accept_langs[0];
if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
$lang = $m[1] . '_' . strtoupper($m[2]);
}
}
if (empty($rcube_languages)) {
@include(RCUBE_LOCALIZATION_DIR . '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(RCUBE_LOCALIZATION_DIR . $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(RCUBE_LOCALIZATION_DIR . 'index.inc');
if ($dh = @opendir(RCUBE_LOCALIZATION_DIR)) {
while (($name = readdir($dh)) !== false) {
if ($name[0] == '.' || !is_dir(RCUBE_LOCALIZATION_DIR . $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");
+ $ckey = $this->config->get_crypto_key($key);
- if (function_exists('mcrypt_module_open') &&
+ if (function_exists('openssl_encrypt')) {
+ $method = 'DES-EDE3-CBC';
+ $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
+ $iv = $this->create_iv(openssl_cipher_iv_length($method));
+ $cipher = $iv . openssl_encrypt($clear, $method, $ckey, $opts, $iv);
+ }
+ else 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);
+ mcrypt_generic_init($td, $ckey, $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);
+ $cipher = $iv . des($ckey, $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"
+ 'message' => "Could not perform encryption; make sure OpenSSL or Mcrypt 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;
+ $ckey = $this->config->get_crypto_key($key);
+
+ if (function_exists('openssl_decrypt')) {
+ $method = 'DES-EDE3-CBC';
+ $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
+ $iv_size = openssl_cipher_iv_length($method);
+ $iv = substr($cipher, 0, $iv_size);
- if (function_exists('mcrypt_module_open') &&
+ // session corruption? (#1485970)
+ if (strlen($iv) < $iv_size) {
+ return '';
+ }
+
+ $cipher = substr($cipher, $iv_size);
+ $clear = openssl_decrypt($cipher, $method, $ckey, $opts, $iv);
+ }
+ else 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);
+ $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);
+ mcrypt_generic_init($td, $ckey, $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);
+ $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);
+ $clear = des($ckey, $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"
+ 'message' => "Could not perform decryption; make sure OpenSSL or Mcrypt 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);
}
// write session data as soon as possible and before
// closing database connection, don't do this before
// registered shutdown functions, they may need the session
// Note: this will run registered gc handlers (ie. cache gc)
if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
$this->session->write_close();
}
if (is_object($this->smtp)) {
$this->smtp->disconnect();
}
foreach ($this->caches as $cache) {
if (is_object($cache)) {
$cache->close();
}
}
if (is_object($this->storage)) {
$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;
}
/**
* Quote a given string.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @return string HTML-quoted string
*/
public static function Q($str, $mode = 'strict', $newlines = true)
{
return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);
}
/**
* Quote a given string for javascript output.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @return string JS-quoted string
*/
public static function JQ($str)
{
return rcube_utils::rep_specialchars_output($str, 'js');
}
/**
* 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;
+ $date_format = $log_driver = $session_key = null;
+ if (self::$instance) {
+ $date_format = self::$instance->config->get('log_date_format');
+ $log_driver = self::$instance->config->get('log_driver');
+ $session_key = intval(self::$instance->config->get('log_session_id', 8));
+ }
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;
}
+ // add session ID to the log
+ if ($session_key > 0 && ($sess = session_id())) {
+ $line = '<' . substr($sess, 0, $session_key) . '> ' . $line;
+ }
+
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 = null;
// per-user logging is activated
if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) {
$log_dir = self::$instance->get_user_log_dir();
if (empty($log_dir))
return false;
}
else if (!empty($log['dir'])) {
$log_dir = $log['dir'];
}
else if (self::$instance) {
$log_dir = self::$instance->config->get('log_dir');
}
if (empty($log_dir)) {
$log_dir = RCUBE_INSTALL_PATH . 'logs';
}
// 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 occurred
* - line: Line where error occurred
* @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')) {
$arg = array(
'code' => $arg->getCode(),
'line' => $arg->getLine(),
'file' => $arg->getFile(),
'message' => $arg->getMessage(),
);
}
else if (is_string($arg)) {
$arg = array('message' => $arg);
}
if (empty($arg['code'])) {
$arg['code'] = 500;
}
// installer
- if (class_exists('rcube_install', false)) {
- $rci = rcube_install::get_instance();
+ if (class_exists('rcmail_install', false)) {
+ $rci = rcmail_install::get_instance();
$rci->raise_error($arg);
return;
}
$cli = php_sapi_name() == 'cli';
if (($log || $terminate) && !$cli && $arg['message']) {
$arg['fatal'] = $terminate;
self::log_bug($arg);
}
// terminate script
if ($terminate) {
// display error page
if (is_object(self::$instance->output)) {
self::$instance->output->raise_error($arg['code'], $arg['message']);
}
else if ($cli) {
fwrite(STDERR, 'ERROR: ' . $arg['message']);
}
exit(1);
}
+ else if ($cli) {
+ fwrite(STDERR, 'ERROR: ' . $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(!empty($arg_arr['type']) ? $arg_arr['type'] : 'php');
$level = self::get_instance()->config->get('debug_level');
// disable errors for ajax requests, write to log instead (#1487831)
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'], E_USER_WARNING);
}
}
// 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));
}
+ /**
+ * Setter for system user object
+ *
+ * @param rcube_user Current user instance
+ */
+ public function set_user($user)
+ {
+ if (is_object($user)) {
+ $this->user = $user;
+
+ // overwrite config with user preferences
+ $this->config->set_user_prefs((array)$this->user->get_prefs());
+ }
+ }
/**
* Getter for logged user ID.
*
* @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();
}
else if (isset($_SESSION['username'])) {
return $_SESSION['username'];
}
}
/**
* Getter for logged user email (derived from user name not identity).
*
* @return string User email address
*/
public function get_user_email()
{
if (is_object($this->user)) {
return $this->user->get_username('mail');
}
}
/**
* Getter for logged user password.
*
* @return string User password
*/
public function get_user_password()
{
if ($this->password) {
return $this->password;
}
else if ($_SESSION['password']) {
return $this->decrypt($_SESSION['password']);
}
}
/**
* Get the per-user log directory
*/
protected function get_user_log_dir()
{
$log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
$user_name = $this->get_user_name();
$user_log_dir = $log_dir . '/' . $user_name;
return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
}
/**
* Getter for logged user language code.
*
* @return string User language code
*/
public function get_user_language()
{
if (is_object($this->user)) {
return $this->user->language;
}
else if (isset($_SESSION['language'])) {
return $_SESSION['language'];
}
}
/**
* Unique Message-ID generator.
*
* @return string Message-ID
*/
public function gen_message_id()
{
$local_part = md5(uniqid('rcube'.mt_rand(), true));
$domain_part = $this->user->get_username('domain');
// Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
$host = preg_replace('/:[0-9]+$/', '', $host);
if ($host && preg_match('/\.[a-z]+$/i', $host)) {
$domain_part = $host;
}
}
}
return sprintf('<%s@%s>', $local_part, $domain_part);
}
/**
* Send the given message using the configured method.
*
* @param object $message Reference to Mail_MIME object
* @param string $from Sender address string
* @param array $mailto Array of recipient address strings
* @param array $error SMTP error array (reference)
* @param string $body_file Location of file with saved message body (reference),
* used when delay_file_io is enabled
* @param array $options SMTP options (e.g. DSN request)
*
* @return boolean Send status.
*/
public function deliver_message(&$message, $from, $mailto, &$error, &$body_file = null, $options = null)
{
$plugin = $this->plugins->exec_hook('message_before_send', array(
'message' => $message,
'from' => $from,
'mailto' => $mailto,
'options' => $options,
));
if ($plugin['abort']) {
if (!empty($plugin['error'])) {
$error = $plugin['error'];
}
if (!empty($plugin['body_file'])) {
$body_file = $plugin['body_file'];
}
return isset($plugin['result']) ? $plugin['result'] : false;
}
$from = $plugin['from'];
$mailto = $plugin['mailto'];
$options = $plugin['options'];
$message = $plugin['message'];
$headers = $message->headers();
// send thru SMTP server using custom SMTP library
if ($this->config->get('smtp_server')) {
// generate list of recipients
$a_recipients = array($mailto);
if (strlen($headers['Cc']))
$a_recipients[] = $headers['Cc'];
if (strlen($headers['Bcc']))
$a_recipients[] = $headers['Bcc'];
// clean Bcc from header for recipients
$send_headers = $headers;
unset($send_headers['Bcc']);
// here too, it because txtHeaders() below use $message->_headers not only $send_headers
unset($message->_headers['Bcc']);
$smtp_headers = $message->txtHeaders($send_headers, true);
if ($message->getParam('delay_file_io')) {
// use common temp dir
$temp_dir = $this->config->get('temp_dir');
$body_file = tempnam($temp_dir, 'rcmMsg');
if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
TRUE, FALSE);
return false;
}
$msg_body = fopen($body_file, 'r');
}
else {
$msg_body = $message->get();
}
// send message
if (!is_object($this->smtp)) {
$this->smtp_init(true);
}
$sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options);
$response = $this->smtp->get_response();
$error = $this->smtp->get_error();
// log error
if (!$sent) {
self::raise_error(array('code' => 800, 'type' => 'smtp',
'line' => __LINE__, 'file' => __FILE__,
'message' => "SMTP error: ".join("\n", $response)), TRUE, FALSE);
}
}
// send mail using PHP's mail() function
else {
// unset some headers because they will be added by the mail() function
$headers_enc = $message->headers($headers);
$headers_php = $message->_headers;
unset($headers_php['To'], $headers_php['Subject']);
// reset stored headers and overwrite
$message->_headers = array();
$header_str = $message->txtHeaders($headers_php);
// #1485779
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
$headers_enc['To'] = implode(', ', $m[1]);
}
}
$msg_body = $message->get();
if (PEAR::isError($msg_body)) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$msg_body->getMessage()),
TRUE, FALSE);
}
else {
$delim = $this->config->header_delimiter();
$to = $headers_enc['To'];
$subject = $headers_enc['Subject'];
$header_str = rtrim($header_str);
if ($delim != "\r\n") {
$header_str = str_replace("\r\n", $delim, $header_str);
$msg_body = str_replace("\r\n", $delim, $msg_body);
$to = str_replace("\r\n", $delim, $to);
$subject = str_replace("\r\n", $delim, $subject);
}
if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))
$sent = mail($to, $subject, $msg_body, $header_str);
else
$sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
}
}
if ($sent) {
$this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
// remove MDN headers after sending
unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
// get all recipients
if ($headers['Cc'])
$mailto .= $headers['Cc'];
if ($headers['Bcc'])
$mailto .= $headers['Bcc'];
if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
$mailto = implode(', ', array_unique($m[1]));
if ($this->config->get('smtp_log')) {
self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
$this->user->get_username(),
$_SERVER['REMOTE_ADDR'],
$mailto,
!empty($response) ? join('; ', $response) : ''));
}
}
else {
// allow plugins to catch sending errors with the same parameters as in 'message_before_send'
$this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
}
if (is_resource($msg_body)) {
fclose($msg_body);
}
$message->_headers = array();
$message->headers($headers);
return $sent;
}
}
/**
* Lightweight plugin API class serving as a dummy if plugins are not enabled
*
* @package Framework
* @subpackage 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_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php
index 4d9fa3d..69027b0 100644
--- a/lib/ext/Roundcube/rcube_addressbook.php
+++ b/lib/ext/Roundcube/rcube_addressbook.php
@@ -1,620 +1,676 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to the local address book database |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Abstract skeleton of an address book/repository
*
* @package Framework
* @subpackage Addressbook
*/
abstract class rcube_addressbook
{
/** constants for error reporting **/
const ERROR_READ_ONLY = 1;
const ERROR_NO_CONNECTION = 2;
const ERROR_VALIDATE = 3;
const ERROR_SAVING = 4;
const ERROR_SEARCH = 5;
/** public properties (mandatory) */
public $primary_key;
public $groups = false;
public $export_groups = true;
public $readonly = true;
public $searchonly = false;
public $undelete = false;
public $ready = false;
public $group_id = null;
public $list_page = 1;
public $page_size = 10;
public $sort_col = 'name';
public $sort_order = 'ASC';
public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1));
public $date_cols = array();
protected $error;
/**
* Returns addressbook name (e.g. for addressbooks listing)
*/
abstract function get_name();
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
abstract function set_search_set($filter);
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
abstract function get_search_set();
/**
* Reset saved results and search parameters
*/
abstract function reset();
/**
* Refresh saved search set after data has changed
*
* @return mixed New search set
*/
function refresh_search()
{
return $this->get_search_set();
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @return array Indexed list of contact records, each a hash array
*/
abstract function list_records($cols=null, $subset=0);
/**
* Search records
*
* @param array List of fields to search in
* @param string Search value
* @param int Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param boolean True if results are requested, False if count only
* @param boolean True to skip the count query (select only)
* @param array List of fields that cannot be empty
* @return object rcube_result_set List of contact records and 'count' value
*/
abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array());
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
abstract function count();
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
abstract function get_result();
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @param boolean True to return record as associative array, otherwise a result set is returned
*
* @return mixed Result object with all record fields or False if not found
*/
abstract function get_record($id, $assoc=false);
/**
* Returns the last error occurred (e.g. when updating/inserting failed)
*
* @return array Hash array with the following fields: type, message
*/
function get_error()
{
return $this->error;
}
/**
* Setter for errors for internal use
*
* @param int Error type (one of this class' error constants)
* @param string Error message (name of a text label)
*/
protected function set_error($type, $message)
{
$this->error = array('type' => $type, 'message' => $message);
}
/**
* Close connection to source
* Called on script shutdown
*/
function close() { }
/**
* Set internal list page
*
* @param number Page number to list
* @access public
*/
function set_page($page)
{
$this->list_page = (int)$page;
}
/**
* Set internal page size
*
* @param number Number of messages to display on one page
* @access public
*/
function set_pagesize($size)
{
$this->page_size = (int)$size;
}
/**
* 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 ($sort_col != null && ($this->coltypes[$sort_col] || in_array($sort_col, $this->coltypes))) {
$this->sort_col = $sort_col;
}
if ($sort_order != null) {
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
}
/**
* Check the given data before saving.
* If input isn't valid, the message to display can be fetched using get_error()
*
* @param array Assoziative array with data to save
* @param boolean Attempt to fix/complete record automatically
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
$rcube = rcube::get_instance();
$valid = true;
// check validity of email addresses
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (strlen($email)) {
if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
$error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
$this->set_error(self::ERROR_VALIDATE, $error);
$valid = false;
break;
}
}
}
// allow plugins to do contact validation and auto-fixing
$plugin = $rcube->plugins->exec_hook('contact_validate', array(
'record' => $save_data,
'autofix' => $autofix,
'valid' => $valid,
));
if ($valid && !$plugin['valid']) {
$this->set_error(self::ERROR_VALIDATE, $plugin['error']);
}
if (is_array($plugin['record'])) {
$save_data = $plugin['record'];
}
return $plugin['valid'];
}
/**
* Create a new contact record
*
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
function insert($save_data, $check=false)
{
/* empty for read-only address books */
}
/**
* Create new contact records for every item in the record set
*
* @param object rcube_result_set Recordset to insert
* @param boolean True to check for duplicates first
* @return array List of created record IDs
*/
function insertMultiple($recset, $check=false)
{
$ids = array();
if (is_object($recset) && is_a($recset, rcube_result_set)) {
while ($row = $recset->next()) {
if ($insert = $this->insert($row, $check))
$ids[] = $insert;
}
}
return $ids;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
*
* @return mixed On success if ID has been changed returns ID, otherwise True, False on error
*/
function update($id, $save_cols)
{
/* empty for read-only address books */
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param bool Remove records irreversible (see self::undelete)
*/
function delete($ids, $force=true)
{
/* empty for read-only address books */
}
/**
* Unmark delete flag on contact record(s)
*
* @param array Record identifiers
*/
function undelete($ids)
{
/* empty for read-only address books */
}
/**
* Mark all records in database as deleted
*
* @param bool $with_groups Remove also groups
*/
function delete_all($with_groups = false)
{
/* empty for read-only address books */
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($gid) { }
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @param int Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
/* empty for address books don't supporting groups */
return array();
}
/**
* 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)
{
/* empty for address books don't supporting groups */
return null;
}
/**
* 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)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* 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)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* 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($gid, $newname, &$newid)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $ids)
{
/* empty for address books don't supporting groups */
return 0;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $ids)
{
/* empty for address books don't supporting groups */
return 0;
}
/**
* 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($id)
{
/* empty for address books don't supporting groups */
return array();
}
/**
* Utility function to return all values of a certain data column
* either as flat list or grouped by subtype
*
* @param string Col name
* @param array Record data array as used for saving
* @param boolean True to return one array with all values, False for hash array with values grouped by type
* @return array List of column values
*/
public static function get_col_values($col, $data, $flat = false)
{
$out = array();
foreach ((array)$data as $c => $values) {
if ($c === $col || strpos($c, $col.':') === 0) {
if ($flat) {
$out = array_merge($out, (array)$values);
}
else {
list(, $type) = explode(':', $c);
$out[$type] = array_merge((array)$out[$type], (array)$values);
}
}
}
// remove duplicates
if ($flat && !empty($out)) {
$out = array_unique($out);
}
return $out;
}
/**
* Normalize the given string for fulltext search.
* Currently only optimized for Latin-1 characters; to be extended
*
* @param string Input string (UTF-8)
* @return string Normalized string
* @deprecated since 0.9-beta
*/
protected static function normalize_string($str)
{
return rcube_utils::normalize_string($str);
}
/**
* Compose a valid display name from the given structured contact data
*
* @param array Hash array with contact data as key-value pairs
* @param bool Don't attempt to extract components from the email address
*
* @return string Display name
*/
public static function compose_display_name($contact, $full_email = false)
{
$contact = rcube::get_instance()->plugins->exec_hook('contact_displayname', $contact);
$fn = $contact['name'];
if (!$fn) // default display name composition according to vcard standard
$fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
// use email address part for name
$email = self::get_col_values('email', $contact, true);
$email = $email[0];
if ($email && (empty($fn) || $fn == $email)) {
// return full email
if ($full_email)
return $email;
list($emailname) = explode('@', $email);
if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match))
$fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
else
$fn = ucfirst($emailname);
}
return $fn;
}
/**
* Compose the name to display in the contacts list for the given contact record.
* This respects the settings parameter how to list conacts.
*
* @param array Hash array with contact data as key-value pairs
* @return string List name
*/
public static function compose_list_name($contact)
{
static $compose_mode;
if (!isset($compose_mode)) // cache this
$compose_mode = rcube::get_instance()->config->get('addressbook_name_listing', 0);
if ($compose_mode == 3)
$fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename']));
else if ($compose_mode == 2)
$fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
else if ($compose_mode == 1)
$fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
else if ($compose_mode == 0)
$fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
else {
$plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
$fn = $plugin['fn'];
}
$fn = trim($fn, ', ');
// fallback to display name
if (empty($fn) && $contact['name'])
$fn = $contact['name'];
// fallback to email address
if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
return $email[0];
}
return $fn;
}
+ /**
+ * Build contact display name for autocomplete listing
+ *
+ * @param array Hash array with contact data as key-value pairs
+ * @param string Optional email address
+ * @param string Optional name (self::compose_list_name() result)
+ * @param string Optional template to use (defaults to the 'contact_search_name' config option)
+ *
+ * @return string Display name
+ */
+ public static function compose_search_name($contact, $email = null, $name = null, $templ = null)
+ {
+ static $template;
+
+ if (empty($templ) && !isset($template)) { // cache this
+ $template = rcube::get_instance()->config->get('contact_search_name');
+ if (empty($template)) {
+ $template = '{name} <{email}>';
+ }
+ }
+
+ $result = $templ ?: $template;
+
+ if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) {
+ foreach ($matches[0] as $key) {
+ $key = trim($key, '{}');
+ $value = '';
+
+ switch ($key) {
+ case 'name':
+ $value = $name ?: self::compose_list_name($contact);
+ break;
+
+ case 'email':
+ $value = $email;
+ break;
+ }
+
+ if (empty($value)) {
+ $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
+ if (is_array($value)) {
+ $value = $value[0];
+ }
+ }
+
+ $result = str_replace('{' . $key . '}', $value, $result);
+ }
+ }
+
+ $result = preg_replace('/\s+/', ' ', $result);
+ $result = preg_replace('/\s*(<>|\(\)|\[\])/', '', $result);
+ $result = trim($result, '/ ');
+
+ return $result;
+ }
+
/**
* Create a unique key for sorting contacts
*/
public static function compose_contact_key($contact, $sort_col)
{
$key = $contact[$sort_col] . ':' . $contact['sourceid'];
// add email to a key to not skip contacts with the same name (#1488375)
if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
$key .= ':' . implode(':', (array)$email);
}
return $key;
}
/**
* Compare search value with contact data
*
* @param string $colname Data name
* @param string|array $value Data value
* @param string $search Search value
* @param int $mode Search mode
*
* @return bool Comparision result
*/
protected function compare_search_value($colname, $value, $search, $mode)
{
// The value is a date string, for date we'll
// use only strict comparison (mode = 1)
// @TODO: partial search, e.g. match only day and month
if (in_array($colname, $this->date_cols)) {
return (($value = rcube_utils::anytodatetime($value))
&& ($search = rcube_utils::anytodatetime($search))
&& $value->format('Ymd') == $search->format('Ymd'));
}
// 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);
}
if ($got) {
return true;
}
}
return false;
}
}
diff --git a/lib/ext/Roundcube/rcube_cache.php b/lib/ext/Roundcube/rcube_cache.php
index a708cb2..7210ce6 100644
--- a/lib/ext/Roundcube/rcube_cache.php
+++ b/lib/ext/Roundcube/rcube_cache.php
@@ -1,594 +1,624 @@
<?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 Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache
{
/**
* Instance of database handler
*
* @var rcube_db|Memcache|bool
*/
private $db;
private $type;
private $userid;
private $prefix;
private $table;
private $ttl;
private $packed;
private $index;
private $cache = array();
private $cache_changes = array();
private $cache_sums = array();
+ private $max_packet = -1;
/**
* 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();
- $this->table = $this->db->table_name('cache');
+ $this->table = $this->db->table_name('cache', true);
}
// 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->serialize($data));
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
function remove($key=null, $prefix_mode=false)
{
// Remove all keys
if ($key === null) {
$this->cache = array();
$this->cache_changed = false;
$this->cache_changes = array();
$this->cache_sums = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach (array_keys($this->cache) as $k) {
if (strpos($k, $key) === 0) {
$this->cache[$k] = null;
$this->cache_changes[$k] = false;
unset($this->cache_sums[$k]);
}
}
}
// Remove one key by name
else {
$this->cache[$key] = null;
$this->cache_changes[$key] = false;
unset($this->cache_sums[$key]);
}
// Remove record(s) from the backend
$this->remove_record($key, $prefix_mode);
}
/**
* Remove cache records older than ttl
*/
function expunge()
{
if ($this->type == 'db' && $this->db && $this->ttl) {
$this->db->query(
- "DELETE FROM ".$this->table.
- " WHERE user_id = ?".
- " AND cache_key LIKE ?".
- " AND expires < " . $this->db->now(),
+ "DELETE FROM {$this->table}".
+ " WHERE `user_id` = ?".
+ " AND `cache_key` LIKE ?".
+ " AND `expires` < " . $this->db->now(),
$this->userid,
$this->prefix.'.%');
}
}
/**
* Remove expired records of all caches
*/
static function gc()
{
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
- $db->query("DELETE FROM " . $db->table_name('cache') . " WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
}
/**
* Writes the cache back to the DB.
*/
function close()
{
if (!$this->cache_changed) {
return;
}
foreach ($this->cache as $key => $data) {
// The key has been used
if ($this->cache_changes[$key]) {
// Make sure we're not going to write unchanged data
// by comparing current md5 sum with the sum calculated on DB read
$data = $this->serialize($data);
if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
$this->write_record($key, $data);
}
}
}
$this->write_index();
}
/**
* Reads cache entry.
*
* @param string $key Cache key name
* @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
*/
private function read_record($key, $nostore=false)
{
if (!$this->db) {
return null;
}
if ($this->type != 'db') {
if ($this->type == 'memcache') {
$data = $this->db->get($this->ckey($key));
}
else if ($this->type == 'apc') {
$data = apc_fetch($this->ckey($key));
}
if ($data) {
$md5sum = md5($data);
$data = $this->unserialize($data);
if ($nostore) {
return $data;
}
$this->cache_sums[$key] = $md5sum;
$this->cache[$key] = $data;
}
else {
$this->cache[$key] = null;
}
}
else {
$sql_result = $this->db->limitquery(
- "SELECT data, cache_key".
- " FROM " . $this->table.
- " WHERE user_id = ?".
- " AND cache_key = ?".
+ "SELECT `data`, `cache_key`".
+ " FROM {$this->table}".
+ " WHERE `user_id` = ? AND `cache_key` = ?".
// for better performance we allow more records for one key
// get the newer one
- " ORDER BY created DESC",
+ " 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->unserialize($sql_arr['data']);
}
if ($nostore) {
return $data;
}
$this->cache[$key] = $data;
$this->cache_sums[$key] = $md5sum;
}
else {
$this->cache[$key] = null;
}
}
return $this->cache[$key];
}
/**
* Writes single cache record into DB.
*
* @param string $key Cache key name
- * @param mxied $data Serialized cache data
+ * @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
private function write_record($key, $data)
{
if (!$this->db) {
return false;
}
+ // don't attempt to write too big data sets
+ if (strlen($data) > $this->max_packet_size()) {
+ trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING);
+ return false;
+ }
+
if ($this->type == 'memcache' || $this->type == 'apc') {
return $this->add_record($this->ckey($key), $data);
}
$key_exists = array_key_exists($key, $this->cache_sums);
$key = $this->prefix . '.' . $key;
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
$this->db->query(
- "DELETE FROM " . $this->table.
- " WHERE user_id = ?".
- " AND cache_key = ?",
+ "DELETE FROM {$this->table}".
+ " WHERE `user_id` = ? AND `cache_key` = ?",
$this->userid, $key);
return true;
}
// update existing cache record
if ($key_exists) {
$result = $this->db->query(
- "UPDATE " . $this->table.
- " SET created = " . $this->db->now().
- ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
- ", data = ?".
- " WHERE user_id = ?".
- " AND cache_key = ?",
+ "UPDATE {$this->table}".
+ " SET `created` = " . $this->db->now().
+ ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
+ ", `data` = ?".
+ " WHERE `user_id` = ?".
+ " AND `cache_key` = ?",
$data, $this->userid, $key);
}
// add new cache record
else {
// for better performance we allow more records for one key
// so, no need to check if record exist (see rcube_cache::read_record())
$result = $this->db->query(
- "INSERT INTO " . $this->table.
- " (created, expires, user_id, cache_key, data)".
+ "INSERT INTO {$this->table}".
+ " (`created`, `expires`, `user_id`, `cache_key`, `data`)".
" VALUES (" . $this->db->now() . ", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?, ?)",
$this->userid, $key, $data);
}
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.'.%');
+ $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.'%');
+ $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);
+ $where = " AND `cache_key` = " . $this->db->quote($this->prefix.'.'.$key);
}
$this->db->query(
- "DELETE FROM " . $this->table.
- " WHERE user_id = ?" . $where,
+ "DELETE FROM {$this->table} 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') {
// #1488592: use 2nd argument
$this->db->delete($this->ckey($key), 0);
}
else {
apc_delete($this->ckey($key));
}
if ($index) {
if (($idx = array_search($key, $this->index)) !== false) {
unset($this->index[$idx]);
}
}
}
/**
* Writes the index entry into memcache/apc DB.
*/
private function write_index()
{
if (!$this->db) {
return;
}
if ($this->type == 'db') {
return;
}
$this->load_index();
// Make sure index contains new keys
foreach ($this->cache as $key => $value) {
if ($value !== null) {
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
}
}
}
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
/**
* Gets the index entry from memcache/apc DB.
*/
private function load_index()
{
if (!$this->db) {
return;
}
if ($this->index !== null) {
return;
}
$index_key = $this->ikey();
if ($this->type == 'memcache') {
$data = $this->db->get($index_key);
}
else if ($this->type == 'apc') {
$data = apc_fetch($index_key);
}
$this->index = $data ? unserialize($data) : array();
}
/**
* Creates 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');
}
/**
* Serializes data for storing
*/
private function serialize($data)
{
if ($this->type == 'db') {
return $this->db->encode($data, $this->packed);
}
return $this->packed ? serialize($data) : $data;
}
/**
* Unserializes serialized data
*/
private function unserialize($data)
{
if ($this->type == 'db') {
return $this->db->decode($data, $this->packed);
}
return $this->packed ? @unserialize($data) : $data;
}
+
+ /**
+ * Determine the maximum size for cache data to be written
+ */
+ private function max_packet_size()
+ {
+ if ($this->max_packet < 0) {
+ $this->max_packet = 2097152; // default/max is 2 MB
+
+ if ($this->type == 'db') {
+ $value = $this->db->get_variable('max_allowed_packet', 1048500);
+ $this->max_packet = min($value, $this->max_packet) - 2000;
+ }
+ else if ($this->type == 'memcache') {
+ $stats = $this->db->getStats();
+ $remaining = $stats['limit_maxbytes'] - $stats['bytes'];
+ $this->max_packet = min($remaining / 5, $this->max_packet);
+ }
+ else if ($this->type == 'apc' && function_exists('apc_sma_info')) {
+ $stats = apc_sma_info();
+ $this->max_packet = min($stats['avail_mem'] / 5, $this->max_packet);
+ }
+ }
+
+ return $this->max_packet;
+ }
}
diff --git a/lib/ext/Roundcube/rcube_cache_shared.php b/lib/ext/Roundcube/rcube_cache_shared.php
index 8f25740..a2bf092 100644
--- a/lib/ext/Roundcube/rcube_cache_shared.php
+++ b/lib/ext/Roundcube/rcube_cache_shared.php
@@ -1,581 +1,581 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011-2013, The Roundcube Dev Team |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Shared (cross-user) caching engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube shared cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache_shared
{
/**
* Instance of database handler
*
* @var rcube_db|Memcache|bool
*/
private $db;
private $type;
private $prefix;
private $ttl;
private $packed;
private $index;
private $table;
private $cache = array();
private $cache_changes = array();
private $cache_sums = array();
/**
* Object constructor.
*
* @param string $type Engine type ('db' or 'memcache' or 'apc')
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
function __construct($type, $prefix='', $ttl=0, $packed=true)
{
$rcube = rcube::get_instance();
$type = strtolower($type);
if ($type == 'memcache') {
$this->type = 'memcache';
$this->db = $rcube->get_memcache();
}
else if ($type == 'apc') {
$this->type = 'apc';
$this->db = function_exists('apc_exists'); // APC 3.1.4 required
}
else {
$this->type = 'db';
$this->db = $rcube->get_dbh();
- $this->table = $this->db->table_name('cache_shared');
+ $this->table = $this->db->table_name('cache_shared', true);
}
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->ttl = $ttl;
$this->packed = $packed;
$this->prefix = $prefix;
}
/**
* Returns cached value.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
function get($key)
{
if (!array_key_exists($key, $this->cache)) {
return $this->read_record($key);
}
return $this->cache[$key];
}
/**
* Sets (add/update) value in cache.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*/
function set($key, $data)
{
$this->cache[$key] = $data;
$this->cache_changed = true;
$this->cache_changes[$key] = true;
}
/**
* Returns cached value without storing it in internal memory.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
function read($key)
{
if (array_key_exists($key, $this->cache)) {
return $this->cache[$key];
}
return $this->read_record($key, true);
}
/**
* Sets (add/update) value in cache and immediately saves
* it in the backend, no internal memory will be used.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*
* @param boolean True on success, False on failure
*/
function write($key, $data)
{
return $this->write_record($key, $this->serialize($data));
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
function remove($key=null, $prefix_mode=false)
{
// Remove all keys
if ($key === null) {
$this->cache = array();
$this->cache_changed = false;
$this->cache_changes = array();
$this->cache_sums = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach (array_keys($this->cache) as $k) {
if (strpos($k, $key) === 0) {
$this->cache[$k] = null;
$this->cache_changes[$k] = false;
unset($this->cache_sums[$k]);
}
}
}
// Remove one key by name
else {
$this->cache[$key] = null;
$this->cache_changes[$key] = false;
unset($this->cache_sums[$key]);
}
// Remove record(s) from the backend
$this->remove_record($key, $prefix_mode);
}
/**
* Remove cache records older than ttl
*/
function expunge()
{
if ($this->type == 'db' && $this->db && $this->ttl) {
$this->db->query(
- "DELETE FROM " . $this->table
- . " WHERE cache_key LIKE ?"
- . " AND expires < " . $this->db->now(),
+ "DELETE FROM {$this->table}"
+ . " WHERE `cache_key` LIKE ?"
+ . " AND `expires` < " . $this->db->now(),
$this->prefix . '.%');
}
}
/**
* Remove expired records of all caches
*/
static function gc()
{
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
- $db->query("DELETE FROM " . $db->table_name('cache_shared') . " WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now());
}
/**
* Writes the cache back to the DB.
*/
function close()
{
if (!$this->cache_changed) {
return;
}
foreach ($this->cache as $key => $data) {
// The key has been used
if ($this->cache_changes[$key]) {
// Make sure we're not going to write unchanged data
// by comparing current md5 sum with the sum calculated on DB read
$data = $this->serialize($data);
if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
$this->write_record($key, $data);
}
}
}
$this->write_index();
}
/**
* Reads cache entry.
*
* @param string $key Cache key name
* @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
*/
private function read_record($key, $nostore=false)
{
if (!$this->db) {
return null;
}
if ($this->type != 'db') {
if ($this->type == 'memcache') {
$data = $this->db->get($this->ckey($key));
}
else if ($this->type == 'apc') {
$data = apc_fetch($this->ckey($key));
}
if ($data) {
$md5sum = md5($data);
$data = $this->unserialize($data);
if ($nostore) {
return $data;
}
$this->cache_sums[$key] = $md5sum;
$this->cache[$key] = $data;
}
else {
$this->cache[$key] = null;
}
}
else {
$sql_result = $this->db->limitquery(
- "SELECT data, cache_key".
- " FROM " . $this->table .
- " WHERE cache_key = ?".
+ "SELECT `data`, `cache_key`".
+ " FROM {$this->table}" .
+ " WHERE `cache_key` = ?".
// for better performance we allow more records for one key
// get the newer one
- " ORDER BY created DESC",
+ " ORDER BY `created` DESC",
0, 1, $this->prefix . '.' . $key);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
if ($sql_arr['data']) {
$data = $this->unserialize($sql_arr['data']);
}
if ($nostore) {
return $data;
}
$this->cache[$key] = $data;
$this->cache_sums[$key] = $md5sum;
}
else {
$this->cache[$key] = null;
}
}
return $this->cache[$key];
}
/**
* Writes single cache record into DB.
*
* @param string $key Cache key name
* @param mxied $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
private function write_record($key, $data)
{
if (!$this->db) {
return false;
}
if ($this->type == 'memcache' || $this->type == 'apc') {
return $this->add_record($this->ckey($key), $data);
}
$key_exists = array_key_exists($key, $this->cache_sums);
$key = $this->prefix . '.' . $key;
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
- $this->db->query("DELETE FROM " . $this->table . " WHERE cache_key = ?", $key);
+ $this->db->query("DELETE FROM {$this->table} WHERE `cache_key` = ?", $key);
return true;
}
// update existing cache record
if ($key_exists) {
$result = $this->db->query(
- "UPDATE " . $this->table .
- " SET created = " . $this->db->now() .
- ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .
- ", data = ?".
- " WHERE cache_key = ?",
+ "UPDATE {$this->table}" .
+ " SET `created` = " . $this->db->now() .
+ ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .
+ ", `data` = ?".
+ " WHERE `cache_key` = ?",
$data, $key);
}
// add new cache record
else {
// for better performance we allow more records for one key
// so, no need to check if record exist (see rcube_cache::read_record())
$result = $this->db->query(
- "INSERT INTO ".$this->table.
- " (created, expires, cache_key, data)".
+ "INSERT INTO {$this->table}".
+ " (`created`, `expires`, `cache_key`, `data`)".
" VALUES (".$this->db->now().", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?)",
$key, $data);
}
return $this->db->affected_rows($result);
}
/**
* Deletes the cache record(s).
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
private function remove_record($key=null, $prefix_mode=false)
{
if (!$this->db) {
return;
}
if ($this->type != 'db') {
$this->load_index();
// Remove all keys
if ($key === null) {
foreach ($this->index as $key) {
$this->delete_record($key, false);
}
$this->index = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach ($this->index as $k) {
if (strpos($k, $key) === 0) {
$this->delete_record($k);
}
}
}
// Remove one key by name
else {
$this->delete_record($key);
}
return;
}
// Remove all keys (in specified cache)
if ($key === null) {
- $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+ $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.%');
}
// Remove keys by name prefix
else if ($prefix_mode) {
- $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
+ $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
}
// Remove one key by name
else {
- $where = " WHERE cache_key = " . $this->db->quote($this->prefix.'.'.$key);
+ $where = " WHERE `cache_key` = " . $this->db->quote($this->prefix.'.'.$key);
}
$this->db->query("DELETE FROM " . $this->table . $where);
}
/**
* Adds entry into memcache/apc DB.
*
* @param string $key Cache key name
* @param mxied $data Serialized cache data
* @param bollean $index Enables immediate index update
*
* @param boolean True on success, False on failure
*/
private function add_record($key, $data, $index=false)
{
if ($this->type == 'memcache') {
$result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
if (!$result) {
$result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
}
}
else if ($this->type == 'apc') {
if (apc_exists($key)) {
apc_delete($key);
}
$result = apc_store($key, $data, $this->ttl);
}
// Update index
if ($index && $result) {
$this->load_index();
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
}
return $result;
}
/**
* Deletes entry from memcache/apc DB.
*/
private function delete_record($key, $index=true)
{
if ($this->type == 'memcache') {
// #1488592: use 2nd argument
$this->db->delete($this->ckey($key), 0);
}
else {
apc_delete($this->ckey($key));
}
if ($index) {
if (($idx = array_search($key, $this->index)) !== false) {
unset($this->index[$idx]);
}
}
}
/**
* Writes the index entry into memcache/apc DB.
*/
private function write_index()
{
if (!$this->db) {
return;
}
if ($this->type == 'db') {
return;
}
$this->load_index();
// Make sure index contains new keys
foreach ($this->cache as $key => $value) {
if ($value !== null) {
if (array_search($key, $this->index) === false) {
$this->index[] = $key;
}
}
}
$data = serialize($this->index);
$this->add_record($this->ikey(), $data);
}
/**
* Gets the index entry from memcache/apc DB.
*/
private function load_index()
{
if (!$this->db) {
return;
}
if ($this->index !== null) {
return;
}
$index_key = $this->ikey();
if ($this->type == 'memcache') {
$data = $this->db->get($index_key);
}
else if ($this->type == 'apc') {
$data = apc_fetch($index_key);
}
$this->index = $data ? unserialize($data) : array();
}
/**
* Creates cache key name (for memcache and apc)
*
* @param string $key Cache key name
*
* @return string Cache key
*/
private function ckey($key)
{
return $this->prefix . ':' . $key;
}
/**
* Creates index cache key name (for memcache and apc)
*
* @return string Cache key
*/
private function ikey()
{
// This way each cache will have its own index
return $this->prefix . 'INDEX';
}
/**
* Serializes data for storing
*/
private function serialize($data)
{
if ($this->type == 'db') {
return $this->db->encode($data, $this->packed);
}
return $this->packed ? serialize($data) : $data;
}
/**
* Unserializes serialized data
*/
private function unserialize($data)
{
if ($this->type == 'db') {
return $this->db->decode($data, $this->packed);
}
return $this->packed ? @unserialize($data) : $data;
}
}
diff --git a/lib/ext/Roundcube/rcube_charset.php b/lib/ext/Roundcube/rcube_charset.php
index ffec673..d6ca3c0 100644
--- a/lib/ext/Roundcube/rcube_charset.php
+++ b/lib/ext/Roundcube/rcube_charset.php
@@ -1,828 +1,810 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| Copyright (C) 2011-2012, Kolab Systems AG |
| Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org> |
| |
| 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: |
| Provide charset conversion functionality |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Character sets conversion functionality
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @author Edmund Grimley Evans <edmundo@rano.org>
*/
class rcube_charset
{
// Aliases: some of them from HTML5 spec.
static public $aliases = array(
'USASCII' => 'WINDOWS-1252',
'ANSIX31101983' => 'WINDOWS-1252',
'ANSIX341968' => 'WINDOWS-1252',
'UNKNOWN8BIT' => 'ISO-8859-15',
'UNKNOWN' => 'ISO-8859-15',
'USERDEFINED' => 'ISO-8859-15',
'KSC56011987' => 'EUC-KR',
'GB2312' => 'GBK',
'GB231280' => 'GBK',
'UNICODE' => 'UTF-8',
'UTF7IMAP' => 'UTF7-IMAP',
'TIS620' => 'WINDOWS-874',
'ISO88599' => 'WINDOWS-1254',
'ISO885911' => 'WINDOWS-874',
'MACROMAN' => 'MACINTOSH',
'77' => 'MAC',
'128' => 'SHIFT-JIS',
'129' => 'CP949',
'130' => 'CP1361',
'134' => 'GBK',
'136' => 'BIG5',
'161' => 'WINDOWS-1253',
'162' => 'WINDOWS-1254',
'163' => 'WINDOWS-1258',
'177' => 'WINDOWS-1255',
'178' => 'WINDOWS-1256',
'186' => 'WINDOWS-1257',
'204' => 'WINDOWS-1251',
'222' => 'WINDOWS-874',
'238' => 'WINDOWS-1250',
'MS950' => 'CP950',
'WINDOWS949' => 'UHC',
);
/**
* Catch an error and throw an exception.
*
* @param int Level of the error
* @param string Error message
*/
public static function error_handler($errno, $errstr)
{
throw new ErrorException($errstr, 0, $errno);
}
/**
* Parse and validate charset name string (see #1485758).
* Sometimes charset string is malformed, there are also charset aliases
* but we need strict names for charset conversion (specially utf8 class)
*
* @param string $input Input charset name
*
* @return string The validated charset name
*/
public static function parse_charset($input)
{
static $charsets = array();
$charset = strtoupper($input);
if (isset($charsets[$input])) {
return $charsets[$input];
}
$charset = preg_replace(array(
'/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO
'/\$.*$/', // e.g. _ISO-8859-JP$SIO
'/UNICODE-1-1-*/', // RFC1641/1642
'/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8)
), '', $charset);
if ($charset == 'BINARY') {
return $charsets[$input] = null;
}
// allow A-Z and 0-9 only
$str = preg_replace('/[^A-Z0-9]/', '', $charset);
if (isset(self::$aliases[$str])) {
$result = self::$aliases[$str];
}
// UTF
else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) {
$result = 'UTF-' . $m[1] . $m[2];
}
// ISO-8859
else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
$iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
// some clients sends windows-1252 text as latin1,
// it is safe to use windows-1252 for all latin1
$result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
}
// handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
$result = 'WINDOWS-' . $m[2];
}
// LATIN
else if (preg_match('/LATIN(.*)/', $str, $m)) {
$aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
'7' => 13, '8' => 14, '9' => 15, '10' => 16,
'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8
);
// some clients sends windows-1252 text as latin1,
// it is safe to use windows-1252 for all latin1
if ($m[1] == 1) {
$result = 'WINDOWS-1252';
}
// if iconv is not supported we need ISO labels, it's also safe for iconv
else if (!empty($aliases[$m[1]])) {
$result = 'ISO-8859-'.$aliases[$m[1]];
}
// iconv requires convertion of e.g. LATIN-1 to LATIN1
else {
$result = $str;
}
}
else {
$result = $charset;
}
$charsets[$input] = $result;
return $result;
}
/**
* Convert a string from one charset to another.
* Uses mbstring and iconv functions if possible
*
* @param string Input string
* @param string Suspected charset of the input string
* @param string Target charset to convert to; defaults to RCUBE_CHARSET
*
* @return string Converted string
*/
public static function convert($str, $from, $to = null)
{
static $iconv_options = null;
static $mbstring_list = null;
static $mbstring_sch = null;
static $conv = null;
$to = empty($to) ? RCUBE_CHARSET : $to;
$from = self::parse_charset($from);
// It is a common case when UTF-16 charset is used with US-ASCII content (#1488654)
// In that case we can just skip the conversion (use UTF-8)
if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) {
$from = 'UTF-8';
}
if ($from == $to || empty($str) || empty($from)) {
return $str;
}
if ($iconv_options === null) {
if (function_exists('iconv')) {
// ignore characters not available in output charset
$iconv_options = '//IGNORE';
if (iconv('', $iconv_options, '') === false) {
// iconv implementation does not support options
$iconv_options = '';
}
}
else {
$iconv_options = false;
}
}
// convert charset using iconv module
if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
// throw an exception if iconv reports an illegal character in input
// it means that input string has been truncated
set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE);
try {
$_iconv = iconv($from, $to . $iconv_options, $str);
} catch (ErrorException $e) {
$_iconv = false;
}
restore_error_handler();
if ($_iconv !== false) {
return $_iconv;
}
}
if ($mbstring_list === null) {
if (extension_loaded('mbstring')) {
$mbstring_sch = mb_substitute_character();
$mbstring_list = mb_list_encodings();
$mbstring_list = array_map('strtoupper', $mbstring_list);
}
else {
$mbstring_list = false;
}
}
// convert charset using mbstring module
if ($mbstring_list !== false) {
$aliases['WINDOWS-1257'] = 'ISO-8859-13';
// it happens that mbstring supports ASCII but not US-ASCII
if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) {
$aliases['US-ASCII'] = 'ASCII';
}
$mb_from = $aliases[$from] ? $aliases[$from] : $from;
$mb_to = $aliases[$to] ? $aliases[$to] : $to;
// return if encoding found, string matches encoding and convert succeeded
if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
if (mb_check_encoding($str, $mb_from)) {
// Do the same as //IGNORE with iconv
mb_substitute_character('none');
$out = mb_convert_encoding($str, $mb_to, $mb_from);
mb_substitute_character($mbstring_sch);
if ($out !== false) {
return $out;
}
}
}
}
// convert charset using bundled classes/functions
if ($to == 'UTF-8') {
if ($from == 'UTF7-IMAP') {
if ($_str = self::utf7imap_to_utf8($str)) {
return $_str;
}
}
else if ($from == 'UTF-7') {
if ($_str = self::utf7_to_utf8($str)) {
return $_str;
}
}
else if ($from == 'ISO-8859-1' && function_exists('utf8_encode')) {
return utf8_encode($str);
}
- else if (class_exists('utf8')) {
- if (!$conv) {
- $conv = new utf8($from);
- }
- else {
- $conv->loadCharset($from);
- }
-
- if ($_str = $conv->strToUtf8($str)) {
- return $_str;
- }
+ else {
+ user_error("No suitable function found for UTF-8 encoding", E_USER_WARNING);
}
}
// encode string for output
if ($from == 'UTF-8') {
// @TODO: we need a function for UTF-7 (RFC2152) conversion
if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
if ($_str = self::utf8_to_utf7imap($str)) {
return $_str;
}
}
else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
return utf8_decode($str);
}
- else if (class_exists('utf8')) {
- if (!$conv) {
- $conv = new utf8($to);
- }
- else {
- $conv->loadCharset($from);
- }
-
- if ($_str = $conv->strToUtf8($str)) {
- return $_str;
- }
+ else {
+ user_error("No suitable function found for UTF-8 decoding", E_USER_WARNING);
}
}
// return original string
return $str;
}
/**
* Converts string from standard UTF-7 (RFC 2152) to UTF-8.
*
* @param string Input string (UTF-7)
*
* @return string Converted string (UTF-8)
*/
public static function utf7_to_utf8($str)
{
$Index_64 = array(
0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
);
$u7len = strlen($str);
$str = strval($str);
$res = '';
for ($i=0; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if ($u7 == '+') {
$i++;
$u7len--;
$ch = '';
for (; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if (!$Index_64[ord($u7)]) {
break;
}
$ch .= $u7;
}
if ($ch == '') {
if ($u7 == '-') {
$res .= '+';
}
continue;
}
$res .= self::utf16_to_utf8(base64_decode($ch));
}
else {
$res .= $u7;
}
}
return $res;
}
/**
* Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
*
* @param string Input string
*
* @return string The converted string
*/
public static function utf16_to_utf8($str)
{
$len = strlen($str);
$dec = '';
for ($i = 0; $i < $len; $i += 2) {
$c = ord($str[$i]) << 8 | ord($str[$i + 1]);
if ($c >= 0x0001 && $c <= 0x007F) {
$dec .= chr($c);
}
else if ($c > 0x07FF) {
$dec .= chr(0xE0 | (($c >> 12) & 0x0F));
$dec .= chr(0x80 | (($c >> 6) & 0x3F));
$dec .= chr(0x80 | (($c >> 0) & 0x3F));
}
else {
$dec .= chr(0xC0 | (($c >> 6) & 0x1F));
$dec .= chr(0x80 | (($c >> 0) & 0x3F));
}
}
return $dec;
}
/**
* Convert the data ($str) from RFC 2060's UTF-7 to UTF-8.
* If input data is invalid, return the original input string.
* RFC 2060 obviously intends the encoding to be unique (see
* point 5 in section 5.1.3), so we reject any non-canonical
* form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead
* of &AMAAwA-).
*
* Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
*
* @param string $str Input string (UTF7-IMAP)
*
* @return string Output string (UTF-8)
*/
public static function utf7imap_to_utf8($str)
{
$Index_64 = array(
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, 63,-1,-1,-1,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
);
$u7len = strlen($str);
$str = strval($str);
$p = '';
$err = '';
for ($i=0; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if ($u7 == '&') {
$i++;
$u7len--;
$u7 = $str[$i];
if ($u7len && $u7 == '-') {
$p .= '&';
continue;
}
$ch = 0;
$k = 10;
for (; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if ((ord($u7) & 0x80) || ($b = $Index_64[ord($u7)]) == -1) {
break;
}
if ($k > 0) {
$ch |= $b << $k;
$k -= 6;
}
else {
$ch |= $b >> (-$k);
if ($ch < 0x80) {
// Printable US-ASCII
if (0x20 <= $ch && $ch < 0x7f) {
return $err;
}
$p .= chr($ch);
}
else if ($ch < 0x800) {
$p .= chr(0xc0 | ($ch >> 6));
$p .= chr(0x80 | ($ch & 0x3f));
}
else {
$p .= chr(0xe0 | ($ch >> 12));
$p .= chr(0x80 | (($ch >> 6) & 0x3f));
$p .= chr(0x80 | ($ch & 0x3f));
}
$ch = ($b << (16 + $k)) & 0xffff;
$k += 10;
}
}
// Non-zero or too many extra bits
if ($ch || $k < 6) {
return $err;
}
// BASE64 not properly terminated
if (!$u7len || $u7 != '-') {
return $err;
}
// Adjacent BASE64 sections
if ($u7len > 2 && $str[$i+1] == '&' && $str[$i+2] != '-') {
return $err;
}
}
// Not printable US-ASCII
else if (ord($u7) < 0x20 || ord($u7) >= 0x7f) {
return $err;
}
else {
$p .= $u7;
}
}
return $p;
}
/**
* Convert the data ($str) from UTF-8 to RFC 2060's UTF-7.
* Unicode characters above U+FFFF are replaced by U+FFFE.
* If input data is invalid, return an empty string.
*
* Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
*
* @param string $str Input string (UTF-8)
*
* @return string Output string (UTF7-IMAP)
*/
public static function utf8_to_utf7imap($str)
{
$B64Chars = array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '+', ','
);
$u8len = strlen($str);
$base64 = 0;
$i = 0;
$p = '';
$err = '';
while ($u8len) {
$u8 = $str[$i];
$c = ord($u8);
if ($c < 0x80) {
$ch = $c;
$n = 0;
}
else if ($c < 0xc2) {
return $err;
}
else if ($c < 0xe0) {
$ch = $c & 0x1f;
$n = 1;
}
else if ($c < 0xf0) {
$ch = $c & 0x0f;
$n = 2;
}
else if ($c < 0xf8) {
$ch = $c & 0x07;
$n = 3;
}
else if ($c < 0xfc) {
$ch = $c & 0x03;
$n = 4;
}
else if ($c < 0xfe) {
$ch = $c & 0x01;
$n = 5;
}
else {
return $err;
}
$i++;
$u8len--;
if ($n > $u8len) {
return $err;
}
for ($j=0; $j < $n; $j++) {
$o = ord($str[$i+$j]);
if (($o & 0xc0) != 0x80) {
return $err;
}
$ch = ($ch << 6) | ($o & 0x3f);
}
if ($n > 1 && !($ch >> ($n * 5 + 1))) {
return $err;
}
$i += $n;
$u8len -= $n;
if ($ch < 0x20 || $ch >= 0x7f) {
if (!$base64) {
$p .= '&';
$base64 = 1;
$b = 0;
$k = 10;
}
if ($ch & ~0xffff) {
$ch = 0xfffe;
}
$p .= $B64Chars[($b | $ch >> $k)];
$k -= 6;
for (; $k >= 0; $k -= 6) {
$p .= $B64Chars[(($ch >> $k) & 0x3f)];
}
$b = ($ch << (-$k)) & 0x3f;
$k += 16;
}
else {
if ($base64) {
if ($k > 10) {
$p .= $B64Chars[$b];
}
$p .= '-';
$base64 = 0;
}
$p .= chr($ch);
if (chr($ch) == '&') {
$p .= '-';
}
}
}
if ($base64) {
if ($k > 10) {
$p .= $B64Chars[$b];
}
$p .= '-';
}
return $p;
}
/**
* A method to guess character set of a string.
*
* @param string $string String
* @param string $failover Default result for failover
* @param string $language User language
*
* @return string Charset name
*/
public static function detect($string, $failover = null, $language = null)
{
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';
if (function_exists('mb_detect_encoding')) {
if (empty($language)) {
$rcube = rcube::get_instance();
$language = $rcube->get_user_language();
}
// Prioritize charsets according to current language (#1485669)
switch ($language) {
case 'ja_JP':
$prio = array('ISO-2022-JP', 'JIS', 'UTF-8', 'EUC-JP', 'eucJP-win', 'SJIS', 'SJIS-win');
break;
case 'zh_CN':
case 'zh_TW':
$prio = array('UTF-8', 'BIG-5', 'GB2312', 'EUC-TW');
break;
case 'ko_KR':
$prio = array('UTF-8', 'EUC-KR', 'ISO-2022-KR');
break;
case 'ru_RU':
$prio = array('UTF-8', 'WINDOWS-1251', 'KOI8-R');
break;
case 'tr_TR':
$prio = array('UTF-8', 'ISO-8859-9', 'WINDOWS-1254');
break;
default:
$prio = array('UTF-8', 'SJIS', 'GB2312',
'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', 'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG-5',
'ISO-2022-KR', 'ISO-2022-JP',
);
}
$encodings = array_unique(array_merge($prio, mb_list_encodings()));
return mb_detect_encoding($string, $encodings);
}
// 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 $failover;
}
/**
* Removes non-unicode characters from input.
*
* @param mixed $input String or array.
*
* @return mixed String or array
*/
public static function clean($input)
{
// handle input of type array
if (is_array($input)) {
foreach ($input as $idx => $val) {
$input[$idx] = self::clean($val);
}
return $input;
}
if (!is_string($input) || $input == '') {
return $input;
}
// iconv/mbstring are much faster (especially with long strings)
if (function_exists('mb_convert_encoding')) {
$msch = mb_substitute_character('none');
mb_substitute_character('none');
$res = mb_convert_encoding($input, 'UTF-8', 'UTF-8');
mb_substitute_character($msch);
if ($res !== false) {
return $res;
}
}
if (function_exists('iconv')) {
if (($res = @iconv('UTF-8', 'UTF-8//IGNORE', $input)) !== false) {
return $res;
}
}
$seq = '';
$out = '';
$regexp = '/^('.
// '[\x00-\x7F]'. // UTF8-1
'|[\xC2-\xDF][\x80-\xBF]'. // UTF8-2
'|\xE0[\xA0-\xBF][\x80-\xBF]'. // UTF8-3
'|[\xE1-\xEC][\x80-\xBF][\x80-\xBF]'. // UTF8-3
'|\xED[\x80-\x9F][\x80-\xBF]'. // UTF8-3
'|[\xEE-\xEF][\x80-\xBF][\x80-\xBF]'. // UTF8-3
'|\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]'. // UTF8-4
'|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]'.// UTF8-4
'|\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]'. // UTF8-4
')$/';
for ($i = 0, $len = strlen($input); $i < $len; $i++) {
$chr = $input[$i];
$ord = ord($chr);
// 1-byte character
if ($ord <= 0x7F) {
if ($seq) {
$out .= preg_match($regexp, $seq) ? $seq : '';
}
$seq = '';
$out .= $chr;
}
// first (or second) byte of multibyte sequence
else if ($ord >= 0xC0) {
if (strlen($seq) > 1) {
$out .= preg_match($regexp, $seq) ? $seq : '';
$seq = '';
}
else if ($seq && ord($seq) < 0xC0) {
$seq = '';
}
$seq .= $chr;
}
// next byte of multibyte sequence
else if ($seq) {
$seq .= $chr;
}
}
if ($seq) {
$out .= preg_match($regexp, $seq) ? $seq : '';
}
return $out;
}
}
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index 5e1a40e..bd3a3f8 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -1,1018 +1,1018 @@
<?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 Framework
* @subpackage 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_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');
public $date_cols = array('birthday', 'anniversary');
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
* @param int Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
$results = array();
if (!$this->groups)
return $results;
if ($search) {
switch (intval($mode)) {
case 1:
$sql_filter = $this->db->ilike('name', $search);
break;
case 2:
$sql_filter = $this->db->ilike('name', $search . '%');
break;
default:
$sql_filter = $this->db->ilike('name', '%' . $search . '%');
}
$sql_filter = " AND $sql_filter";
}
$sql_result = $this->db->query(
- "SELECT * FROM ".$this->db->table_name($this->db_groups).
- " WHERE del<>1".
- " AND user_id=?".
- $sql_filter.
- " ORDER BY name",
+ "SELECT * FROM " . $this->db->table_name($this->db_groups, true)
+ . " 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=?",
+ "SELECT * FROM " . $this->db->table_name($this->db_groups, true)
+ . " 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.")";
+ $join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " 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);
+ $order_cols = array("c.`$order_col`");
if ($order_col == 'firstname')
- $order_cols[] = 'c.surname';
+ $order_cols[] = 'c.`surname`';
else if ($order_col == 'surname')
- $order_cols[] = 'c.firstname';
+ $order_cols[] = 'c.`firstname`';
if ($order_col != 'name')
- $order_cols[] = 'c.name';
- $order_cols[] = 'c.email';
+ $order_cols[] = 'c.`name`';
+ $order_cols[] = 'c.`email`';
$sql_result = $this->db->limitquery(
- "SELECT * FROM ".$this->db->table_name($this->db_name)." AS c" .
+ "SELECT * FROM " . $this->db->table_name($this->db_name, true) . " AS c" .
$join .
- " WHERE c.del<>1" .
- " AND c.user_id=?" .
- ($this->group_id ? " AND m.contactgroup_id=?" : "").
+ " 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'] = $sql_arr['email'] ? explode(self::SEPARATOR, $sql_arr['email']) : array();
$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->quote_identifier($col) . ' = ' . $this->db->quote($val)
. ' OR ' . $this->db->ilike($col, $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')';
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->quote_identifier($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) {
if ($this->compare_search_value($colname, $value, $search, $mode)) {
$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.')';
+ $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.")";
+ $join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " 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".
+ "SELECT COUNT(c.`contact_id`) AS rows".
+ " FROM " . $this->db->table_name($this->db_name, true) . " AS c".
$join.
- " WHERE c.del<>1".
- " AND c.user_id=?".
- ($this->group_id ? " AND m.contactgroup_id=?" : "").
+ " 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",
+ "SELECT * FROM " . $this->db->table_name($this->db_name, true).
+ " 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=?",
+ "SELECT cgm.`contactgroup_id`, cg.`name` "
+ . " FROM " . $this->db->table_name($this->db_groupmembers, true) . " AS cgm"
+ . " LEFT JOIN " . $this->db->table_name($this->db_groups, true) . " 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 email address or a name
if ($valid && !strlen($save_data['firstname'].$save_data['surname'].$save_data['name']) && !array_filter($this->get_col_values('email', $save_data, true))) {
$this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
$valid = false;
}
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->quote_identifier($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).")".
+ "INSERT INTO " . $this->db->table_name($this->db_name, true).
+ " (`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);
}
$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->quote_identifier($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",
+ "UPDATE " . $this->db->table_name($this->db_name, true).
+ " 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 ? true : false;
}
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'], RCUBE_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'], RCUBE_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);
// avoid casting DateTime objects to array
if (is_object($values) && is_a($values, 'DateTime')) {
$values = array(0 => $values);
}
foreach ((array)$values as $value) {
if (isset($value))
$vcard->set($field, $value, $section);
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)",
+ "UPDATE " . $this->db->table_name($this->db_name, true).
+ " 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)",
+ "UPDATE " . $this->db->table_name($this->db_name, true).
+ " 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
*
* @param bool $with_groups Remove also groups
*
* @return int Number of removed records
*/
function delete_all($with_groups = false)
{
$this->cache = null;
- $this->db->query("UPDATE " . $this->db->table_name($this->db_name)
- . " SET del = 1, changed = " . $this->db->now()
- . " WHERE user_id = ?", $this->user_id);
+ $now = $this->db->now();
+
+ $this->db->query("UPDATE " . $this->db->table_name($this->db_name, true)
+ . " SET `del` = 1, `changed` = $now"
+ . " WHERE `user_id` = ?", $this->user_id);
$count = $this->db->affected_rows();
if ($with_groups) {
- $this->db->query("UPDATE " . $this->db->table_name($this->db_groups)
- . " SET del = 1, changed = " . $this->db->now()
- . " WHERE user_id = ?", $this->user_id);
+ $this->db->query("UPDATE " . $this->db->table_name($this->db_groups, true)
+ . " SET `del` = 1, `changed` = $now"
+ . " WHERE `user_id` = ?", $this->user_id);
$count += $this->db->affected_rows();
}
return $count;
}
/**
* 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)".
+ "INSERT INTO " . $this->db->table_name($this->db_groups, true).
+ " (`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))
+ 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
$this->db->query(
- "UPDATE " . $this->db->table_name($this->db_groups)
- . " SET del = 1, changed = " . $this->db->now()
- . " WHERE contactgroup_id = ?"
- . " AND user_id = ?",
+ "UPDATE " . $this->db->table_name($this->db_groups, true)
+ . " 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=?",
+ "UPDATE " . $this->db->table_name($this->db_groups, true).
+ " 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|string 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').")",
+ "SELECT `contact_id` FROM " . $this->db->table_name($this->db_groupmembers, true).
+ " 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)".
+ "INSERT INTO " . $this->db->table_name($this->db_groupmembers, true).
+ " (`contactgroup_id`, `contact_id`, `created`)".
" VALUES (?, ?, ".$this->db->now().")",
$group_id,
$contact_id
);
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|string 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)",
+ "DELETE FROM " . $this->db->table_name($this->db_groupmembers, true).
+ " 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=?",
+ "SELECT 1 FROM " . $this->db->table_name($this->db_groups, true).
+ " WHERE `del` <> 1".
+ " AND `user_id` = ?".
+ " AND `name` = ?",
$this->user_id,
$checkname);
// append number to make name unique
if ($hit = $this->db->fetch_array($sql_result)) {
$checkname = $name . ' ' . $num++;
}
} while ($hit);
return $checkname;
}
-
}
diff --git a/lib/ext/Roundcube/rcube_csv2vcard.php b/lib/ext/Roundcube/rcube_csv2vcard.php
index 06bc387..b7d1591 100644
--- a/lib/ext/Roundcube/rcube_csv2vcard.php
+++ b/lib/ext/Roundcube/rcube_csv2vcard.php
@@ -1,473 +1,630 @@
<?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: |
| CSV to vCard data conversion |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* CSV to vCard data converter
*
* @package Framework
* @subpackage Addressbook
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_csv2vcard
{
/**
* CSV to vCard fields mapping
*
* @var array
*/
protected $csv2vcard_map = array(
// MS Outlook 2010
'anniversary' => 'anniversary',
'assistants_name' => 'assistant',
'assistants_phone' => 'phone:assistant',
'birthday' => 'birthday',
'business_city' => 'locality:work',
'business_countryregion' => 'country:work',
'business_fax' => 'phone:work,fax',
'business_phone' => 'phone:work',
'business_phone_2' => 'phone:work2',
'business_postal_code' => 'zipcode:work',
'business_state' => 'region:work',
'business_street' => 'street:work',
//'business_street_2' => '',
//'business_street_3' => '',
'car_phone' => 'phone:car',
'categories' => 'groups',
//'children' => '',
'company' => 'organization',
//'company_main_phone' => '',
'department' => 'department',
//'email_2_address' => '', //@TODO
//'email_2_type' => '',
//'email_3_address' => '', //@TODO
//'email_3_type' => '',
'email_address' => 'email:pref',
//'email_type' => '',
'first_name' => 'firstname',
'gender' => 'gender',
'home_city' => 'locality:home',
'home_countryregion' => 'country:home',
'home_fax' => 'phone:home,fax',
'home_phone' => 'phone:home',
'home_phone_2' => 'phone:home2',
'home_postal_code' => 'zipcode:home',
'home_state' => 'region:home',
'home_street' => 'street:home',
//'home_street_2' => '',
//'home_street_3' => '',
//'initials' => '',
//'isdn' => '',
'job_title' => 'jobtitle',
//'keywords' => '',
//'language' => '',
'last_name' => 'surname',
//'location' => '',
'managers_name' => 'manager',
'middle_name' => 'middlename',
//'mileage' => '',
'mobile_phone' => 'phone:cell',
'notes' => 'notes',
//'office_location' => '',
'other_city' => 'locality:other',
'other_countryregion' => 'country:other',
'other_fax' => 'phone:other,fax',
'other_phone' => 'phone:other',
'other_postal_code' => 'zipcode:other',
'other_state' => 'region:other',
'other_street' => 'street:other',
//'other_street_2' => '',
//'other_street_3' => '',
'pager' => 'phone:pager',
'primary_phone' => 'phone:pref',
//'profession' => '',
//'radio_phone' => '',
'spouse' => 'spouse',
'suffix' => 'suffix',
'title' => 'title',
'web_page' => 'website:homepage',
// Thunderbird
'birth_day' => 'birthday-d',
'birth_month' => 'birthday-m',
'birth_year' => 'birthday-y',
'display_name' => 'displayname',
'fax_number' => 'phone:fax',
'home_address' => 'street:home',
//'home_address_2' => '',
'home_country' => 'country:home',
'home_zipcode' => 'zipcode:home',
'mobile_number' => 'phone:cell',
'nickname' => 'nickname',
'organization' => 'organization',
'pager_number' => 'phone:pager',
'primary_email' => 'email:pref',
'secondary_email' => 'email:other',
'web_page_1' => 'website:homepage',
'web_page_2' => 'website:other',
'work_phone' => 'phone:work',
'work_address' => 'street:work',
//'work_address_2' => '',
'work_country' => 'country:work',
'work_zipcode' => 'zipcode:work',
'last' => 'surname',
'first' => 'firstname',
'work_city' => 'locality:work',
'work_state' => 'region:work',
'home_city_short' => 'locality:home',
'home_state_short' => 'region:home',
// Atmail
'date_of_birth' => 'birthday',
'email' => 'email:pref',
'home_mobile' => 'phone:cell',
'home_zip' => 'zipcode:home',
'info' => 'notes',
'user_photo' => 'photo',
'url' => 'website:homepage',
'work_company' => 'organization',
'work_dept' => 'departament',
'work_fax' => 'phone:work,fax',
'work_mobile' => 'phone:work,cell',
'work_title' => 'jobtitle',
'work_zip' => 'zipcode:work',
'group' => 'groups',
// GMail
'groups' => 'groups',
+ 'group_membership' => 'groups',
+ 'given_name' => 'firstname',
+ 'additional_name' => 'middlename',
+ 'family_name' => 'surname',
+ 'name' => 'displayname',
+ 'name_prefix' => 'prefix',
+ 'name_suffix' => 'suffix',
);
/**
* CSV label to text mapping for English
*
* @var array
*/
protected $label_map = array(
// MS Outlook 2010
'anniversary' => "Anniversary",
'assistants_name' => "Assistant's Name",
'assistants_phone' => "Assistant's Phone",
'birthday' => "Birthday",
'business_city' => "Business City",
'business_countryregion' => "Business Country/Region",
'business_fax' => "Business Fax",
'business_phone' => "Business Phone",
'business_phone_2' => "Business Phone 2",
'business_postal_code' => "Business Postal Code",
'business_state' => "Business State",
'business_street' => "Business Street",
//'business_street_2' => "Business Street 2",
//'business_street_3' => "Business Street 3",
'car_phone' => "Car Phone",
'categories' => "Categories",
//'children' => "Children",
'company' => "Company",
//'company_main_phone' => "Company Main Phone",
'department' => "Department",
//'directory_server' => "Directory Server",
//'email_2_address' => "E-mail 2 Address",
//'email_2_type' => "E-mail 2 Type",
//'email_3_address' => "E-mail 3 Address",
//'email_3_type' => "E-mail 3 Type",
'email_address' => "E-mail Address",
//'email_type' => "E-mail Type",
'first_name' => "First Name",
'gender' => "Gender",
'home_city' => "Home City",
'home_countryregion' => "Home Country/Region",
'home_fax' => "Home Fax",
'home_phone' => "Home Phone",
'home_phone_2' => "Home Phone 2",
'home_postal_code' => "Home Postal Code",
'home_state' => "Home State",
'home_street' => "Home Street",
//'home_street_2' => "Home Street 2",
//'home_street_3' => "Home Street 3",
//'initials' => "Initials",
//'isdn' => "ISDN",
'job_title' => "Job Title",
//'keywords' => "Keywords",
//'language' => "Language",
'last_name' => "Last Name",
//'location' => "Location",
'managers_name' => "Manager's Name",
'middle_name' => "Middle Name",
//'mileage' => "Mileage",
'mobile_phone' => "Mobile Phone",
'notes' => "Notes",
//'office_location' => "Office Location",
'other_city' => "Other City",
'other_countryregion' => "Other Country/Region",
'other_fax' => "Other Fax",
'other_phone' => "Other Phone",
'other_postal_code' => "Other Postal Code",
'other_state' => "Other State",
'other_street' => "Other Street",
//'other_street_2' => "Other Street 2",
//'other_street_3' => "Other Street 3",
'pager' => "Pager",
'primary_phone' => "Primary Phone",
//'profession' => "Profession",
//'radio_phone' => "Radio Phone",
'spouse' => "Spouse",
'suffix' => "Suffix",
'title' => "Title",
'web_page' => "Web Page",
// Thunderbird
'birth_day' => "Birth Day",
'birth_month' => "Birth Month",
'birth_year' => "Birth Year",
'display_name' => "Display Name",
'fax_number' => "Fax Number",
'home_address' => "Home Address",
//'home_address_2' => "Home Address 2",
'home_country' => "Home Country",
'home_zipcode' => "Home ZipCode",
'mobile_number' => "Mobile Number",
'nickname' => "Nickname",
'organization' => "Organization",
'pager_number' => "Pager Namber",
'primary_email' => "Primary Email",
'secondary_email' => "Secondary Email",
'web_page_1' => "Web Page 1",
'web_page_2' => "Web Page 2",
'work_phone' => "Work Phone",
'work_address' => "Work Address",
//'work_address_2' => "Work Address 2",
'work_city' => "Work City",
'work_country' => "Work Country",
'work_state' => "Work State",
'work_zipcode' => "Work ZipCode",
// Atmail
'date_of_birth' => "Date of Birth",
'email' => "Email",
//'email_2' => "Email2",
//'email_3' => "Email3",
//'email_4' => "Email4",
//'email_5' => "Email5",
'home_mobile' => "Home Mobile",
'home_zip' => "Home Zip",
'info' => "Info",
'user_photo' => "User Photo",
'url' => "URL",
'work_company' => "Work Company",
'work_dept' => "Work Dept",
'work_fax' => "Work Fax",
'work_mobile' => "Work Mobile",
'work_title' => "Work Title",
'work_zip' => "Work Zip",
- 'groups' => "Group",
+ 'group' => "Group",
+
+ // GMail
+ 'groups' => "Groups",
+ 'group_membership' => "Group Membership",
+ 'given_name' => "Given Name",
+ 'additional_name' => "Additional Name",
+ 'family_name' => "Family Name",
+ 'name' => "Name",
+ 'name_prefix' => "Name Prefix",
+ 'name_suffix' => "Name Suffix",
+ );
+
+ /**
+ * Special fields map for GMail format
+ *
+ * @var array
+ */
+ protected $gmail_label_map = array(
+ 'E-mail' => array(
+ 'Value' => array(
+ 'home' => 'email:home',
+ 'work' => 'email:work',
+ ),
+ ),
+ 'Phone' => array(
+ 'Value' => array(
+ 'home' => 'phone:home',
+ 'homefax' => 'phone:homefax',
+ 'main' => 'phone:pref',
+ 'pager' => 'phone:pager',
+ 'mobile' => 'phone:cell',
+ 'work' => 'phone:work',
+ 'workfax' => 'phone:workfax',
+ ),
+ ),
+ 'Relation' => array(
+ 'Value' => array(
+ 'spouse' => 'spouse',
+ ),
+ ),
+ 'Website' => array(
+ 'Value' => array(
+ 'profile' => 'website:profile',
+ 'blog' => 'website:blog',
+ 'homepage' => 'website:homepage',
+ 'work' => 'website:work',
+ ),
+ ),
+ 'Address' => array(
+ 'Street' => array(
+ 'home' => 'street:home',
+ 'work' => 'street:work',
+ ),
+ 'City' => array(
+ 'home' => 'locality:home',
+ 'work' => 'locality:work',
+ ),
+ 'Region' => array(
+ 'home' => 'region:home',
+ 'work' => 'region:work',
+ ),
+ 'Postal Code' => array(
+ 'home' => 'zipcode:home',
+ 'work' => 'zipcode:work',
+ ),
+ 'Country' => array(
+ 'home' => 'country:home',
+ 'work' => 'country:work',
+ ),
+ ),
+ 'Organization' => array(
+ 'Name' => array(
+ '' => 'organization',
+ ),
+ 'Title' => array(
+ '' => 'jobtitle',
+ ),
+ 'Department' => array(
+ '' => 'department',
+ ),
+ ),
);
+
protected $local_label_map = array();
- protected $vcards = array();
- protected $map = array();
+ protected $vcards = array();
+ protected $map = array();
+ protected $gmail_map = array();
/**
* Class constructor
*
* @param string $lang File language
*/
public function __construct($lang = 'en_US')
{
// Localize fields map
if ($lang && $lang != 'en_US') {
if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc")) {
include RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc";
}
if (!empty($map)) {
$this->local_label_map = array_merge($this->label_map, $map);
}
}
$this->label_map = array_flip($this->label_map);
$this->local_label_map = array_flip($this->local_label_map);
}
/**
*
*/
public function import($csv)
{
// convert to UTF-8
- $head = substr($csv, 0, 4096);
- $charset = rcube_charset::detect($head, RCUBE_CHARSET);
- $csv = rcube_charset::convert($csv, $charset);
- $head = '';
+ $head = substr($csv, 0, 4096);
+ $charset = rcube_charset::detect($head, RCUBE_CHARSET);
+ $csv = rcube_charset::convert($csv, $charset);
+ $csv = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM
+ $head = '';
+ $prev_line = false;
- $this->map = array();
+ $this->map = array();
+ $this->gmail_map = array();
// Parse file
foreach (preg_split("/[\r\n]+/", $csv) as $line) {
+ if (!empty($prev_line)) {
+ $line = '"' . $line;
+ }
+
$elements = $this->parse_line($line);
+
if (empty($elements)) {
continue;
}
// Parse header
if (empty($this->map)) {
$this->parse_header($elements);
if (empty($this->map)) {
break;
}
}
// Parse data row
else {
+ // handle multiline elements (e.g. Gmail)
+ if (!empty($prev_line)) {
+ $first = array_shift($elements);
+
+ if ($first[0] == '"') {
+ $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1);
+ }
+ else {
+ $prev_line[count($prev_line)-1] .= "\n" . $first;
+ }
+
+ $elements = array_merge($prev_line, $elements);
+ }
+
+ $last_element = $elements[count($elements)-1];
+ if ($last_element[0] == '"') {
+ $elements[count($elements)-1] = substr($last_element, 1);
+ $prev_line = $elements;
+ continue;
+ }
$this->csv_to_vcard($elements);
+ $prev_line = false;
}
}
}
/**
* @return array rcube_vcard List of vcards
*/
public function export()
{
return $this->vcards;
}
/**
* Parse CSV file line
*/
protected function parse_line($line)
{
$line = trim($line);
if (empty($line)) {
return null;
}
$fields = rcube_utils::explode_quoted_string(',', $line);
// remove quotes if needed
if (!empty($fields)) {
foreach ($fields as $idx => $value) {
if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') {
// remove surrounding quotes
$value = substr($value, 1, -1);
// replace doubled quotes inside the string with single quote
$value = str_replace('""', '"', $value);
$fields[$idx] = $value;
}
}
}
return $fields;
}
/**
* Parse CSV header line, detect fields mapping
*/
protected function parse_header($elements)
{
$map1 = array();
$map2 = array();
$size = count($elements);
// check English labels
for ($i = 0; $i < $size; $i++) {
$label = $this->label_map[$elements[$i]];
if ($label && !empty($this->csv2vcard_map[$label])) {
$map1[$i] = $this->csv2vcard_map[$label];
}
}
+
// check localized labels
if (!empty($this->local_label_map)) {
for ($i = 0; $i < $size; $i++) {
$label = $this->local_label_map[$elements[$i]];
// special localization label
if ($label && $label[0] == '_') {
$label = substr($label, 1);
}
if ($label && !empty($this->csv2vcard_map[$label])) {
$map2[$i] = $this->csv2vcard_map[$label];
}
}
}
$this->map = count($map1) >= count($map2) ? $map1 : $map2;
+
+ // support special Gmail format
+ foreach ($this->gmail_label_map as $key => $items) {
+ $num = 1;
+ while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
+ $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found);
+ foreach (array_keys($items) as $item_key) {
+ $_key = "$key $num - $item_key";
+ if (($found = array_search($_key, $elements)) !== false) {
+ $this->gmail_map["$key:$num"][$item_key] = $found;
+ }
+ }
+
+ $num++;
+ }
+ }
}
/**
* Convert CSV data row to vCard
*/
protected function csv_to_vcard($data)
{
$contact = array();
foreach ($this->map as $idx => $name) {
$value = $data[$idx];
if ($value !== null && $value !== '') {
$contact[$name] = $value;
}
}
+ // Gmail format support
+ foreach ($this->gmail_map as $idx => $item) {
+ $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']]));
+ $key = $item['_key'];
+
+ unset($item['_idx']);
+ unset($item['_key']);
+
+ foreach ($item as $item_key => $item_idx) {
+ $value = $data[$item_idx];
+ if ($value !== null && $value !== '' && ($data_idx = $this->gmail_label_map[$key][$item_key][$type])) {
+ $contact[$data_idx] = $value;
+ }
+ }
+ }
+
if (empty($contact)) {
return;
}
// Handle special values
if (!empty($contact['birthday-d']) && !empty($contact['birthday-m']) && !empty($contact['birthday-y'])) {
$contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
}
- // categories/groups separator in vCard is ',' not ';'
if (!empty($contact['groups'])) {
+ // categories/groups separator in vCard is ',' not ';'
$contact['groups'] = str_replace(';', ',', $contact['groups']);
+
+ // remove "* " added by GMail
+ if (!empty($this->gmail_map)) {
+ $contact['groups'] = str_replace('* ', '', $contact['groups']);
+ }
}
// Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
foreach (array('birthday', 'anniversary') as $key) {
if (!empty($contact[$key])) {
$date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
if (empty($date)) {
unset($contact[$key]);
}
}
}
if (!empty($contact['gender']) && ($gender = strtolower($contact['gender']))) {
if (!in_array($gender, array('male', 'female'))) {
unset($contact['gender']);
}
}
// Convert address(es) to rcube_vcard data
foreach ($contact as $idx => $value) {
$name = explode(':', $idx);
if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) {
$contact['address:'.$name[1]][$name[0]] = $value;
unset($contact[$idx]);
}
}
// Create vcard object
$vcard = new rcube_vcard();
foreach ($contact as $name => $value) {
$name = explode(':', $name);
$vcard->set($name[0], $value, $name[1]);
}
// add to the list
$this->vcards[] = $vcard;
}
}
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
index 1e6a206..5a76f69 100644
--- a/lib/ext/Roundcube/rcube_db.php
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -1,1330 +1,1334 @@
<?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 Framework
* @subpackage Database
*/
class rcube_db
{
public $db_provider;
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 $dbhs = array();
protected $table_connections = array();
protected $db_error = false;
protected $db_error_msg = '';
protected $conn_failure = false;
protected $db_index = 0;
protected $last_result;
protected $tables;
protected $variables;
protected $options = array(
// column/table quotes
'identifier_start' => '"',
'identifier_end' => '"',
);
const DEBUG_LINE_LENGTH = 4096;
const DEFAULT_QUOTE = '`';
/**
* 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',
'oci' => 'oracle',
'oci8' => 'oracle',
);
$driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver;
$class = "rcube_db_$driver";
if (!$driver || !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);
$config = rcube::get_instance()->config;
$this->options['table_prefix'] = $config->get('db_prefix');
$this->options['dsnw_noread'] = $config->get('db_dsnw_noread', false);
$this->options['table_dsn_map'] = array_map(array($this, 'table_name'), $config->get('db_table_dsn', array()));
}
/**
* Connect to specific database
*
* @param array $dsn DSN for DB connections
* @param string $mode Connection mode (r|w)
*/
protected function dsn_connect($dsn, $mode)
{
$this->db_error = false;
$this->db_error_msg = null;
// return existing handle
if ($this->dbhs[$mode]) {
$this->dbh = $this->dbhs[$mode];
$this->db_mode = $mode;
return $this->dbh;
}
// connect to database
if ($dbh = $this->conn_create($dsn)) {
$this->dbh = $dbh;
$this->dbhs[$mode] = $dbh;
$this->db_mode = $mode;
$this->db_connected = true;
}
}
/**
* Create PDO connection
*/
protected function conn_create($dsn)
{
// Get database specific connection options
$dsn_string = $this->dsn_string($dsn);
$dsn_options = $this->dsn_options($dsn);
if ($this->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);
$this->conn_configure($dsn, $dbh);
}
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;
}
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)
{
}
/**
* Connect to appropriate database depending on the operation
*
* @param string $mode Connection mode (r|w)
* @param boolean $force Enforce using the given mode
*/
public function db_connect($mode, $force = false)
{
// 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 allowed)
if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->options['dsnw_noread']) {
return;
}
}
$dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
$this->dsn_connect($dsn, $mode);
// use write-master when read-only fails
if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) {
$this->dsn_connect($this->db_dsnw_array, 'w');
}
$this->conn_failure = !$this->db_connected;
}
/**
* Analyze the given SQL statement and select the appropriate connection to use
*/
protected function dsn_select($query)
{
// no replication
if ($this->db_dsnw == $this->db_dsnr) {
return 'w';
}
// Read or write ?
$mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
$start = '[' . $this->options['identifier_start'] . self::DEFAULT_QUOTE . ']';
$end = '[' . $this->options['identifier_end'] . self::DEFAULT_QUOTE . ']';
$regex = '/(?:^|\s)(from|update|into|join)\s+'.$start.'?([a-z0-9._]+)'.$end.'?\s+/i';
// find tables involved in this query
if (preg_match_all($regex, $query, $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$table = $m[2];
// always use direct mapping
if ($this->options['table_dsn_map'][$table]) {
$mode = $this->options['table_dsn_map'][$table];
break; // primary table rules
}
else if ($mode == 'r') {
// connected to db with the same or "higher" mode for this table
$db_mode = $this->table_connections[$table];
if ($db_mode == 'w' && !$this->options['dsnw_noread']) {
$mode = $db_mode;
}
}
}
// remember mode chosen (for primary table)
$table = $matches[0][2];
$this->table_connections[$table] = $mode;
}
return $mode;
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if SQL queries should be logged
*/
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']) {
if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$query = substr($query, 0, self::DEBUG_LINE_LENGTH)
. "... [truncated $diff bytes]";
}
rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
}
}
/**
* Getter for error state
*
* @param mixed $result Optional query result
*
* @return string Error message
*/
public function is_error($result = null)
{
if ($result !== null) {
return $result === 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 PDOStatement|bool Query handle or False on error
*/
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 PDOStatement|bool Query handle or False on error
*/
protected function _query($query, $offset, $numrows, $params)
{
$query = ltrim($query);
$this->db_connect($this->dsn_select($query), true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
if ($numrows || $offset) {
$query = $this->set_limit($query, $numrows, $offset);
}
// replace self::DEFAULT_QUOTE with driver-specific quoting
$query = $this->query_parse($query);
// Because in Roundcube we mostly use queries that are
// executed only once, we will not use prepared queries
$pos = 0;
$idx = 0;
if (count($params)) {
while ($pos = strpos($query, '?', $pos)) {
if ($query[$pos+1] == '?') { // skip escaped '?'
$pos += 2;
}
else {
$val = $this->quote($params[$idx++]);
unset($params[$idx-1]);
$query = substr_replace($query, $val, $pos, 1);
$pos += strlen($val);
}
}
}
// replace escaped '?' back to normal, see self::quote()
$query = str_replace('??', '?', $query);
$query = rtrim($query, " \t\n\r\0\x0B;");
// log query
$this->debug($query);
return $this->query_execute($query);
}
/**
* Query execution
*/
protected function query_execute($query)
{
// destroy reference to previous result, required for SQLite driver (#1488874)
$this->last_result = null;
$this->db_error_msg = null;
// send query
$result = $this->dbh->query($query);
if ($result === false) {
$result = $this->handle_error($query);
}
return $this->last_result = $result;
}
/**
* Parse SQL query and replace identifier quoting
*
* @param string $query SQL query
*
* @return string SQL query
*/
protected function query_parse($query)
{
$start = $this->options['identifier_start'];
$end = $this->options['identifier_end'];
$quote = self::DEFAULT_QUOTE;
if ($start == $quote) {
return $query;
}
$pos = 0;
$in = false;
while ($pos = strpos($query, $quote, $pos)) {
if ($query[$pos+1] == $quote) { // skip escaped quote
$pos += 2;
}
else {
if ($in) {
$q = $end;
$in = false;
}
else {
$q = $start;
$in = true;
}
$query = substr_replace($query, $q, $pos, 1);
$pos++;
}
}
// replace escaped quote back to normal, see self::quote()
$query = str_replace($quote.$quote, $quote, $query);
return $query;
}
/**
* Helper method to handle DB errors.
* This by default logs the error but could be overriden by a driver implementation
*
* @param string Query that triggered the error
* @return mixed Result to be stored and returned
*/
protected function handle_error($query)
{
$error = $this->dbh->errorInfo();
if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) {
$this->db_error = true;
$this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg . " (SQL Query: $query)"
), true, false);
}
return false;
}
/**
* Get number of affected rows for the last query
*
* @param mixed $result Optional query handle
*
* @return int Number of (matching) rows
*/
public function affected_rows($result = null)
{
if ($result || ($result === null && ($result = $this->last_result))) {
if ($result !== true) {
return $result->rowCount();
}
}
return 0;
}
/**
* Get number of rows for a SQL query
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
* @return mixed Number of rows or false on failure
* @deprecated This method shows very poor performance and should be avoided.
*/
public function num_rows($result = null)
{
if (($result || ($result === null && ($result = $this->last_result))) && $result !== true) {
// repeat query with SELECT COUNT(*) ...
if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) {
$query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM);
return $query ? intval($query->fetchColumn(0)) : false;
}
else {
$num = count($result->fetchAll());
$result->execute(); // re-execute query because there's no seek(0)
return $num;
}
}
return false;
}
/**
* Get last inserted record ID
*
* @param string $table Table name (to find the incremented sequence)
*
* @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 mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_assoc($result = null)
{
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 mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_array($result = null)
{
return $this->_fetch_row($result, PDO::FETCH_NUM);
}
/**
* Get col values for a result row
*
* @param mixed $result Optional query handle
* @param int $mode Fetch mode identifier
*
* @return mixed Array with col values or false on failure
*/
protected function _fetch_row($result, $mode)
{
if ($result || ($result === null && ($result = $this->last_result))) {
if ($result !== true) {
return $result->fetch($mode);
}
}
return false;
}
/**
* 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 ($q) {
$this->tables = $q->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 ($q) {
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
}
return array();
}
/**
* Start transaction
*
* @return bool True on success, False on failure
*/
public function startTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('BEGIN TRANSACTION');
return $this->last_result = $this->dbh->beginTransaction();
}
/**
* Commit transaction
*
* @return bool True on success, False on failure
*/
public function endTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('COMMIT TRANSACTION');
return $this->last_result = $this->dbh->commit();
}
/**
* Rollback transaction
*
* @return bool True on success, False on failure
*/
public function rollbackTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('ROLLBACK TRANSACTION');
return $this->last_result = $this->dbh->rollBack();
}
/**
* Formats input so it can be safely used in a query
*
* @param mixed $input Value to quote
* @param string $type Type of data (integer, bool, ident)
*
* @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);
}
if (is_null($input)) {
return 'NULL';
}
if ($type == 'ident') {
return $this->quote_identifier($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 strtr($this->dbh->quote($input, $type),
// escape ? and `
array('?' => '??', self::DEFAULT_QUOTE => self::DEFAULT_QUOTE.self::DEFAULT_QUOTE)
);
}
return 'NULL';
}
/**
* Escapes a string so it can be safely used in a query
*
* @param string $str A string to escape
*
* @return string Escaped string for use in a query
*/
public function escape($str)
{
if (is_null($str)) {
return 'NULL';
}
return substr($this->quote($str), 1, -1);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string $str Value to quote
*
* @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);
}
/**
* Escapes a string so it can be safely used in a query
*
* @param string $str A string to escape
*
* @return string Escaped string for use in a query
* @deprecated Replaced by rcube_db::escape
* @see rcube_db::escape
*/
public function escapeSimple($str)
{
return $this->escape($str);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string $str Value to quote
*
* @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
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL ';
$add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
$add .= ' SECOND';
}
return "now()" . $add;
}
/**
* Return list of elements for use with SQL's IN clause
*
* @param array $arr Input array
* @param string $type Type of data (integer, bool, ident)
*
* @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
* @param bool $serialized Enable serialization
*
* @return mixed Properly UTF-8 encoded data
*/
public static function encode($input, $serialized = false)
{
// use Base64 encoding to workaround issues with invalid
// or null characters in serialized string (#1489142)
if ($serialized) {
return base64_encode(serialize($input));
}
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value) {
$input->$idx = self::encode($value);
}
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
* @param bool $serialized Enable serialization
*
* @return mixed Decoded data
*/
public static function decode($input, $serialized = false)
{
// use Base64 encoding to workaround issues with invalid
// or null characters in serialized string (#1489142)
if ($serialized) {
// Keep backward compatybility where base64 wasn't used
if (strpos(substr($input, 0, 16), ':') !== false) {
return self::decode(@unserialize($input));
}
return @unserialize(base64_decode($input));
}
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value) {
$input->$idx = self::decode($value);
}
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value) {
$input[$idx] = self::decode($value);
}
return $input;
}
return utf8_decode($input);
}
/**
* Return correct name for a specific database table
*
* @param string $table Table name
* @param bool $quoted Quote table identifier
*
* @return string Translated table name
*/
public function table_name($table, $quoted = false)
{
+ // let plugins alter the table name (#1489837)
+ $plugin = rcube::get_instance()->plugins->exec_hook('db_table_name', array('table' => $table));
+ $table = $plugin['table'];
+
// add prefix to the table name if configured
if (($prefix = $this->options['table_prefix']) && strpos($table, $prefix) !== 0) {
$table = $prefix . $table;
}
if ($quoted) {
$table = $this->quote_identifier($table);
}
return $table;
}
/**
* Set class option value
*
* @param string $name Option name
* @param mixed $value Option value
*/
public function set_option($name, $value)
{
$this->options[$name] = $value;
}
/**
* Set DSN connection to be used for the given table
*
* @param string Table name
* @param string DSN connection ('r' or 'w') to be used
*/
public function set_table_dsn($table, $mode)
{
$this->options['table_dsn_map'][$this->table_name($table)] = $mode;
}
/**
* 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;
}
/**
* Execute the given SQL script
*
* @param string SQL queries to execute
*
* @return boolen True on success, False on error
*/
public function exec_script($sql)
{
$sql = $this->fix_table_names($sql);
$buff = '';
foreach (explode("\n", $sql) as $line) {
if (preg_match('/^--/', $line) || trim($line) == '')
continue;
$buff .= $line . "\n";
if (preg_match('/(;|^GO)$/', trim($line))) {
$this->query($buff);
$buff = '';
if ($this->db_error) {
break;
}
}
}
return !$this->db_error;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
$sql = preg_replace_callback(
'/((TABLE|TRUNCATE|(?<!ON )UPDATE|INSERT INTO|FROM'
. '| ON(?! (DELETE|UPDATE))|REFERENCES|CONSTRAINT|FOREIGN KEY|INDEX)'
. '\s+(IF (NOT )?EXISTS )?[`"]*)([^`"\( \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
return $sql;
}
/**
* Preg_replace callback for fix_table_names()
*/
protected function fix_table_names_callback($matches)
{
return $matches[1] . $this->options['table_prefix'] . $matches[count($matches)-1];
}
}
diff --git a/lib/ext/Roundcube/rcube_db_oracle.php b/lib/ext/Roundcube/rcube_db_oracle.php
index 338eb2e..362beb0 100644
--- a/lib/ext/Roundcube/rcube_db_oracle.php
+++ b/lib/ext/Roundcube/rcube_db_oracle.php
@@ -1,599 +1,599 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011-2014, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements database functions |
| for Oracle database using OCI8 extension |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
*
* @package Framework
* @subpackage Database
*/
class rcube_db_oracle extends rcube_db
{
public $db_provider = 'oracle';
/**
* Create connection instance
*/
protected function conn_create($dsn)
{
// Get database specific connection options
$dsn_options = $this->dsn_options($dsn);
$function = $this->db_pconn ? 'oci_pconnect' : 'oci_connect';
if (!function_exists($function)) {
$this->db_error = true;
$this->db_error_msg = 'OCI8 extension not loaded. See http://php.net/manual/en/book.oci8.php';
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
return;
}
// connect
$dbh = @$function($dsn['username'], $dsn['password'], $dsn_options['database'], $dsn_options['charset']);
if (!$dbh) {
$error = oci_error();
$this->db_error = true;
$this->db_error_msg = $error['message'];
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
return;
}
// configure session
$this->conn_configure($dsn, $dbh);
return $dbh;
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
$init_queries = array(
"ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'",
"ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'",
);
foreach ($init_queries as $query) {
$stmt = oci_parse($dbh, $query);
oci_execute($stmt);
}
}
/**
* Connection state checker
*
* @return boolean True if in connected state
*/
public function is_connected()
{
return empty($this->dbh) ? false : $this->db_connected;
}
/**
* 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 PDOStatement|bool Query handle or False on error
*/
protected function _query($query, $offset, $numrows, $params)
{
$query = ltrim($query);
$this->db_connect($this->dsn_select($query), true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
if ($numrows || $offset) {
$query = $this->set_limit($query, $numrows, $offset);
}
// replace self::DEFAULT_QUOTE with driver-specific quoting
$query = $this->query_parse($query);
// Because in Roundcube we mostly use queries that are
// executed only once, we will not use prepared queries
$pos = 0;
$idx = 0;
$args = array();
if (count($params)) {
while ($pos = strpos($query, '?', $pos)) {
if ($query[$pos+1] == '?') { // skip escaped '?'
$pos += 2;
}
else {
$val = $this->quote($params[$idx++]);
// long strings are not allowed inline, need to be parametrized
if (strlen($val) > 4000) {
$key = ':param' . (count($args) + 1);
$args[$key] = $params[$idx-1];
$val = $key;
}
unset($params[$idx-1]);
$query = substr_replace($query, $val, $pos, 1);
$pos += strlen($val);
}
}
}
// replace escaped '?' back to normal, see self::quote()
$query = str_replace('??', '?', $query);
$query = rtrim($query, " \t\n\r\0\x0B;");
// log query
$this->debug($query);
// destroy reference to previous result
$this->last_result = null;
$this->db_error_msg = null;
// prepare query
$result = @oci_parse($this->dbh, $query);
$mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
if ($result) {
foreach ($args as $param => $arg) {
oci_bind_by_name($result, $param, $args[$param], -1, SQLT_LNG);
}
}
// execute query
if (!$result || !@oci_execute($result, $mode)) {
$result = $this->handle_error($query, $result);
}
return $this->last_result = $result;
}
/**
* Helper method to handle DB errors.
* This by default logs the error but could be overriden by a driver implementation
*
* @param string Query that triggered the error
* @return mixed Result to be stored and returned
*/
protected function handle_error($query, $result = null)
{
$error = oci_error(is_resource($result) ? $result : $this->dbh);
// @TODO: Find error codes for key errors
if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], array('23000', '23505'))) {
$this->db_error = true;
$this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']);
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg . " (SQL Query: $query)"
), true, false);
}
return false;
}
/**
* Get 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' || empty($table)) {
return false;
}
$sequence = $this->quote_identifier($this->sequence_name($table));
$result = $this->query("SELECT $sequence.currval FROM dual");
$result = $this->fetch_array($result);
return $result[0] ?: false;
}
/**
* Get number of affected rows for the last query
*
* @param mixed $result Optional query handle
*
* @return int Number of (matching) rows
*/
public function affected_rows($result = null)
{
if ($result || ($result === null && ($result = $this->last_result))) {
return oci_num_rows($result);
}
return 0;
}
/**
* Get number of rows for a SQL query
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
* @return mixed Number of rows or false on failure
* @deprecated This method shows very poor performance and should be avoided.
*/
public function num_rows($result = null)
{
// not implemented
return false;
}
/**
* Get an associative array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_assoc($result = null)
{
return $this->_fetch_row($result, OCI_ASSOC);
}
/**
* Get an index array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_array($result = null)
{
return $this->_fetch_row($result, OCI_NUM);
}
/**
* Get col values for a result row
*
* @param mixed $result Optional query handle
* @param int $mode Fetch mode identifier
*
* @return mixed Array with col values or false on failure
*/
protected function _fetch_row($result, $mode)
{
if ($result || ($result === null && ($result = $this->last_result))) {
return oci_fetch_array($result, $mode + OCI_RETURN_NULLS + OCI_RETURN_LOBS);
}
return false;
}
/**
* Formats input so it can be safely used in a query
* PDO_OCI does not implement quote() method
*
* @param mixed $input Value to quote
* @param string $type Type of data (integer, bool, ident)
*
* @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);
}
if (is_null($input)) {
return 'NULL';
}
if ($type == 'ident') {
return $this->quote_identifier($input);
}
switch ($type) {
case 'bool':
case 'integer':
return intval($input);
default:
return "'" . strtr($input, array(
'?' => '??',
"'" => "''",
rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE
)) . "'";
}
}
/**
* Return correct name for a specific database sequence
*
* @param string $table Table name
*
* @return string Translated sequence name
*/
protected function sequence_name($table)
{
// Note: we support only one sequence per table
// Note: The sequence name must be <table_name>_seq
$sequence = $table . '_seq';
// modify sequence name if prefix is configured
if ($prefix = $this->options['table_prefix']) {
return $prefix . $sequence;
}
return $sequence;
}
/**
* 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 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')';
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$interval = intval($interval);
return "current_timestamp + INTERVAL '$interval' SECOND";
}
return "current_timestamp";
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)";
}
/**
* 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);
$end = $offset + $limit;
// @TODO: Oracle 12g has better OFFSET support
if (!$offset) {
$query = "SELECT * FROM ($query) a WHERE rownum <= $end";
}
else {
$query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset";
}
return $query;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
$sql = parent::fix_table_names($sql);
// replace sequence names, and other Oracle-specific commands
$sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
$sql = preg_replace_callback(
'/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/',
array($this, 'fix_table_names_seq_callback'),
$sql
);
return $sql;
}
/**
* Preg_replace callback for fix_table_names()
*/
protected function fix_table_names_seq_callback($matches)
{
return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3];
}
/**
* Returns connection options from DSN array
*/
protected function dsn_options($dsn)
{
$params = array();
if ($dsn['hostspec']) {
$host = $dsn['hostspec'];
if ($dsn['port']) {
$host .= ':' . $dsn['port'];
}
$params['database'] = $host . '/' . $dsn['database'];
}
$params['charset'] = 'UTF8';
return $params;
}
/**
* Execute the given SQL script
*
* @param string SQL queries to execute
*
* @return boolen True on success, False on error
*/
public function exec_script($sql)
{
$sql = $this->fix_table_names($sql);
$buff = '';
$body = false;
foreach (explode("\n", $sql) as $line) {
$tok = strtolower(trim($line));
- if (preg_match('/^--/', $line) || $tok == '') {
+ if (preg_match('/^--/', $line) || $tok == '' || $tok == '/') {
continue;
}
$buff .= $line . "\n";
// detect PL/SQL function bodies, don't break on semicolon
if ($body && $tok == 'end;') {
$body = false;
}
else if (!$body && $tok == 'begin') {
$body = true;
}
if (!$body && substr($tok, -1) == ';') {
$this->query($buff);
$buff = '';
if ($this->db_error) {
break;
}
}
}
return !$this->db_error;
}
/**
* Start transaction
*
* @return bool True on success, False on failure
*/
public function startTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('BEGIN TRANSACTION');
return $this->last_result = $this->in_transaction = true;
}
/**
* Commit transaction
*
* @return bool True on success, False on failure
*/
public function endTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('COMMIT TRANSACTION');
if ($result = @oci_commit($this->dbh)) {
$this->in_transaction = true;
}
else {
$this->handle_error('COMMIT');
}
return $this->last_result = $result;
}
/**
* Rollback transaction
*
* @return bool True on success, False on failure
*/
public function rollbackTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('ROLLBACK TRANSACTION');
if ($result = @oci_rollback($this->dbh)) {
$this->in_transaction = false;
}
else {
$this->handle_error('ROLLBACK');
}
return $this->last_result = $this->dbh->rollBack();
}
}
diff --git a/lib/ext/Roundcube/rcube_html2text.php b/lib/ext/Roundcube/rcube_html2text.php
index 8628371..499c4b0 100644
--- a/lib/ext/Roundcube/rcube_html2text.php
+++ b/lib/ext/Roundcube/rcube_html2text.php
@@ -1,711 +1,711 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| Copyright (c) 2005-2007, Jon Abernathy <jon@chuggnutt.com> |
| |
| 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: |
| Converts HTML to formatted plain text (based on html2text class) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Jon Abernathy <jon@chuggnutt.com> |
+-----------------------------------------------------------------------+
*/
/**
* Takes HTML and converts it to formatted, plain text.
*
* Thanks to Alexander Krug (http://www.krugar.de/) to pointing out and
* correcting an error in the regexp search array. Fixed 7/30/03.
*
* Updated set_html() function's file reading mechanism, 9/25/03.
*
* Thanks to Joss Sanglier (http://www.dancingbear.co.uk/) for adding
* several more HTML entity codes to the $search and $replace arrays.
* Updated 11/7/03.
*
* Thanks to Darius Kasperavicius (http://www.dar.dar.lt/) for
* suggesting the addition of $allowed_tags and its supporting function
* (which I slightly modified). Updated 3/12/04.
*
* Thanks to Justin Dearing for pointing out that a replacement for the
* <TH> tag was missing, and suggesting an appropriate fix.
* Updated 8/25/04.
*
* Thanks to Mathieu Collas (http://www.myefarm.com/) for finding a
* display/formatting bug in the _build_link_list() function: email
* readers would show the left bracket and number ("[1") as part of the
* rendered email address.
* Updated 12/16/04.
*
* Thanks to Wojciech Bajon (http://histeria.pl/) for submitting code
* to handle relative links, which I hadn't considered. I modified his
* code a bit to handle normal HTTP links and MAILTO links. Also for
* suggesting three additional HTML entity codes to search for.
* Updated 03/02/05.
*
* Thanks to Jacob Chandler for pointing out another link condition
* for the _build_link_list() function: "https".
* Updated 04/06/05.
*
* Thanks to Marc Bertrand (http://www.dresdensky.com/) for
* suggesting a revision to the word wrapping functionality; if you
* specify a $width of 0 or less, word wrapping will be ignored.
* Updated 11/02/06.
*
* *** Big housecleaning updates below:
*
* Thanks to Colin Brown (http://www.sparkdriver.co.uk/) for
* suggesting the fix to handle </li> and blank lines (whitespace).
* Christian Basedau (http://www.movetheweb.de/) also suggested the
* blank lines fix.
*
* Special thanks to Marcus Bointon (http://www.synchromedia.co.uk/),
* Christian Basedau, Norbert Laposa (http://ln5.co.uk/),
* Bas van de Weijer, and Marijn van Butselaar
* for pointing out my glaring error in the <th> handling. Marcus also
* supplied a host of fixes.
*
* Thanks to Jeffrey Silverman (http://www.newtnotes.com/) for pointing
* out that extra spaces should be compressed--a problem addressed with
* Marcus Bointon's fixes but that I had not yet incorporated.
*
* Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for
* suggesting a valuable fix with <a> tag handling.
*
* Thanks to Wojciech Bajon (again!) for suggesting fixes and additions,
* including the <a> tag handling that Daniel Schledermann pointed
* out but that I had not yet incorporated. I haven't (yet)
* incorporated all of Wojciech's changes, though I may at some
* future time.
*
* *** End of the housecleaning updates. Updated 08/08/07.
*/
/**
* Converts HTML to formatted plain text
*
* @package Framework
* @subpackage Utils
*/
class rcube_html2text
{
/**
* Contains the HTML content to convert.
*
* @var string $html
*/
protected $html;
/**
* Contains the converted, formatted text.
*
* @var string $text
*/
protected $text;
/**
* Maximum width of the formatted text, in columns.
*
* Set this value to 0 (or less) to ignore word wrapping
* and not constrain text to a fixed-width column.
*
* @var integer $width
*/
protected $width = 70;
/**
* Target character encoding for output text
*
* @var string $charset
*/
protected $charset = 'UTF-8';
/**
* List of preg* regular expression patterns to search for,
* used in conjunction with $replace.
*
* @var array $search
* @see $replace
*/
protected $search = array(
"/\r/", // Non-legal carriage return
"/[\n\t]+/", // Newlines and tabs
'/<head[^>]*>.*?<\/head>/i', // <head>
'/<script[^>]*>.*?<\/script>/i', // <script>s -- which strip_tags supposedly has problems with
'/<style[^>]*>.*?<\/style>/i', // <style>s -- which strip_tags supposedly has problems with
'/<p[^>]*>/i', // <P>
'/<br[^>]*>/i', // <br>
'/<i[^>]*>(.*?)<\/i>/i', // <i>
'/<em[^>]*>(.*?)<\/em>/i', // <em>
'/(<ul[^>]*>|<\/ul>)/i', // <ul> and </ul>
'/(<ol[^>]*>|<\/ol>)/i', // <ol> and </ol>
'/<li[^>]*>(.*?)<\/li>/i', // <li> and </li>
'/<li[^>]*>/i', // <li>
'/<hr[^>]*>/i', // <hr>
'/<div[^>]*>/i', // <div>
'/(<table[^>]*>|<\/table>)/i', // <table> and </table>
'/(<tr[^>]*>|<\/tr>)/i', // <tr> and </tr>
'/<td[^>]*>(.*?)<\/td>/i', // <td> and </td>
);
/**
* List of pattern replacements corresponding to patterns searched.
*
* @var array $replace
* @see $search
*/
protected $replace = array(
'', // Non-legal carriage return
' ', // Newlines and tabs
'', // <head>
'', // <script>s -- which strip_tags supposedly has problems with
'', // <style>s -- which strip_tags supposedly has problems with
"\n\n", // <P>
"\n", // <br>
'_\\1_', // <i>
'_\\1_', // <em>
"\n\n", // <ul> and </ul>
"\n\n", // <ol> and </ol>
"\t* \\1\n", // <li> and </li>
"\n\t* ", // <li>
"\n-------------------------\n", // <hr>
"<div>\n", // <div>
"\n\n", // <table> and </table>
"\n", // <tr> and </tr>
"\t\t\\1\n", // <td> and </td>
);
/**
* List of preg* regular expression patterns to search for,
* used in conjunction with $ent_replace.
*
* @var array $ent_search
* @see $ent_replace
*/
protected $ent_search = array(
'/&(nbsp|#160);/i', // Non-breaking space
'/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i',
// Double quotes
'/&(apos|rsquo|lsquo|#8216|#8217);/i', // Single quotes
'/&gt;/i', // Greater-than
'/&lt;/i', // Less-than
'/&(copy|#169);/i', // Copyright
'/&(trade|#8482|#153);/i', // Trademark
'/&(reg|#174);/i', // Registered
'/&(mdash|#151|#8212);/i', // mdash
'/&(ndash|minus|#8211|#8722);/i', // ndash
'/&(bull|#149|#8226);/i', // Bullet
'/&(pound|#163);/i', // Pound sign
'/&(euro|#8364);/i', // Euro sign
'/&(amp|#38);/i', // Ampersand: see _converter()
'/[ ]{2,}/', // Runs of spaces, post-handling
);
/**
* List of pattern replacements corresponding to patterns searched.
*
* @var array $ent_replace
* @see $ent_search
*/
protected $ent_replace = array(
' ', // Non-breaking space
'"', // Double quotes
"'", // Single quotes
'>',
'<',
'(c)',
'(tm)',
'(R)',
'--',
'-',
'*',
'£',
'EUR', // Euro sign. € ?
'|+|amp|+|', // Ampersand: see _converter()
' ', // Runs of spaces, post-handling
);
/**
* List of preg* regular expression patterns to search for
* and replace using callback function.
*
* @var array $callback_search
*/
protected $callback_search = array(
'/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i', // <a href="">
'/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i', // h1 - h6
'/<(b)( [^>]*)?>(.*?)<\/b>/i', // <b>
'/<(strong)( [^>]*)?>(.*?)<\/strong>/i', // <strong>
'/<(th)( [^>]*)?>(.*?)<\/th>/i', // <th> and </th>
);
/**
* List of preg* regular expression patterns to search for in PRE body,
* used in conjunction with $pre_replace.
*
* @var array $pre_search
* @see $pre_replace
*/
protected $pre_search = array(
"/\n/",
"/\t/",
'/ /',
'/<pre[^>]*>/',
'/<\/pre>/'
);
/**
* List of pattern replacements corresponding to patterns searched for PRE body.
*
* @var array $pre_replace
* @see $pre_search
*/
protected $pre_replace = array(
'<br>',
'&nbsp;&nbsp;&nbsp;&nbsp;',
'&nbsp;',
'',
''
);
/**
* Contains a list of HTML tags to allow in the resulting text.
*
* @var string $allowed_tags
* @see set_allowed_tags()
*/
protected $allowed_tags = '';
/**
* Contains the base URL that relative links should resolve to.
*
* @var string $url
*/
protected $url;
/**
* Indicates whether content in the $html variable has been converted yet.
*
* @var boolean $_converted
* @see $html, $text
*/
protected $_converted = false;
/**
* Contains URL addresses from links to be rendered in plain text.
*
* @var array $_link_list
* @see _build_link_list()
*/
protected $_link_list = array();
/**
* Boolean flag, true if a table of link URLs should be listed after the text.
*
* @var boolean $_do_links
* @see __construct()
*/
protected $_do_links = true;
/**
* Constructor.
*
* If the HTML source string (or file) is supplied, the class
* will instantiate with that source propagated, all that has
* to be done it to call get_text().
*
* @param string $source HTML content
* @param boolean $from_file Indicates $source is a file to pull content from
* @param boolean $do_links Indicate whether a table of link URLs is desired
* @param integer $width Maximum width of the formatted text, 0 for no limit
*/
function __construct($source = '', $from_file = false, $do_links = true, $width = 75, $charset = 'UTF-8')
{
if (!empty($source)) {
$this->set_html($source, $from_file);
}
$this->set_base_url();
$this->_do_links = $do_links;
$this->width = $width;
$this->charset = $charset;
}
/**
* Loads source HTML into memory, either from $source string or a file.
*
* @param string $source HTML content
* @param boolean $from_file Indicates $source is a file to pull content from
*/
function set_html($source, $from_file = false)
{
if ($from_file && file_exists($source)) {
$this->html = file_get_contents($source);
}
else {
$this->html = $source;
}
$this->_converted = false;
}
/**
* Returns the text, converted from HTML.
*
* @return string Plain text
*/
function get_text()
{
if (!$this->_converted) {
$this->_convert();
}
return $this->text;
}
/**
* Prints the text, converted from HTML.
*/
function print_text()
{
print $this->get_text();
}
/**
* Sets the allowed HTML tags to pass through to the resulting text.
*
* Tags should be in the form "<p>", with no corresponding closing tag.
*/
function set_allowed_tags($allowed_tags = '')
{
if (!empty($allowed_tags)) {
$this->allowed_tags = $allowed_tags;
}
}
/**
* Sets a base URL to handle relative links.
*/
function set_base_url($url = '')
{
if (empty($url)) {
if (!empty($_SERVER['HTTP_HOST'])) {
$this->url = 'http://' . $_SERVER['HTTP_HOST'];
}
else {
$this->url = '';
}
}
else {
// Strip any trailing slashes for consistency (relative
// URLs may already start with a slash like "/file.html")
if (substr($url, -1) == '/') {
$url = substr($url, 0, -1);
}
$this->url = $url;
}
}
/**
* Workhorse function that does actual conversion (calls _converter() method).
*/
protected function _convert()
{
// Variables used for building the link list
$this->_link_list = array();
- $text = trim(stripslashes($this->html));
+ $text = $this->html;
// Convert HTML to TXT
$this->_converter($text);
// Add link list
if (!empty($this->_link_list)) {
$text .= "\n\nLinks:\n------\n";
foreach ($this->_link_list as $idx => $url) {
$text .= '[' . ($idx+1) . '] ' . $url . "\n";
}
}
$this->text = $text;
$this->_converted = true;
}
/**
* Workhorse function that does actual conversion.
*
* First performs custom tag replacement specified by $search and
* $replace arrays. Then strips any remaining HTML tags, reduces whitespace
* and newlines to a readable format, and word wraps the text to
* $width characters.
*
* @param string Reference to HTML content string
*/
protected function _converter(&$text)
{
// Convert <BLOCKQUOTE> (before PRE!)
$this->_convert_blockquotes($text);
// Convert <PRE>
$this->_convert_pre($text);
// Run our defined tags search-and-replace
$text = preg_replace($this->search, $this->replace, $text);
// Run our defined tags search-and-replace with callback
$text = preg_replace_callback($this->callback_search, array($this, 'tags_preg_callback'), $text);
// Strip any other HTML tags
$text = strip_tags($text, $this->allowed_tags);
// Run our defined entities/characters search-and-replace
$text = preg_replace($this->ent_search, $this->ent_replace, $text);
// Replace known html entities
$text = html_entity_decode($text, ENT_QUOTES, $this->charset);
// Replace unicode nbsp to regular spaces
$text = preg_replace('/\xC2\xA0/', ' ', $text);
// Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
$text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
// Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities
// This properly handles situation of "&amp;quot;" in input string
$text = str_replace('|+|amp|+|', '&', $text);
// Bring down number of empty lines to 2 max
$text = preg_replace("/\n\s+\n/", "\n\n", $text);
$text = preg_replace("/[\n]{3,}/", "\n\n", $text);
// remove leading empty lines (can be produced by eg. P tag on the beginning)
$text = ltrim($text, "\n");
// Wrap the text to a readable format
// for PHP versions >= 4.0.2. Default width is 75
// If width is 0 or less, don't wrap the text.
if ( $this->width > 0 ) {
$text = wordwrap($text, $this->width);
}
}
/**
* Helper function called by preg_replace() on link replacement.
*
* Maintains an internal list of links to be displayed at the end of the
* text, with numeric indices to the original point in the text they
* appeared. Also makes an effort at identifying and handling absolute
* and relative links.
*
* @param string $link URL of the link
* @param string $display Part of the text to associate number with
*/
protected function _build_link_list( $link, $display )
{
if (!$this->_do_links || empty($link)) {
return $display;
}
// Ignored link types
if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
return $display;
}
if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) {
$url = $link;
}
else {
$url = $this->url;
if (substr($link, 0, 1) != '/') {
$url .= '/';
}
$url .= "$link";
}
if (($index = array_search($url, $this->_link_list)) === false) {
$index = count($this->_link_list);
$this->_link_list[] = $url;
}
return $display . ' [' . ($index+1) . ']';
}
/**
* Helper function for PRE body conversion.
*
* @param string HTML content
*/
protected function _convert_pre(&$text)
{
// get the content of PRE element
while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
$this->pre_content = $matches[1];
// Run our defined tags search-and-replace with callback
$this->pre_content = preg_replace_callback($this->callback_search,
array($this, 'tags_preg_callback'), $this->pre_content);
// convert the content
$this->pre_content = sprintf('<div><br>%s<br></div>',
preg_replace($this->pre_search, $this->pre_replace, $this->pre_content));
// replace the content (use callback because content can contain $0 variable)
$text = preg_replace_callback('/<pre[^>]*>.*<\/pre>/ismU',
array($this, 'pre_preg_callback'), $text, 1);
// free memory
$this->pre_content = '';
}
}
/**
* Helper function for BLOCKQUOTE body conversion.
*
* @param string HTML content
*/
protected function _convert_blockquotes(&$text)
{
$level = 0;
$offset = 0;
while (($start = strpos($text, '<blockquote', $offset)) !== false) {
$offset = $start + 12;
do {
$end = strpos($text, '</blockquote>', $offset);
$next = strpos($text, '<blockquote', $offset);
// nested <blockquote>, skip
if ($next !== false && $next < $end) {
$offset = $next + 12;
$level++;
}
// nested </blockquote> tag
if ($end !== false && $level > 0) {
$offset = $end + 12;
$level--;
}
// found matching end tag
else if ($end !== false && $level == 0) {
$taglen = strpos($text, '>', $start) - $start;
$startpos = $start + $taglen + 1;
// get blockquote content
$body = trim(substr($text, $startpos, $end - $startpos));
// adjust text wrapping width
$p_width = $this->width;
if ($this->width > 0) $this->width -= 2;
// replace content with inner blockquotes
$this->_converter($body);
// resore text width
$this->width = $p_width;
// Add citation markers and create <pre> block
$body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body));
$body = '<pre>' . htmlspecialchars($body) . '</pre>';
$text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
$offset = 0;
break;
}
// abort on invalid tag structure (e.g. no closing tag found)
else {
break;
}
}
while ($end || $next);
}
}
/**
* Callback function to correctly add citation markers for blockquote contents
*/
public function blockquote_citation_callback($m)
{
$line = ltrim($m[2]);
$space = $line[0] == '>' ? '' : ' ';
return $m[1] . '>' . $space . $line;
}
/**
* Callback function for preg_replace_callback use.
*
* @param array PREG matches
* @return string
*/
public function tags_preg_callback($matches)
{
switch (strtolower($matches[1])) {
case 'b':
case 'strong':
return $this->_toupper($matches[3]);
case 'th':
return $this->_toupper("\t\t". $matches[3] ."\n");
case 'h':
return $this->_toupper("\n\n". $matches[3] ."\n\n");
case 'a':
// Remove spaces in URL (#1487805)
$url = str_replace(' ', '', $matches[3]);
return $this->_build_link_list($url, $matches[4]);
}
}
/**
* Callback function for preg_replace_callback use in PRE content handler.
*
* @param array PREG matches
* @return string
*/
public function pre_preg_callback($matches)
{
return $this->pre_content;
}
/**
* Strtoupper function with HTML tags and entities handling.
*
* @param string $str Text to convert
* @return string Converted text
*/
private function _toupper($str)
{
// string can containg HTML tags
$chunks = preg_split('/(<[^>]*>)/', $str, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
// convert toupper only the text between HTML tags
foreach ($chunks as $idx => $chunk) {
if ($chunk[0] != '<') {
$chunks[$idx] = $this->_strtoupper($chunk);
}
}
return implode($chunks);
}
/**
* Strtoupper multibyte wrapper function with HTML entities handling.
*
* @param string $str Text to convert
* @return string Converted text
*/
private function _strtoupper($str)
{
$str = html_entity_decode($str, ENT_COMPAT, $this->charset);
$str = mb_strtoupper($str);
$str = htmlspecialchars($str, ENT_COMPAT, $this->charset);
return $str;
}
}
diff --git a/lib/ext/Roundcube/rcube_image.php b/lib/ext/Roundcube/rcube_image.php
index 122b5f4..9c9f08b 100644
--- a/lib/ext/Roundcube/rcube_image.php
+++ b/lib/ext/Roundcube/rcube_image.php
@@ -1,339 +1,443 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-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: |
| Image resizer and converter |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Image resizer and converter
*
* @package Framework
* @subpackage Utils
*/
class rcube_image
{
private $image_file;
const TYPE_GIF = 1;
const TYPE_JPG = 2;
const TYPE_PNG = 3;
const TYPE_TIF = 4;
public static $extensions = array(
self::TYPE_GIF => 'gif',
self::TYPE_JPG => 'jpg',
self::TYPE_PNG => 'png',
self::TYPE_TIF => 'tif',
);
function __construct($filename)
{
$this->image_file = $filename;
}
/**
* Get image properties.
*
* @return mixed Hash array with image props like type, width, height
*/
public function props()
{
// use GD extension
if (function_exists('getimagesize') && ($imsize = @getimagesize($this->image_file))) {
$width = $imsize[0];
$height = $imsize[1];
$gd_type = $imsize['2'];
$type = image_type_to_extension($imsize['2'], false);
+ $channels = $imsize['channels'];
}
// use ImageMagick
if (!$type && ($data = $this->identify())) {
list($type, $width, $height) = $data;
+ $channels = null;
}
if ($type) {
return array(
'type' => $type,
'gd_type' => $gd_type,
'width' => $width,
'height' => $height,
+ 'channels' => $channels,
);
}
+
+ return null;
}
/**
* Resize image to a given size. Use only to shrink an image.
* If an image is smaller than specified size it will be not resized.
*
* @param int $size Max width/height size
* @param string $filename Output filename
* @param boolean $browser_compat Convert to image type displayable by any browser
*
* @return mixed Output type on success, False on failure
*/
public function resize($size, $filename = null, $browser_compat = false)
{
$result = false;
$rcube = rcube::get_instance();
$convert = $rcube->config->get('im_convert_path', false);
$props = $this->props();
if (empty($props)) {
return false;
}
if (!$filename) {
$filename = $this->image_file;
}
// use Imagemagick
- if ($convert) {
- $p['out'] = $filename;
- $p['in'] = $this->image_file;
- $type = $props['type'];
+ if ($convert || class_exists('Imagick', false)) {
+ $p['out'] = $filename;
+ $p['in'] = $this->image_file;
+ $type = $props['type'];
if (!$type && ($data = $this->identify())) {
$type = $data[0];
}
$type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
$p['intype'] = $type;
// convert to an image format every browser can display
if ($browser_compat && !in_array($type, array('jpg','gif','png'))) {
$type = 'jpg';
}
// If only one dimension is greater than the limit convert doesn't
// work as expected, we need to calculate new dimensions
$scale = $size / max($props['width'], $props['height']);
// if file is smaller than the limit, we do nothing
// but copy original file to destination file
if ($scale >= 1 && $p['intype'] == $type) {
$result = ($this->image_file == $filename || copy($this->image_file, $filename)) ? '' : false;
}
else {
- if ($scale >= 1) {
- $width = $props['width'];
- $height = $props['height'];
- }
- else {
- $width = intval($props['width'] * $scale);
- $height = intval($props['height'] * $scale);
- }
-
$valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif";
- $p += array(
- 'type' => $type,
- 'quality' => 75,
- 'size' => $width . 'x' . $height,
- );
-
if (in_array($type, explode(',', $valid_types))) { // Valid type?
- $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
- . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
+ if ($scale >= 1) {
+ $width = $props['width'];
+ $height = $props['height'];
+ }
+ else {
+ $width = intval($props['width'] * $scale);
+ $height = intval($props['height'] * $scale);
+ }
+
+ // use ImageMagick in command line
+ if ($convert) {
+ $p += array(
+ 'type' => $type,
+ 'quality' => 75,
+ 'size' => $width . 'x' . $height,
+ );
+
+ $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
+ . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
+ }
+ // use PHP's Imagick class
+ else {
+ try {
+ $image = new Imagick($this->image_file);
+ $image = $image->flattenImages();
+
+ $image->setImageColorspace(Imagick::COLORSPACE_SRGB);
+ $image->setImageCompressionQuality(75);
+ $image->setImageFormat($type);
+ $image->stripImage();
+ $image->scaleImage($width, $height);
+
+ if ($image->writeImage($filename)) {
+ $result = '';
+ }
+ }
+ catch (Exception $e) {
+ rcube::raise_error($e, true, false);
+ }
+ }
}
}
if ($result === '') {
@chmod($filename, 0600);
return $type;
}
}
+ // do we have enough memory? (#1489937)
+ if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
+ return false;
+ }
+
// use GD extension
if ($props['gd_type']) {
if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
$image = imagecreatefromjpeg($this->image_file);
$type = 'jpg';
}
else if($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
$image = imagecreatefromgif($this->image_file);
$type = 'gif';
}
else if($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
$image = imagecreatefrompng($this->image_file);
$type = 'png';
}
else {
// @TODO: print error to the log?
return false;
}
if ($image === false) {
return false;
}
$scale = $size / max($props['width'], $props['height']);
// Imagemagick resize is implemented in shrinking mode (see -resize argument above)
// we do the same here, if an image is smaller than specified size
// we do nothing but copy original file to destination file
if ($scale >= 1) {
$result = $this->image_file == $filename || copy($this->image_file, $filename);
}
else {
$width = intval($props['width'] * $scale);
$height = intval($props['height'] * $scale);
$new_image = imagecreatetruecolor($width, $height);
// Fix transparency of gif/png image
if ($props['gd_type'] != IMAGETYPE_JPEG) {
imagealphablending($new_image, false);
imagesavealpha($new_image, true);
$transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
}
imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
$image = $new_image;
// fix rotation of image if EXIF data exists and specifies rotation (GD strips the EXIF data)
- if ($this->image_file && function_exists('exif_read_data')) {
+ if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
$exif = exif_read_data($this->image_file);
if ($exif && $exif['Orientation']) {
switch ($exif['Orientation']) {
case 3:
$image = imagerotate($image, 180, 0);
break;
case 6:
$image = imagerotate($image, -90, 0);
break;
case 8:
$image = imagerotate($image, 90, 0);
break;
}
}
}
if ($props['gd_type'] == IMAGETYPE_JPEG) {
$result = imagejpeg($image, $filename, 75);
}
elseif($props['gd_type'] == IMAGETYPE_GIF) {
$result = imagegif($image, $filename);
}
elseif($props['gd_type'] == IMAGETYPE_PNG) {
$result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
}
}
if ($result) {
@chmod($filename, 0600);
return $type;
}
}
// @TODO: print error to the log?
return false;
}
/**
* Convert image to a given type
*
* @param int $type Destination file type (see class constants)
* @param string $filename Output filename (if empty, original file will be used
* and filename extension will be modified)
*
* @return bool True on success, False on failure
*/
public function convert($type, $filename = null)
{
$rcube = rcube::get_instance();
$convert = $rcube->config->get('im_convert_path', false);
if (!$filename) {
$filename = $this->image_file;
// modify extension
if ($extension = self::$extensions[$type]) {
$filename = preg_replace('/\.[^.]+$/', '', $filename) . '.' . $extension;
}
}
- // use ImageMagick
+ // use ImageMagick in command line
if ($convert) {
$p['in'] = $this->image_file;
$p['out'] = $filename;
$p['type'] = self::$extensions[$type];
$result = rcube::exec($convert . ' 2>&1 -colorspace sRGB -strip -quality 75 {in} {type}:{out}', $p);
if ($result === '') {
- @chmod($filename, 0600);
+ chmod($filename, 0600);
return true;
}
}
+ // use PHP's Imagick class
+ if (class_exists('Imagick', false)) {
+ try {
+ $image = new Imagick($this->image_file);
+
+ $image->setImageColorspace(Imagick::COLORSPACE_SRGB);
+ $image->setImageCompressionQuality(75);
+ $image->setImageFormat(self::$extensions[$type]);
+ $image->stripImage();
+
+ if ($image->writeImage($filename)) {
+ @chmod($filename, 0600);
+ return true;
+ }
+ }
+ catch (Exception $e) {
+ rcube::raise_error($e, true, false);
+ }
+ }
+
// use GD extension (TIFF isn't supported)
$props = $this->props();
+ // do we have enough memory? (#1489937)
+ if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
+ return false;
+ }
+
+
if ($props['gd_type']) {
if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
$image = imagecreatefromjpeg($this->image_file);
}
else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
$image = imagecreatefromgif($this->image_file);
}
else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
$image = imagecreatefrompng($this->image_file);
}
else {
// @TODO: print error to the log?
return false;
}
if ($type == self::TYPE_JPG) {
$result = imagejpeg($image, $filename, 75);
}
else if ($type == self::TYPE_GIF) {
$result = imagegif($image, $filename);
}
else if ($type == self::TYPE_PNG) {
$result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
}
if ($result) {
@chmod($filename, 0600);
return true;
}
}
// @TODO: print error to the log?
return false;
}
/**
- * Identify command handler.
+ * Checks if image format conversion is supported
+ *
+ * @return boolean True if specified format can be converted to another format
+ */
+ public static function is_convertable($mimetype = null)
+ {
+ $rcube = rcube::get_instance();
+
+ // @TODO: check if specified mimetype is really supported
+ return class_exists('Imagick', false) || $rcube->config->get('im_convert_path');
+ }
+
+ /**
+ * ImageMagick based image properties read.
*/
private function identify()
{
$rcube = rcube::get_instance();
+ // use ImageMagick in command line
if ($cmd = $rcube->config->get('im_identify_path')) {
$args = array('in' => $this->image_file, 'format' => "%m %[fx:w] %[fx:h]");
$id = rcube::exec($cmd. ' 2>/dev/null -format {format} {in}', $args);
if ($id) {
return explode(' ', strtolower($id));
}
}
+
+ // use PHP's Imagick class
+ if (class_exists('Imagick', false)) {
+ try {
+ $image = new Imagick($this->image_file);
+
+ return array(
+ strtolower($image->getImageFormat()),
+ $image->getImageWidth(),
+ $image->getImageHeight(),
+ );
+ }
+ catch (Exception $e) {}
+ }
}
+ /**
+ * Check if we have enough memory to load specified image
+ */
+ private function mem_check($props)
+ {
+ // image size is unknown, we can't calculate required memory
+ if (!$props['width']) {
+ return true;
+ }
+
+ // channels: CMYK - 4, RGB - 3
+ $multip = ($props['channels'] ?: 3) + 1;
+
+ // calculate image size in memory (in bytes)
+ $size = $props['width'] * $props['height'] * $multip;
+ return rcube_utils::mem_check($size);
+ }
}
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index 18cf46d..a1fd874 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -1,4247 +1,4541 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-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: |
| IMAP Storage Engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing an IMAP server
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_imap extends rcube_storage
{
/**
* Instance of rcube_imap_generic
*
* @var rcube_imap_generic
*/
public $conn;
/**
* Instance of rcube_imap_cache
*
* @var rcube_imap_cache
*/
protected $mcache;
/**
* Instance of rcube_cache
*
* @var rcube_cache
*/
protected $cache;
/**
* Internal (in-memory) cache
*
* @var array
*/
protected $icache = array();
+ protected $plugins;
protected $list_page = 1;
protected $delimiter;
protected $namespace;
protected $sort_field = '';
protected $sort_order = 'DESC';
protected $struct_charset;
protected $uid_id_map = array();
protected $msg_headers = array();
protected $search_set;
protected $search_string = '';
protected $search_charset = '';
protected $search_sort_field = '';
protected $search_threads = false;
protected $search_sorted = false;
protected $options = array('auth_type' => 'check');
protected $caching = false;
protected $messages_caching = false;
protected $threading = false;
/**
* Object constructor.
*/
public function __construct()
{
$this->conn = new rcube_imap_generic();
+ $this->plugins = rcube::get_instance()->plugins;
// Set namespace and delimiter from session,
// so some methods would work before connection
if (isset($_SESSION['imap_namespace'])) {
$this->namespace = $_SESSION['imap_namespace'];
}
if (isset($_SESSION['imap_delimiter'])) {
$this->delimiter = $_SESSION['imap_delimiter'];
}
}
/**
* Magic getter for backward compat.
*
* @deprecated.
*/
public function __get($name)
{
if (isset($this->{$name})) {
return $this->{$name};
}
}
/**
* Connect to an IMAP server
*
- * @param string $host Host to connect
- * @param string $user Username for IMAP account
- * @param string $pass Password for IMAP account
- * @param integer $port Port to connect to
- * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
+ * @param string $host Host to connect
+ * @param string $user Username for IMAP account
+ * @param string $pass Password for IMAP account
+ * @param integer $port Port to connect to
+ * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
*
- * @return boolean TRUE on success, FALSE on failure
+ * @return boolean True on success, False on failure
*/
public function connect($host, $user, $pass, $port=143, $use_ssl=null)
{
// check for OpenSSL support in PHP build
if ($use_ssl && extension_loaded('openssl')) {
$this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
}
else if ($use_ssl) {
rcube::raise_error(array('code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "OpenSSL not available"), true, false);
$port = 143;
}
$this->options['port'] = $port;
if ($this->options['debug']) {
$this->set_debug(true);
$this->options['ident'] = array(
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
);
}
$attempt = 0;
do {
- $data = rcube::get_instance()->plugins->exec_hook('storage_connect',
+ $data = $this->plugins->exec_hook('storage_connect',
array_merge($this->options, array('host' => $host, 'user' => $user,
'attempt' => ++$attempt)));
if (!empty($data['pass'])) {
$pass = $data['pass'];
}
$this->conn->connect($data['host'], $data['user'], $pass, $data);
} while(!$this->conn->connected() && $data['retry']);
$config = array(
'host' => $data['host'],
'user' => $data['user'],
'password' => $pass,
'port' => $port,
'ssl' => $use_ssl,
);
$this->options = array_merge($this->options, $config);
$this->connect_done = true;
if ($this->conn->connected()) {
+ // check for session identifier
+ $session = null;
+ if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) {
+ $session = $m[1];
+ }
+
// get namespace and delimiter
$this->set_env();
+
+ // trigger post-connect hook
+ $this->plugins->exec_hook('storage_connected', array(
+ 'host' => $host, 'user' => $user, 'session' => $session
+ ));
+
return true;
}
// write error log
else if ($this->conn->error) {
if ($pass && $user) {
$message = sprintf("Login failed for %s from %s. %s",
$user, rcube_utils::remote_ip(), $this->conn->error);
rcube::raise_error(array('code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => $message), true, false);
}
}
return false;
}
/**
* Close IMAP connection.
* Usually done on script shutdown
*/
public function close()
{
$this->conn->closeConnection();
if ($this->mcache) {
$this->mcache->close();
}
}
/**
* Check connection state, connect if not connected.
*
* @return bool Connection state.
*/
public function check_connection()
{
// Establish connection if it wasn't done yet
if (!$this->connect_done && !empty($this->options['user'])) {
return $this->connect(
$this->options['host'],
$this->options['user'],
$this->options['password'],
$this->options['port'],
$this->options['ssl']
);
}
return $this->is_connected();
}
/**
* Checks IMAP connection.
*
* @return boolean TRUE on success, FALSE on failure
*/
public function is_connected()
{
return $this->conn->connected();
}
/**
* Returns code of last error
*
* @return int Error code
*/
public function get_error_code()
{
return $this->conn->errornum;
}
/**
* Returns text of last error
*
* @return string Error string
*/
public function get_error_str()
{
return $this->conn->error;
}
/**
* Returns code of last command response
*
* @return int Response code
*/
public function get_response_code()
{
switch ($this->conn->resultcode) {
case 'NOPERM':
return self::NOPERM;
case 'READ-ONLY':
return self::READONLY;
case 'TRYCREATE':
return self::TRYCREATE;
case 'INUSE':
return self::INUSE;
case 'OVERQUOTA':
return self::OVERQUOTA;
case 'ALREADYEXISTS':
return self::ALREADYEXISTS;
case 'NONEXISTENT':
return self::NONEXISTENT;
case 'CONTACTADMIN':
return self::CONTACTADMIN;
default:
return self::UNKNOWN;
}
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if IMAP conversation should be logged
*/
public function set_debug($dbg = true)
{
$this->options['debug'] = $dbg;
$this->conn->setDebug($dbg, array($this, 'debug_handler'));
}
/**
* Set internal folder reference.
* All operations will be perfomed on this folder.
*
* @param string $folder Folder name
*/
public function set_folder($folder)
{
$this->folder = $folder;
}
/**
* Save a search result for future message listing methods
*
* @param array $set Search set, result from rcube_imap::get_search_set():
* 0 - searching criteria, string
* 1 - search result, rcube_result_index|rcube_result_thread
* 2 - searching character set, string
* 3 - sorting field, string
* 4 - true if sorted, bool
*/
public function set_search_set($set)
{
$set = (array)$set;
$this->search_string = $set[0];
$this->search_set = $set[1];
$this->search_charset = $set[2];
$this->search_sort_field = $set[3];
$this->search_sorted = $set[4];
$this->search_threads = is_a($this->search_set, 'rcube_result_thread');
+
+ if (is_a($this->search_set, 'rcube_result_multifolder')) {
+ $this->set_threading(false);
+ }
}
/**
* Return the saved search set as hash array
*
* @return array Search set
*/
public function get_search_set()
{
if (empty($this->search_set)) {
return null;
}
return array(
$this->search_string,
$this->search_set,
$this->search_charset,
$this->search_sort_field,
$this->search_sorted,
);
}
/**
* Returns the IMAP server's capability.
*
* @param string $cap Capability name
*
* @return mixed Capability value or TRUE if supported, FALSE if not
*/
public function get_capability($cap)
{
$cap = strtoupper($cap);
$sess_key = "STORAGE_$cap";
if (!isset($_SESSION[$sess_key])) {
if (!$this->check_connection()) {
return false;
}
$_SESSION[$sess_key] = $this->conn->getCapability($cap);
}
return $_SESSION[$sess_key];
}
/**
* Checks the PERMANENTFLAGS capability of the current folder
* and returns true if the given flag is supported by the IMAP server
*
* @param string $flag Permanentflag name
*
* @return boolean True if this flag is supported
*/
public function check_permflag($flag)
{
$flag = strtoupper($flag);
$perm_flags = $this->get_permflags($this->folder);
$imap_flag = $this->conn->flags[$flag];
return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
}
/**
* Returns PERMANENTFLAGS of the specified folder
*
* @param string $folder Folder name
*
* @return array Flags
*/
public function get_permflags($folder)
{
if (!strlen($folder)) {
return array();
}
if (!$this->check_connection()) {
return array();
}
if ($this->conn->select($folder)) {
$permflags = $this->conn->data['PERMANENTFLAGS'];
}
else {
return array();
}
if (!is_array($permflags)) {
$permflags = array();
}
return $permflags;
}
/**
* Returns the delimiter that is used by the IMAP server for folder separation
*
* @return string Delimiter string
* @access public
*/
public function get_hierarchy_delimiter()
{
return $this->delimiter;
}
/**
* Get namespace
*
* @param string $name Namespace array index: personal, other, shared, prefix
*
* @return array Namespace data
*/
public function get_namespace($name = null)
{
$ns = $this->namespace;
if ($name) {
return isset($ns[$name]) ? $ns[$name] : null;
}
unset($ns['prefix']);
return $ns;
}
/**
* Sets delimiter and namespaces
*/
protected function set_env()
{
if ($this->delimiter !== null && $this->namespace !== null) {
return;
}
$config = rcube::get_instance()->config;
$imap_personal = $config->get('imap_ns_personal');
$imap_other = $config->get('imap_ns_other');
$imap_shared = $config->get('imap_ns_shared');
$imap_delimiter = $config->get('imap_delimiter');
if (!$this->check_connection()) {
return;
}
$ns = $this->conn->getNamespace();
// Set namespaces (NAMESPACE supported)
if (is_array($ns)) {
$this->namespace = $ns;
}
else {
$this->namespace = array(
'personal' => NULL,
'other' => NULL,
'shared' => NULL,
);
}
if ($imap_delimiter) {
$this->delimiter = $imap_delimiter;
}
if (empty($this->delimiter)) {
$this->delimiter = $this->namespace['personal'][0][1];
}
if (empty($this->delimiter)) {
$this->delimiter = $this->conn->getHierarchyDelimiter();
}
if (empty($this->delimiter)) {
$this->delimiter = '/';
}
// Overwrite namespaces
if ($imap_personal !== null) {
$this->namespace['personal'] = NULL;
foreach ((array)$imap_personal as $dir) {
$this->namespace['personal'][] = array($dir, $this->delimiter);
}
}
if ($imap_other !== null) {
$this->namespace['other'] = NULL;
foreach ((array)$imap_other as $dir) {
if ($dir) {
$this->namespace['other'][] = array($dir, $this->delimiter);
}
}
}
if ($imap_shared !== null) {
$this->namespace['shared'] = NULL;
foreach ((array)$imap_shared as $dir) {
if ($dir) {
$this->namespace['shared'][] = array($dir, $this->delimiter);
}
}
}
// Find personal namespace prefix for mod_folder()
// Prefix can be removed when there is only one personal namespace
if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
$this->namespace['prefix'] = $this->namespace['personal'][0][0];
}
$_SESSION['imap_namespace'] = $this->namespace;
$_SESSION['imap_delimiter'] = $this->delimiter;
}
/**
* Get message count for a specific folder
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
*/
public function count($folder='', $mode='ALL', $force=false, $status=true)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->countmessages($folder, $mode, $force, $status);
}
/**
* protected method for getting nr of messages
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
* @see rcube_imap::count()
*/
protected function countmessages($folder, $mode='ALL', $force=false, $status=true)
{
$mode = strtoupper($mode);
// count search set, assume search set is always up-to-date (don't check $force flag)
if ($this->search_string && $folder == $this->folder && ($mode == 'ALL' || $mode == 'THREADS')) {
if ($mode == 'ALL') {
return $this->search_set->count_messages();
}
else {
return $this->search_set->count();
}
}
// EXISTS is a special alias for ALL, it allows to get the number
// of all messages in a folder also when search is active and with
// any skip_deleted setting
$a_folder_cache = $this->get_cache('messagecount');
// return cached value
if (!$force && is_array($a_folder_cache[$folder]) && isset($a_folder_cache[$folder][$mode])) {
return $a_folder_cache[$folder][$mode];
}
if (!is_array($a_folder_cache[$folder])) {
$a_folder_cache[$folder] = array();
}
if ($mode == 'THREADS') {
$res = $this->threads($folder);
$count = $res->count();
if ($status) {
$msg_count = $res->count_messages();
$this->set_folder_stats($folder, 'cnt', $msg_count);
$this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0);
}
}
// Need connection here
else if (!$this->check_connection()) {
return 0;
}
// RECENT count is fetched a bit different
else if ($mode == 'RECENT') {
$count = $this->conn->countRecent($folder);
}
// use SEARCH for message counting
else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) {
$search_str = "ALL UNDELETED";
$keys = array('COUNT');
if ($mode == 'UNSEEN') {
$search_str .= " UNSEEN";
}
else {
if ($this->messages_caching) {
$keys[] = 'ALL';
}
if ($status) {
$keys[] = 'MAX';
}
}
// @TODO: if $mode == 'ALL' we could try to use cache index here
// get message count using (E)SEARCH
// not very performant but more precise (using UNDELETED)
$index = $this->conn->search($folder, $search_str, true, $keys);
$count = $index->count();
if ($mode == 'ALL') {
// Cache index data, will be used in index_direct()
$this->icache['undeleted_idx'] = $index;
if ($status) {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $index->max());
}
}
}
else {
if ($mode == 'UNSEEN') {
$count = $this->conn->countUnseen($folder);
}
else {
$count = $this->conn->countMessages($folder);
if ($status && $mode == 'ALL') {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0);
}
}
}
$a_folder_cache[$folder][$mode] = (int)$count;
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
return (int)$count;
}
/**
* Public method for listing message flags
*
* @param string $folder Folder name
* @param array $uids Message UIDs
* @param int $mod_seq Optional MODSEQ value (of last flag update)
*
* @return array Indexed array with message flags
*/
public function list_flags($folder, $uids, $mod_seq = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return array();
}
// @TODO: when cache was synchronized in this request
// we might already have asked for flag updates, use it.
$flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
$result = array();
if (!empty($flags)) {
foreach ($flags as $message) {
$result[$message->uid] = $message->flags;
}
}
return $result;
}
/**
* Public method for listing headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
}
/**
* protected method for listing message headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($folder)) {
return array();
}
$this->set_sort_order($sort_field, $sort_order);
$page = $page ? $page : $this->list_page;
// use saved message set
- if ($this->search_string && $folder == $this->folder) {
+ if ($this->search_string) {
return $this->list_search_messages($folder, $page, $slice);
}
if ($this->threading) {
return $this->list_thread_messages($folder, $page, $slice);
}
// get UIDs of all messages in the folder, sorted
$index = $this->index($folder, $this->sort_field, $this->sort_order);
if ($index->is_empty()) {
return array();
}
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$index->slice($from, $to - $from);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch reqested messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
/**
* protected method for listing message headers using threads
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function list_thread_messages($folder, $page, $slice=0)
{
// get all threads (not sorted)
if ($mcache = $this->get_mcache_engine()) {
$threads = $mcache->get_thread($folder);
}
else {
$threads = $this->threads($folder);
}
return $this->fetch_thread_headers($folder, $threads, $page, $slice);
}
/**
* Method for fetching threads data
*
* @param string $folder Folder name
*
* @return rcube_imap_thread Thread data object
*/
function threads($folder)
{
if ($mcache = $this->get_mcache_engine()) {
// don't store in self's internal cache, cache has it's own internal cache
return $mcache->get_thread($folder);
}
if (!empty($this->icache['threads'])) {
if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
return $this->icache['threads'];
}
}
// get all threads
$result = $this->threads_direct($folder);
// add to internal (fast) cache
return $this->icache['threads'] = $result;
}
/**
* Method for direct fetching of threads data
*
* @param string $folder Folder name
*
* @return rcube_imap_thread Thread data object
*/
function threads_direct($folder)
{
if (!$this->check_connection()) {
return new rcube_result_thread();
}
// get all threads
return $this->conn->thread($folder, $this->threading,
$this->options['skip_deleted'] ? 'UNDELETED' : '', true);
}
/**
* protected method for fetching threaded messages headers
*
* @param string $folder Folder name
* @param rcube_result_thread $threads Threads data object
* @param int $page List page number
* @param int $slice Number of threads to slice
*
* @return array Messages headers
*/
protected function fetch_thread_headers($folder, $threads, $page, $slice=0)
{
// Sort thread structure
$this->sort_threads($threads);
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$threads->slice($from, $to - $from);
if ($slice) {
$threads->slice(-$slice, $slice);
}
// Get UIDs of all messages in all threads
$a_index = $threads->get();
// fetch reqested headers from server
$a_msg_headers = $this->fetch_headers($folder, $a_index);
unset($a_index);
// Set depth, has_children and unread_children fields in headers
$this->set_thread_flags($a_msg_headers, $threads);
return array_values($a_msg_headers);
}
/**
* protected method for setting threaded messages flags:
* depth, has_children and unread_children
*
* @param array $headers Reference to headers array indexed by message UID
* @param rcube_result_thread $threads Threads data object
*
* @return array Message headers array indexed by message UID
*/
protected function set_thread_flags(&$headers, $threads)
{
$parents = array();
list ($msg_depth, $msg_children) = $threads->get_thread_data();
foreach ($headers as $uid => $header) {
$depth = $msg_depth[$uid];
$parents = array_slice($parents, 0, $depth);
if (!empty($parents)) {
$headers[$uid]->parent_uid = end($parents);
if (empty($header->flags['SEEN']))
$headers[$parents[0]]->unread_children++;
}
array_push($parents, $uid);
$headers[$uid]->depth = $depth;
$headers[$uid]->has_children = $msg_children[$uid];
}
}
/**
* protected method for listing a set of message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
protected function list_search_messages($folder, $page, $slice=0)
{
if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
return array();
}
+ // gather messages from a multi-folder search
+ if ($this->search_set->multi) {
+ $page_size = $this->page_size;
+ $sort_field = $this->sort_field;
+ $search_set = $this->search_set;
+
+ // prepare paging
+ $cnt = $search_set->count();
+ $from = ($page-1) * $page_size;
+ $to = $from + $page_size;
+ $slice_length = min($page_size, $cnt - $from);
+
+ // fetch resultset headers, sort and slice them
+ if (!empty($sort_field)) {
+ $this->sort_field = null;
+ $this->page_size = 1000; // fetch up to 1000 matching messages per folder
+ $this->threading = false;
+
+ $a_msg_headers = array();
+ foreach ($search_set->sets as $resultset) {
+ if (!$resultset->is_empty()) {
+ $this->search_set = $resultset;
+ $this->search_threads = $resultset instanceof rcube_result_thread;
+ $a_msg_headers = array_merge($a_msg_headers, $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1));
+ }
+ }
+
+ // sort headers
+ if (!empty($a_msg_headers)) {
+ $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
+ }
+
+ // store (sorted) message index
+ $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
+
+ // only return the requested part of the set
+ $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
+ }
+ else {
+ if ($this->sort_order != $search_set->get_parameters('ORDER')) {
+ $search_set->revert();
+ }
+
+ // slice resultset first...
+ $fetch = array();
+ foreach (array_slice($search_set->get(), $from, $slice_length) as $msg_id) {
+ list($uid, $folder) = explode('-', $msg_id, 2);
+ $fetch[$folder][] = $uid;
+ }
+
+ // ... and fetch the requested set of headers
+ $a_msg_headers = array();
+ foreach ($fetch as $folder => $a_index) {
+ $a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
+ }
+ }
+
+ if ($slice) {
+ $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
+ }
+
+ // restore members
+ $this->sort_field = $sort_field;
+ $this->page_size = $page_size;
+ $this->search_set = $search_set;
+
+ return $a_msg_headers;
+ }
+
// use saved messages from searching
if ($this->threading) {
return $this->list_search_thread_messages($folder, $page, $slice);
}
// search set is threaded, we need a new one
if ($this->search_threads) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
$index = clone $this->search_set;
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
// return empty array if no messages found
if ($index->is_empty()) {
return array();
}
// quickest method (default sorting)
if (!$this->search_sort_field && !$this->sort_field) {
$got_index = true;
}
// sorted messages, so we can first slice array and then fetch only wanted headers
else if ($this->search_sorted) { // SORT searching result
$got_index = true;
// reset search set if sorting field has been changed
if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
$index = clone $this->search_set;
// return empty array if no messages found
if ($index->is_empty()) {
return array();
}
}
}
if ($got_index) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
// get messages uids for one page
$index->slice($from, $to-$from);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
// SEARCH result, need sorting
$cnt = $index->count();
// 300: experimantal value for best result
if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
// use memory less expensive (and quick) method for big result set
$index = clone $this->index('', $this->sort_field, $this->sort_order);
// get messages uids for one page...
$index->slice($from, min($cnt-$from, $this->page_size));
if ($slice) {
$index->slice(-$slice, $slice);
}
// ...and fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
else {
// for small result set we can fetch all messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index, false);
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers)) {
return array();
}
if (!$this->check_connection()) {
return array();
}
// if not already sorted
$a_msg_headers = $this->conn->sortHeaders(
$a_msg_headers, $this->sort_field, $this->sort_order);
// only return the requested part of the set
$slice_length = min($this->page_size, $cnt - ($to > $cnt ? $from : $to));
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
return $a_msg_headers;
}
}
/**
* protected method for listing a set of threaded message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_search_messages()
*/
protected function list_search_thread_messages($folder, $page, $slice=0)
{
// update search_set if previous data was fetched with disabled threading
if (!$this->search_threads) {
if ($this->search_set->is_empty()) {
return array();
}
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
}
/**
* Fetches messages headers (by UID)
*
* @param string $folder Folder name
* @param array $msgs Message UIDs
* @param bool $sort Enables result sorting by $msgs
* @param bool $force Disables cache use
*
* @return array Messages headers indexed by UID
*/
function fetch_headers($folder, $msgs, $sort = true, $force = false)
{
if (empty($msgs)) {
return array();
}
if (!$force && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_messages($folder, $msgs);
}
else if (!$this->check_connection()) {
return array();
}
else {
// fetch reqested headers from server
$headers = $this->conn->fetchHeaders(
$folder, $msgs, true, false, $this->get_fetch_headers());
}
if (empty($headers)) {
return array();
}
foreach ($headers as $h) {
+ $h->folder = $folder;
$a_msg_headers[$h->uid] = $h;
}
if ($sort) {
// use this class for message sorting
$sorter = new rcube_message_header_sorter();
$sorter->set_index($msgs);
$sorter->sort_headers($a_msg_headers);
}
return $a_msg_headers;
}
/**
* Returns current status of a folder (compared to the last time use)
*
* We compare the maximum UID to determine the number of
* new messages because the RECENT flag is not reliable.
*
* @param string $folder Folder name
* @param array $diff Difference data
*
* @return int Folder status
*/
public function folder_status($folder = null, &$diff = array())
{
if (!strlen($folder)) {
$folder = $this->folder;
}
$old = $this->get_folder_stats($folder);
// refresh message count -> will update
$this->countmessages($folder, 'ALL', true);
$result = 0;
if (empty($old)) {
return $result;
}
$new = $this->get_folder_stats($folder);
// got new messages
if ($new['maxuid'] > $old['maxuid']) {
$result += 1;
// get new message UIDs range, that can be used for example
// to get the data of these messages
$diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid'];
}
// some messages has been deleted
if ($new['cnt'] < $old['cnt']) {
$result += 2;
}
// @TODO: optional checking for messages flags changes (?)
// @TODO: UIDVALIDITY checking
return $result;
}
/**
* Stores folder statistic data in session
* @TODO: move to separate DB table (cache?)
*
* @param string $folder Folder name
* @param string $name Data name
* @param mixed $data Data value
*/
protected function set_folder_stats($folder, $name, $data)
{
$_SESSION['folders'][$folder][$name] = $data;
}
/**
* Gets folder statistic data
*
* @param string $folder Folder name
*
* @return array Stats data
*/
protected function get_folder_stats($folder)
{
if ($_SESSION['folders'][$folder]) {
return (array) $_SESSION['folders'][$folder];
}
return array();
}
/**
* Return sorted list of message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param bool $no_threads Get not threaded index
* @param bool $no_search Get index not limited to search result (optionally)
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
$no_threads = false, $no_search = false
) {
if (!$no_threads && $this->threading) {
return $this->thread_index($folder, $sort_field, $sort_order);
}
$this->set_sort_order($sort_field, $sort_order);
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string) {
if ($this->search_set->is_empty()) {
return new rcube_result_index($folder, '* SORT');
}
+ if ($this->search_set instanceof rcube_result_multifolder) {
+ $index = $this->search_set;
+ $index->folder = $folder;
+ // TODO: handle changed sorting
+ }
// search result is an index with the same sorting?
- if (($this->search_set instanceof rcube_result_index)
+ else if (($this->search_set instanceof rcube_result_index)
&& ((!$this->sort_field && !$this->search_sorted) ||
($this->search_sorted && $this->search_sort_field == $this->sort_field))
) {
$index = $this->search_set;
}
// $no_search is enabled when we are not interested in
// fetching index for search result, e.g. to sort
// threaded search result we can use full mailbox index.
// This makes possible to use index from cache
else if (!$no_search) {
if (!$this->sort_field) {
// No sorting needed, just build index from the search result
// @TODO: do we need to sort by UID here?
$search = $this->search_set->get_compressed();
$index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
}
else {
$index = $this->index_direct($folder, $this->search_charset,
$this->sort_field, $this->search_set);
}
}
if (isset($index)) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
}
// check local cache
if ($mcache = $this->get_mcache_engine()) {
return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
}
// fetch from IMAP server
return $this->index_direct($folder, $this->sort_field, $this->sort_order);
}
/**
* Return sorted list of message UIDs ignoring current search settings.
* Doesn't uses cache by default.
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param rcube_result_* $search Optional messages set to limit the result
*
* @return rcube_result_index Sorted list of message UIDs
*/
public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
{
if (!empty($search)) {
- $search = $this->search_set->get_compressed();
+ $search = $search->get_compressed();
}
// use message index sort as default sorting
if (!$sort_field) {
// use search result from count() if possible
if (empty($search) && $this->options['skip_deleted']
&& !empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('ALL') !== null
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$index = $this->icache['undeleted_idx'];
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
else {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->search($folder, $query, true);
}
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
// fetch complete message index
else {
if ($this->get_capability('SORT')) {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->sort($folder, $sort_field, $query, true);
}
if (empty($index) || $index->is_error()) {
$index = $this->conn->index($folder, $search ? $search : "1:*",
$sort_field, $this->options['skip_deleted'],
$search ? true : false, true);
}
}
if ($sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
/**
* Return index of threaded message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
*
* @return rcube_result_thread Message UIDs
*/
public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string && $this->search_threads && $folder == $this->folder) {
$threads = $this->search_set;
}
else {
// get all threads (default sort order)
$threads = $this->threads($folder);
}
$this->set_sort_order($sort_field, $sort_order);
$this->sort_threads($threads);
return $threads;
}
/**
* Sort threaded result, using THREAD=REFS method if available.
* If not, use any method and re-sort the result in THREAD=REFS way.
*
* @param rcube_result_thread $threads Threads result set
*/
protected function sort_threads($threads)
{
if ($threads->is_empty()) {
return;
}
// THREAD=ORDEREDSUBJECT: sorting by sent date of root message
// THREAD=REFERENCES: sorting by sent date of root message
// THREAD=REFS: sorting by the most recent date in each thread
if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
$sortby = $this->sort_field ? $this->sort_field : 'date';
$index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
if (!$index->is_empty()) {
$threads->sort($index);
}
}
else if ($this->sort_order != $threads->get_parameters('ORDER')) {
$threads->revert();
}
}
/**
* Invoke search request to IMAP server
*
* @param string $folder Folder name to search in
- * @param string $str Search criteria
+ * @param string $search Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
*
+ * @return rcube_result_index Search result object
* @todo: Search criteria should be provided in non-IMAP format, eg. array
*/
- public function search($folder='', $str='ALL', $charset=NULL, $sort_field=NULL)
+ public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
{
- if (!$str) {
- $str = 'ALL';
+ if (!$search) {
+ $search = 'ALL';
}
- if (!strlen($folder)) {
+ if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
$folder = $this->folder;
}
- $results = $this->search_index($folder, $str, $charset, $sort_field);
+ $plugin = $this->plugins->exec_hook('imap_search_before', array(
+ 'folder' => $folder,
+ 'search' => $search,
+ 'charset' => $charset,
+ 'sort_field' => $sort_field,
+ 'threading' => $this->threading,
+ ));
+
+ $folder = $plugin['folder'];
+ $search = $plugin['search'];
+ $charset = $plugin['charset'];
+ $sort_field = $plugin['sort_field'];
+ $results = $plugin['result'];
+
+ // multi-folder search
+ if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
+ // connect IMAP to have all the required classes and settings loaded
+ $this->check_connection();
+
+ // disable threading
+ $this->threading = false;
+
+ $searcher = new rcube_imap_search($this->options, $this->conn);
- $this->set_search_set(array($str, $results, $charset, $sort_field,
- $this->threading || $this->search_sorted ? true : false));
+ // set limit to not exceed the client's request timeout
+ $searcher->set_timelimit(60);
+
+ // continue existing incomplete search
+ if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) {
+ $searcher->set_results($this->search_set);
+ }
+
+ // execute the search
+ $results = $searcher->exec(
+ $folder,
+ $search,
+ $charset ? $charset : $this->default_charset,
+ $sort_field && $this->get_capability('SORT') ? $sort_field : null,
+ $this->threading
+ );
+ }
+ else if (!$results) {
+ $folder = is_array($folder) ? $folder[0] : $folder;
+ $search = is_array($search) ? $search[$folder] : $search;
+ $results = $this->search_index($folder, $search, $charset, $sort_field);
+ }
+
+ $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
+
+ $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
+
+ return $results;
}
/**
* Direct (real and simple) SEARCH request (without result sorting and caching).
*
* @param string $mailbox Mailbox name to search in
* @param string $str Search string
*
* @return rcube_result_index Search result (UIDs)
*/
public function search_once($folder = null, $str = 'ALL')
{
+ if (!$this->check_connection()) {
+ return new rcube_result_index();
+ }
+
if (!$str) {
$str = 'ALL';
}
- if (!strlen($folder)) {
- $folder = $this->folder;
+ // multi-folder search
+ if (is_array($folder) && count($folder) > 1) {
+ $searcher = new rcube_imap_search($this->options, $this->conn);
+ $index = $searcher->exec($folder, $str, $this->default_charset);
}
-
- if (!$this->check_connection()) {
- return new rcube_result_index();
+ else {
+ $folder = is_array($folder) ? $folder[0] : $folder;
+ if (!strlen($folder)) {
+ $folder = $this->folder;
+ }
+ $index = $this->conn->search($folder, $str, true);
}
- $index = $this->conn->search($folder, $str, true);
-
return $index;
}
/**
* protected search method
*
* @param string $folder Folder name
* @param string $criteria Search criteria
* @param string $charset Charset
* @param string $sort_field Sorting field
*
* @return rcube_result_index|rcube_result_thread Search results (UIDs)
* @see rcube_imap::search()
*/
protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
{
if (!$this->check_connection()) {
if ($this->threading) {
return new rcube_result_thread();
}
else {
return new rcube_result_index();
}
}
if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
$criteria = 'UNDELETED '.$criteria;
}
// unset CHARSET if criteria string is ASCII, this way
// SEARCH won't be re-sent after "unsupported charset" response
if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
$charset = 'US-ASCII';
}
if ($this->threading) {
$threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen that Courier doesn't support UTF-8)
if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
$threads = $this->conn->thread($folder, $this->threading,
- $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+ self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
return $threads;
}
if ($sort_field && $this->get_capability('SORT')) {
$charset = $charset ? $charset : $this->default_charset;
$messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen Courier with disabled UTF-8 support)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->sort($folder, $sort_field,
- $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+ self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
if (!$messages->is_error()) {
$this->search_sorted = true;
return $messages;
}
}
$messages = $this->conn->search($folder,
($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
// Error, try with US-ASCII (some servers may support only US-ASCII)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->search($folder,
- $this->convert_criteria($criteria, $charset), true);
+ self::convert_criteria($criteria, $charset), true);
}
$this->search_sorted = false;
return $messages;
}
/**
* Converts charset of search criteria string
*
* @param string $str Search string
* @param string $charset Original charset
* @param string $dest_charset Destination charset (default US-ASCII)
*
* @return string Search string
*/
- protected function convert_criteria($str, $charset, $dest_charset='US-ASCII')
+ public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
{
// convert strings to US_ASCII
if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
$last = 0; $res = '';
foreach ($matches[1] as $m) {
$string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
$string = substr($str, $string_offset - 1, $m[0]);
$string = rcube_charset::convert($string, $charset, $dest_charset);
- if ($string === false) {
+
+ if ($string === false || !strlen($string)) {
continue;
}
+
$res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
$last = $m[0] + $string_offset - 1;
}
+
if ($last < strlen($str)) {
$res .= substr($str, $last, strlen($str)-$last);
}
}
// strings for conversion not found
else {
$res = $str;
}
return $res;
}
/**
* Refresh saved search set
*
* @return array Current search set
*/
public function refresh_search()
{
if (!empty($this->search_string)) {
- $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
+ $this->search(
+ is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
+ $this->search_string,
+ $this->search_charset,
+ $this->search_sort_field
+ );
}
return $this->get_search_set();
}
+ /**
+ * Flag certain result subsets as 'incomplete'.
+ * For subsequent refresh_search() calls to only refresh the updated parts.
+ */
+ protected function set_search_dirty($folder)
+ {
+ if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
+ if ($subset = $this->search_set->get_set($folder)) {
+ $subset->incomplete = $this->search_set->incomplete = true;
+ }
+ }
+ }
+
/**
* Return message headers object of a specific message
*
* @param int $id Message UID
* @param string $folder Folder to read from
* @param bool $force True to skip cache
*
* @return rcube_message_header Message headers
*/
public function get_message_headers($uid, $folder = null, $force = false)
{
+ // decode combined UID-folder identifier
+ if (preg_match('/^\d+-.+/', $uid)) {
+ list($uid, $folder) = explode('-', $uid, 2);
+ }
+
if (!strlen($folder)) {
$folder = $this->folder;
}
// get cached headers
if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_message($folder, $uid);
}
else if (!$this->check_connection()) {
$headers = false;
}
else {
$headers = $this->conn->fetchHeader(
$folder, $uid, true, true, $this->get_fetch_headers());
+
+ if (is_object($headers))
+ $headers->folder = $folder;
}
return $headers;
}
/**
* Fetch message headers and body structure from the IMAP server and build
* an object structure similar to the one generated by PEAR::Mail_mimeDecode
*
* @param int $uid Message UID to fetch
* @param string $folder Folder to read from
*
* @return object rcube_message_header Message data
*/
public function get_message($uid, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
+ // decode combined UID-folder identifier
+ if (preg_match('/^\d+-.+/', $uid)) {
+ list($uid, $folder) = explode('-', $uid, 2);
+ }
+
// Check internal cache
if (!empty($this->icache['message'])) {
if (($headers = $this->icache['message']) && $headers->uid == $uid) {
return $headers;
}
}
$headers = $this->get_message_headers($uid, $folder);
// message doesn't exist?
if (empty($headers)) {
return null;
}
// structure might be cached
if (!empty($headers->structure)) {
return $headers;
}
$this->msg_uid = $uid;
if (!$this->check_connection()) {
return $headers;
}
if (empty($headers->bodystructure)) {
$headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
}
$structure = $headers->bodystructure;
if (empty($structure)) {
return $headers;
}
// set message charset from message headers
if ($headers->charset) {
$this->struct_charset = $headers->charset;
}
else {
$this->struct_charset = $this->structure_charset($structure);
}
$headers->ctype = @strtolower($headers->ctype);
// Here we can recognize malformed BODYSTRUCTURE and
// 1. [@TODO] parse the message in other way to create our own message structure
// 2. or just show the raw message body.
// Example of structure for malformed MIME message:
// ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
&& strtolower($structure[0].'/'.$structure[1]) == 'text/plain'
) {
// A special known case "Content-type: text" (#1488968)
if ($headers->ctype == 'text') {
$structure[1] = 'plain';
$headers->ctype = 'text/plain';
}
// we can handle single-part messages, by simple fix in structure (#1486898)
else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
$structure[0] = $m[1];
$structure[1] = $m[2];
}
else {
// Try to parse the message using Mail_mimeDecode package
// We need a better solution, Mail_mimeDecode parses message
// in memory, which wouldn't work for very big messages,
// (it uses up to 10x more memory than the message size)
// it's also buggy and not actively developed
if ($headers->size && rcube_utils::mem_check($headers->size * 10)) {
$raw_msg = $this->get_raw_body($uid);
$struct = rcube_mime::parse_message($raw_msg);
}
else {
return $headers;
}
}
}
if (empty($struct)) {
$struct = $this->structure_part($structure, 0, '', $headers);
}
// some workarounds on simple messages...
if (empty($struct->parts)) {
// ...don't trust given content-type
if (!empty($headers->ctype)) {
$struct->mime_id = '1';
$struct->mimetype = strtolower($headers->ctype);
list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
}
// ...and charset (there's a case described in #1488968 where invalid content-type
// results in invalid charset in BODYSTRUCTURE)
if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) {
$struct->charset = $headers->charset;
$struct->ctype_parameters['charset'] = $headers->charset;
}
}
$headers->structure = $struct;
return $this->icache['message'] = $headers;
}
/**
* Build message part object
*
* @param array $part
* @param int $count
* @param string $parent
*/
protected function structure_part($part, $count=0, $parent='', $mime_headers=null)
{
$struct = new rcube_message_part;
$struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
// multipart
if (is_array($part[0])) {
$struct->ctype_primary = 'multipart';
/* RFC3501: BODYSTRUCTURE fields of multipart part
part1 array
part2 array
part3 array
....
1. subtype
2. parameters (optional)
3. description (optional)
4. language (optional)
5. location (optional)
*/
// find first non-array entry
for ($i=1; $i<count($part); $i++) {
if (!is_array($part[$i])) {
$struct->ctype_secondary = strtolower($part[$i]);
break;
}
}
$struct->mimetype = 'multipart/'.$struct->ctype_secondary;
// build parts list for headers pre-fetching
for ($i=0; $i<count($part); $i++) {
if (!is_array($part[$i])) {
break;
}
// fetch message headers if message/rfc822
// or named part (could contain Content-Location header)
if (!is_array($part[$i][0])) {
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
$mime_part_headers[] = $tmp_part_id;
}
else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
$mime_part_headers[] = $tmp_part_id;
}
}
}
// pre-fetch headers of all parts (in one command for better performance)
// @TODO: we could do this before _structure_part() call, to fetch
// headers for parts on all levels
if ($mime_part_headers) {
$mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder,
$this->msg_uid, $mime_part_headers);
}
$struct->parts = array();
for ($i=0, $count=0; $i<count($part); $i++) {
if (!is_array($part[$i])) {
break;
}
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
$struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id,
$mime_part_headers[$tmp_part_id]);
}
return $struct;
}
/* RFC3501: BODYSTRUCTURE fields of non-multipart part
0. type
1. subtype
2. parameters
3. id
4. description
5. encoding
6. size
-- text
7. lines
-- message/rfc822
7. envelope structure
8. body structure
9. lines
--
x. md5 (optional)
x. disposition (optional)
x. language (optional)
x. location (optional)
*/
// regular part
$struct->ctype_primary = strtolower($part[0]);
$struct->ctype_secondary = strtolower($part[1]);
$struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
// read content type parameters
if (is_array($part[2])) {
$struct->ctype_parameters = array();
for ($i=0; $i<count($part[2]); $i+=2) {
$struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
}
if (isset($struct->ctype_parameters['charset'])) {
$struct->charset = $struct->ctype_parameters['charset'];
}
}
// #1487700: workaround for lack of charset in malformed structure
if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
$struct->charset = $mime_headers->charset;
}
// read content encoding
if (!empty($part[5])) {
$struct->encoding = strtolower($part[5]);
$struct->headers['content-transfer-encoding'] = $struct->encoding;
}
// get part size
if (!empty($part[6])) {
$struct->size = intval($part[6]);
}
// read part disposition
$di = 8;
if ($struct->ctype_primary == 'text') {
$di += 1;
}
else if ($struct->mimetype == 'message/rfc822') {
$di += 3;
}
if (is_array($part[$di]) && count($part[$di]) == 2) {
$struct->disposition = strtolower($part[$di][0]);
if (is_array($part[$di][1])) {
for ($n=0; $n<count($part[$di][1]); $n+=2) {
$struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
}
}
}
// get message/rfc822's child-parts
if (is_array($part[8]) && $di != 8) {
$struct->parts = array();
for ($i=0, $count=0; $i<count($part[8]); $i++) {
if (!is_array($part[8][$i])) {
break;
}
$struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id);
}
}
// get part ID
if (!empty($part[3])) {
$struct->content_id = $part[3];
$struct->headers['content-id'] = $part[3];
if (empty($struct->disposition)) {
$struct->disposition = 'inline';
}
}
// fetch message headers if message/rfc822 or named part (could contain Content-Location header)
if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
if (empty($mime_headers)) {
$mime_headers = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $struct->mime_id);
}
if (is_string($mime_headers)) {
$struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
}
else if (is_object($mime_headers)) {
$struct->headers = get_object_vars($mime_headers) + $struct->headers;
}
// get real content-type of message/rfc822
if ($struct->mimetype == 'message/rfc822') {
// single-part
if (!is_array($part[8][0])) {
$struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
}
// multi-part
else {
for ($n=0; $n<count($part[8]); $n++) {
if (!is_array($part[8][$n])) {
break;
}
}
$struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
}
}
if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
if (is_array($part[8]) && $di != 8) {
$struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id);
}
}
}
// normalize filename property
$this->set_part_filename($struct, $mime_headers);
return $struct;
}
/**
* Set attachment filename from message part structure
*
* @param rcube_message_part $part Part object
* @param string $headers Part's raw headers
*/
protected function set_part_filename(&$part, $headers=null)
{
if (!empty($part->d_parameters['filename'])) {
$filename_mime = $part->d_parameters['filename'];
}
else if (!empty($part->d_parameters['filename*'])) {
$filename_encoded = $part->d_parameters['filename*'];
}
else if (!empty($part->ctype_parameters['name*'])) {
$filename_encoded = $part->ctype_parameters['name*'];
}
// RFC2231 value continuations
// TODO: this should be rewrited to support RFC2231 4.1 combinations
else if (!empty($part->d_parameters['filename*0'])) {
$i = 0;
while (isset($part->d_parameters['filename*'.$i])) {
$filename_mime .= $part->d_parameters['filename*'.$i];
$i++;
}
// some servers (eg. dovecot-1.x) have no support for parameter value continuations
// we must fetch and parse headers "manually"
if ($i<2) {
if (!$headers) {
$headers = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $part->mime_id);
}
$filename_mime = '';
$i = 0;
while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_mime .= $matches[1];
$i++;
}
}
}
else if (!empty($part->d_parameters['filename*0*'])) {
$i = 0;
while (isset($part->d_parameters['filename*'.$i.'*'])) {
$filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
$i++;
}
if ($i<2) {
if (!$headers) {
$headers = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $part->mime_id);
}
$filename_encoded = '';
$i = 0; $matches = array();
while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_encoded .= $matches[1];
$i++;
}
}
}
else if (!empty($part->ctype_parameters['name*0'])) {
$i = 0;
while (isset($part->ctype_parameters['name*'.$i])) {
$filename_mime .= $part->ctype_parameters['name*'.$i];
$i++;
}
if ($i<2) {
if (!$headers) {
$headers = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $part->mime_id);
}
$filename_mime = '';
$i = 0; $matches = array();
while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_mime .= $matches[1];
$i++;
}
}
}
else if (!empty($part->ctype_parameters['name*0*'])) {
$i = 0;
while (isset($part->ctype_parameters['name*'.$i.'*'])) {
$filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
$i++;
}
if ($i<2) {
if (!$headers) {
$headers = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $part->mime_id);
}
$filename_encoded = '';
$i = 0; $matches = array();
while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_encoded .= $matches[1];
$i++;
}
}
}
// read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
else if (!empty($part->ctype_parameters['name'])) {
$filename_mime = $part->ctype_parameters['name'];
}
// Content-Disposition
else if (!empty($part->headers['content-description'])) {
$filename_mime = $part->headers['content-description'];
}
else {
return;
}
// decode filename
if (!empty($filename_mime)) {
if (!empty($part->charset)) {
$charset = $part->charset;
}
else if (!empty($this->struct_charset)) {
$charset = $this->struct_charset;
}
else {
$charset = rcube_charset::detect($filename_mime, $this->default_charset);
}
$part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
}
else if (!empty($filename_encoded)) {
// decode filename according to RFC 2231, Section 4
if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
$filename_charset = $fmatches[1];
$filename_encoded = $fmatches[2];
}
$part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset);
}
}
/**
* Get charset name from message structure (first part)
*
* @param array $structure Message structure
*
* @return string Charset name
*/
protected function structure_charset($structure)
{
while (is_array($structure)) {
if (is_array($structure[2]) && $structure[2][0] == 'charset') {
return $structure[2][1];
}
$structure = $structure[0];
}
}
/**
* Fetch message body of a specific message from the server
*
* @param int Message UID
* @param string Part number
* @param rcube_message_part Part object created by get_structure()
* @param mixed True to print part, resource to write part contents in
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
* @param int Only read this number of bytes
* @param boolean Enables formatting of text/* parts bodies
*
* @return string Message/part body if not printed
*/
public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0, $formatted=true)
{
if (!$this->check_connection()) {
return null;
}
// get part data if not provided
if (!is_object($o_part)) {
$structure = $this->conn->getStructure($this->folder, $uid, true);
$part_data = rcube_imap_generic::getStructurePartData($structure, $part);
$o_part = new rcube_message_part;
$o_part->ctype_primary = $part_data['type'];
$o_part->encoding = $part_data['encoding'];
$o_part->charset = $part_data['charset'];
$o_part->size = $part_data['size'];
}
if ($o_part && $o_part->size) {
$formatted = $formatted && $o_part->ctype_primary == 'text';
$body = $this->conn->handlePartBody($this->folder, $uid, true,
$part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
}
if ($fp || $print) {
return true;
}
// convert charset (if text or message part)
if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
// Remove NULL characters if any (#1486189)
if ($formatted && strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
if (!$skip_charset_conv) {
if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
// try to extract charset information from HTML meta tag (#1488125)
if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
$o_part->charset = strtoupper($m[1]);
}
else {
$o_part->charset = $this->default_charset;
}
}
$body = rcube_charset::convert($body, $o_part->charset);
}
}
return $body;
}
/**
* Returns the whole message source as string (or saves to a file)
*
* @param int $uid Message UID
* @param resource $fp File pointer to save the message
*
* @return string Message source string
*/
public function get_raw_body($uid, $fp=null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->handlePartBody($this->folder, $uid,
true, null, null, false, $fp);
}
/**
* Returns the message headers as string
*
* @param int $uid Message UID
*
* @return string Message headers string
*/
public function get_raw_headers($uid)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->fetchPartHeader($this->folder, $uid, true);
}
/**
* Sends the whole message source to stdout
*
* @param int $uid Message UID
* @param bool $formatted Enables line-ending formatting
*/
public function print_raw_body($uid, $formatted = true)
{
if (!$this->check_connection()) {
return;
}
$this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
}
/**
* Set message flag to one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
* @param boolean $skip_cache True to skip message cache clean up
*
* @return boolean Operation status
*/
public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
$flag = strtoupper($flag);
list($uids, $all_mode) = $this->parse_uids($uids);
if (strpos($flag, 'UN') === 0) {
$result = $this->conn->unflag($folder, $uids, substr($flag, 2));
}
else {
$result = $this->conn->flag($folder, $uids, $flag);
}
if ($result && !$skip_cache) {
// reload message headers if cached
// update flags instead removing from cache
if ($mcache = $this->get_mcache_engine()) {
$status = strpos($flag, 'UN') !== 0;
$mflag = preg_replace('/^UN/', '', $flag);
$mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
$mflag, $status);
}
// clear cached counters
if ($flag == 'SEEN' || $flag == 'UNSEEN') {
$this->clear_messagecount($folder, 'SEEN');
$this->clear_messagecount($folder, 'UNSEEN');
}
else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
$this->clear_messagecount($folder, 'DELETED');
// remove cached messages
if ($this->options['skip_deleted']) {
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
}
+
+ $this->set_search_dirty($folder);
}
return $result;
}
/**
* Append a mail message (source) to a specific folder
*
* @param string $folder Target folder
* @param string|array $message The message source string or filename
* or array (of strings and file pointers)
* @param string $headers Headers string if $message contains only the body
* @param boolean $is_file True if $message is a filename
* @param array $flags Message flags
* @param mixed $date Message internal date
* @param bool $binary Enables BINARY append
*
* @return int|bool Appended message UID or True on success, False on error
*/
public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// make sure folder exists
if (!$this->folder_exists($folder)) {
return false;
}
$date = $this->date_format($date);
if ($is_file) {
$saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary);
}
else {
$saved = $this->conn->append($folder, $message, $flags, $date, $binary);
}
if ($saved) {
// increase messagecount of the target folder
$this->set_messagecount($folder, 'ALL', 1);
+
+ $this->plugins->exec_hook('message_saved', array(
+ 'folder' => $folder,
+ 'message' => $message,
+ 'headers' => $headers,
+ 'is_file' => $is_file,
+ 'flags' => $flags,
+ 'date' => $date,
+ 'binary' => $binary,
+ 'result' => $saved,
+ ));
}
return $saved;
}
/**
* Move a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return boolean True on success, False on error
*/
public function move_message($uids, $to_mbox, $from_mbox='')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
if ($to_mbox === $from_mbox) {
return false;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
- // make sure folder exists
- if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
- if (in_array($to_mbox, $this->default_folders)) {
- if (!$this->create_folder($to_mbox, true)) {
- return false;
- }
- }
- else {
- return false;
- }
- }
-
- $config = rcube::get_instance()->config;
+ $config = rcube::get_instance()->config;
$to_trash = $to_mbox == $config->get('trash_mbox');
// flag messages as read before moving them
if ($to_trash && $config->get('read_when_deleted')) {
// don't flush cache (4th argument)
$this->set_flag($uids, 'SEEN', $from_mbox, true);
}
// move messages
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
if ($moved) {
$this->clear_messagecount($from_mbox);
$this->clear_messagecount($to_mbox);
+
+ $this->set_search_dirty($from_mbox);
+ $this->set_search_dirty($to_mbox);
}
// moving failed
else if ($to_trash && $config->get('delete_always', false)) {
$moved = $this->delete_message($uids, $from_mbox);
}
if ($moved) {
// unset threads internal cache
unset($this->icache['threads']);
// remove message ids from search set
if ($this->search_set && $from_mbox == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
- else {
- $this->search_set->filter(explode(',', $uids));
+ else if (!$this->search_set->incomplete) {
+ $this->search_set->filter(explode(',', $uids), $this->folder);
}
}
// remove cached messages
// @TODO: do cache update instead of clearing it
$this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
}
return $moved;
}
/**
* Copy a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return boolean True on success, False on error
*/
public function copy_message($uids, $to_mbox, $from_mbox='')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
- // make sure folder exists
- if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
- if (in_array($to_mbox, $this->default_folders)) {
- if (!$this->create_folder($to_mbox, true)) {
- return false;
- }
- }
- else {
- return false;
- }
- }
-
// copy messages
$copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
if ($copied) {
$this->clear_messagecount($to_mbox);
}
return $copied;
}
/**
* Mark messages as deleted and expunge them
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Source folder
*
* @return boolean True on success, False on error
*/
public function delete_message($uids, $folder='')
{
if (!strlen($folder)) {
$folder = $this->folder;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$deleted = $this->conn->flag($folder, $uids, 'DELETED');
if ($deleted) {
// send expunge command in order to have the deleted message
// really deleted from the folder
$this->expunge_message($uids, $folder, false);
$this->clear_messagecount($folder);
unset($this->uid_id_map[$folder]);
// unset threads internal cache
unset($this->icache['threads']);
+ $this->set_search_dirty($folder);
+
// remove message ids from search set
if ($this->search_set && $folder == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
- else {
+ else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids));
}
}
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
return $deleted;
}
/**
* Send IMAP expunge command and clear cache
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on failure
*/
public function expunge_message($uids, $folder = null, $clear_cache = true)
{
if ($uids && $this->get_capability('UIDPLUS')) {
list($uids, $all_mode) = $this->parse_uids($uids);
}
else {
$uids = null;
}
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// force folder selection and check if folder is writeable
// to prevent a situation when CLOSE is executed on closed
// or EXPUNGE on read-only folder
$result = $this->conn->select($folder);
if (!$result) {
return false;
}
if (!$this->conn->data['READ-WRITE']) {
$this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
return false;
}
// CLOSE(+SELECT) should be faster than EXPUNGE
if (empty($uids) || $all_mode) {
$result = $this->conn->close();
}
else {
$result = $this->conn->expunge($folder, $uids);
}
if ($result && $clear_cache) {
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
$this->clear_messagecount($folder);
}
return $result;
}
/* --------------------------------
* folder managment
* --------------------------------*/
/**
* Public method for listing subscribed folders.
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
* @param string $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array List of folders
*/
public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
{
$cache_key = $root.':'.$name;
if (!empty($filter)) {
$cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
}
$cache_key .= ':'.$rights;
$cache_key = 'mailboxes.'.md5($cache_key);
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
- $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
+ $data = $this->plugins->exec_hook('storage_folders',
array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
$a_mboxes = $this->list_folders_subscribed_direct($root, $name);
}
if (!is_array($a_mboxes)) {
return array();
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// INBOX should always be available
if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
array_unshift($a_mboxes, 'INBOX');
}
// sort folders (always sort for cache)
if (!$skip_sort || $this->cache) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LSUB)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of subscribed folders
* @see rcube_imap::list_folders_subscribed()
*/
public function list_folders_subscribed_direct($root='', $name='*')
{
if (!$this->check_connection()) {
return null;
}
$config = rcube::get_instance()->config;
// Server supports LIST-EXTENDED, we can use selection options
// #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
$list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
if ($list_extended) {
// This will also set folder options, LSUB doesn't do that
$a_folders = $this->conn->listMailboxes($root, $name,
NULL, array('SUBSCRIBED'));
}
else {
// retrieve list of folders from IMAP server using LSUB
$a_folders = $this->conn->listSubscribed($root, $name);
}
if (!is_array($a_folders)) {
return array();
}
// #1486796: some server configurations doesn't return folders in all namespaces
if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
$this->list_folders_update($a_folders, ($list_extended ? 'ext-' : '') . 'subscribed');
}
if ($list_extended) {
// unsubscribe non-existent folders, remove from the list
if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
foreach ($a_folders as $idx => $folder) {
if (($opts = $this->conn->data['LIST'][$folder])
&& in_array('\\NonExistent', $opts)
) {
$this->conn->unsubscribe($folder);
unset($a_folders[$idx]);
}
}
}
}
else {
// unsubscribe non-existent folders, remove them from the list
if (is_array($a_folders) && !empty($a_folders) && $name == '*') {
$existing = $this->list_folders($root, $name);
$nonexisting = array_diff($a_folders, $existing);
$a_folders = array_diff($a_folders, $nonexisting);
foreach ($nonexisting as $folder) {
$this->conn->unsubscribe($folder);
}
}
}
return $a_folders;
}
/**
* Get a list of all folders available on the server
*
* @param string $root IMAP root dir
* @param string $name Optional name pattern
* @param mixed $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array Indexed array with folder names
*/
public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
{
$cache_key = $root.':'.$name;
if (!empty($filter)) {
$cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
}
$cache_key .= ':'.$rights;
$cache_key = 'mailboxes.list.'.md5($cache_key);
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
- $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
+ $data = $this->plugins->exec_hook('storage_folders',
array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
// retrieve list of folders from IMAP server
$a_mboxes = $this->list_folders_direct($root, $name);
}
if (!is_array($a_mboxes)) {
$a_mboxes = array();
}
// INBOX should always be available
if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
array_unshift($a_mboxes, 'INBOX');
}
// cache folder attributes
if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
$this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// filter folders and sort them
if (!$skip_sort) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LIST)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of folders
* @see rcube_imap::list_folders()
*/
public function list_folders_direct($root='', $name='*')
{
if (!$this->check_connection()) {
return null;
}
$result = $this->conn->listMailboxes($root, $name);
if (!is_array($result)) {
return array();
}
$config = rcube::get_instance()->config;
// #1486796: some server configurations doesn't return folders in all namespaces
if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
$this->list_folders_update($result);
}
return $result;
}
/**
* Fix folders list by adding folders from other namespaces.
* Needed on some servers eg. Courier IMAP
*
* @param array $result Reference to folders list
* @param string $type Listing type (ext-subscribed, subscribed or all)
*/
- private function list_folders_update(&$result, $type = null)
+ protected function list_folders_update(&$result, $type = null)
{
$namespace = $this->get_namespace();
$search = array();
// build list of namespace prefixes
foreach ((array)$namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $ns_data) {
if (strlen($ns_data[0])) {
$search[] = $ns_data[0];
}
}
}
}
if (!empty($search)) {
// go through all folders detecting namespace usage
foreach ($result as $folder) {
foreach ($search as $idx => $prefix) {
if (strpos($folder, $prefix) === 0) {
unset($search[$idx]);
}
}
if (empty($search)) {
break;
}
}
// get folders in hidden namespaces and add to the result
foreach ($search as $prefix) {
if ($type == 'ext-subscribed') {
$list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED'));
}
else if ($type == 'subscribed') {
$list = $this->conn->listSubscribed('', $prefix . '*');
}
else {
$list = $this->conn->listMailboxes('', $prefix . '*');
}
if (!empty($list)) {
$result = array_merge($result, $list);
}
}
}
}
/**
* Filter the given list of folders according to access rights
*
* For performance reasons we assume user has full rights
* on all personal folders.
*/
protected function filter_rights($a_folders, $rights)
{
$regex = '/('.$rights.')/';
foreach ($a_folders as $idx => $folder) {
if ($this->folder_namespace($folder) == 'personal') {
continue;
}
$myrights = join('', (array)$this->my_rights($folder));
if ($myrights !== null && !preg_match($regex, $myrights)) {
unset($a_folders[$idx]);
}
}
return $a_folders;
}
/**
* Get mailbox quota information
- * added by Nuny
+ *
+ * @param string $folder Folder name
*
* @return mixed Quota info or False if not supported
*/
- public function get_quota()
+ public function get_quota($folder = null)
{
if ($this->get_capability('QUOTA') && $this->check_connection()) {
- return $this->conn->getQuota();
+ return $this->conn->getQuota($folder);
}
return false;
}
/**
* Get folder size (size of all messages in a folder)
*
* @param string $folder Folder name
*
* @return int Folder size in bytes, False on error
*/
public function folder_size($folder)
{
if (!$this->check_connection()) {
return 0;
}
// @TODO: could we try to use QUOTA here?
$result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
if (is_array($result)) {
$result = array_sum($result);
}
return $result;
}
/**
* Subscribe to a specific folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
public function subscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'subscribe');
}
/**
* Unsubscribe folder(s)
*
* @param array $a_mboxes Folder name(s)
*
* @return boolean True on success
*/
public function unsubscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'unsubscribe');
}
/**
* Create a new folder on the server and register it in local cache
*
* @param string $folder New folder name
* @param boolean $subscribe True if the new folder should be subscribed
+ * @param string $type Optional folder type (junk, trash, drafts, sent, archive)
*
* @return boolean True on success
*/
- public function create_folder($folder, $subscribe=false)
+ public function create_folder($folder, $subscribe = false, $type = null)
{
if (!$this->check_connection()) {
return false;
}
- $result = $this->conn->createFolder($folder);
+ $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
+
+ // it's quite often situation that we're trying to create and subscribe
+ // a folder that already exist, but is unsubscribed
+ if (!$result) {
+ if ($this->get_response_code() == rcube_storage::ALREADYEXISTS
+ || preg_match('/already exists/i', $this->get_error_str())
+ ) {
+ $result = true;
+ }
+ }
// try to subscribe it
if ($result) {
// clear cache
$this->clear_cache('mailboxes', true);
if ($subscribe) {
$this->subscribe($folder);
}
}
return $result;
}
/**
* Set a new name to an existing folder
*
* @param string $folder Folder to rename
* @param string $new_name New folder name
*
* @return boolean True on success
*/
public function rename_folder($folder, $new_name)
{
if (!strlen($new_name)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of subscribed folders
if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
$a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
$subscribed = $this->folder_exists($folder, true);
}
else {
$a_subscribed = $this->list_folders_subscribed();
$subscribed = in_array($folder, $a_subscribed);
}
$result = $this->conn->renameFolder($folder, $new_name);
if ($result) {
// unsubscribe the old folder, subscribe the new one
if ($subscribed) {
$this->conn->unsubscribe($folder);
$this->conn->subscribe($new_name);
}
// check if folder children are subscribed
foreach ($a_subscribed as $c_subscribed) {
if (strpos($c_subscribed, $folder.$delm) === 0) {
$this->conn->unsubscribe($c_subscribed);
$this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
$new_name, $c_subscribed));
// clear cache
$this->clear_message_cache($c_subscribed);
}
}
// clear cache
$this->clear_message_cache($folder);
$this->clear_cache('mailboxes', true);
}
return $result;
}
/**
* Remove folder from server
*
* @param string $folder Folder name
*
* @return boolean True on success
*/
function delete_folder($folder)
{
$delm = $this->get_hierarchy_delimiter();
if (!$this->check_connection()) {
return false;
}
// get list of folders
if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
$sub_mboxes = $this->list_folders('', $folder . $delm . '*');
}
else {
$sub_mboxes = $this->list_folders();
}
// send delete command to server
$result = $this->conn->deleteFolder($folder);
if ($result) {
// unsubscribe folder
$this->conn->unsubscribe($folder);
foreach ($sub_mboxes as $c_mbox) {
if (strpos($c_mbox, $folder.$delm) === 0) {
$this->conn->unsubscribe($c_mbox);
if ($this->conn->deleteFolder($c_mbox)) {
$this->clear_message_cache($c_mbox);
}
}
}
// clear folder-related cache
$this->clear_message_cache($folder);
$this->clear_cache('mailboxes', true);
}
return $result;
}
/**
- * Create all folders specified as default
+ * Detect special folder associations stored in storage backend
*/
- public function create_default_folders()
+ public function get_special_folders($forced = false)
{
- // create default folders if they do not exist
- foreach ($this->default_folders as $folder) {
- if (!$this->folder_exists($folder)) {
- $this->create_folder($folder, true);
+ $result = parent::get_special_folders();
+
+ if (isset($this->icache['special-use'])) {
+ return array_merge($result, $this->icache['special-use']);
+ }
+
+ if (!$forced || !$this->get_capability('SPECIAL-USE')) {
+ return $result;
+ }
+
+ if (!$this->check_connection()) {
+ return $result;
+ }
+
+ $types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
+ $special = array();
+
+ // request \Subscribed flag in LIST response as performance improvement for folder_exists()
+ $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
+
+ if (!empty($folders)) {
+ foreach ($folders as $folder) {
+ if ($flags = $this->conn->data['LIST'][$folder]) {
+ foreach ($types as $type) {
+ if (in_array($type, $flags)) {
+ $type = strtolower(substr($type, 1));
+ $special[$type] = $folder;
+ }
+ }
+ }
}
- else if (!$this->folder_exists($folder, true)) {
- $this->subscribe($folder);
+ }
+
+ $this->icache['special-use'] = $special;
+ unset($this->icache['special-folders']);
+
+ return array_merge($result, $special);
+ }
+
+
+ /**
+ * Set special folder associations stored in storage backend
+ */
+ public function set_special_folders($specials)
+ {
+ if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
+ return false;
+ }
+
+ if (!$this->check_connection()) {
+ return false;
+ }
+
+ $folders = $this->get_special_folders(true);
+ $old = (array) $this->icache['special-use'];
+
+ foreach ($specials as $type => $folder) {
+ if (in_array($type, rcube_storage::$folder_types)) {
+ $old_folder = $old[$type];
+ if ($old_folder !== $folder) {
+ // unset old-folder metadata
+ if ($old_folder !== null) {
+ $this->delete_metadata($old_folder, array('/private/specialuse'));
+ }
+ // set new folder metadata
+ if ($folder) {
+ $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
+ }
+ }
}
}
+
+ $this->icache['special-use'] = $specials;
+ unset($this->icache['special-folders']);
+
+ return true;
}
/**
* Checks if folder exists and is subscribed
*
* @param string $folder Folder name
* @param boolean $subscription Enable subscription checking
*
* @return boolean TRUE or FALSE
*/
- public function folder_exists($folder, $subscription=false)
+ public function folder_exists($folder, $subscription = false)
{
if ($folder == 'INBOX') {
return true;
}
- $key = $subscription ? 'subscribed' : 'existing';
+ $key = $subscription ? 'subscribed' : 'existing';
if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
return true;
}
if (!$this->check_connection()) {
return false;
}
if ($subscription) {
- $a_folders = $this->conn->listSubscribed('', $folder);
+ // It's possible we already called LIST command, check LIST data
+ if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
+ && in_array('\\Subscribed', $this->conn->data['LIST'][$folder])
+ ) {
+ $a_folders = array($folder);
+ }
+ else {
+ $a_folders = $this->conn->listSubscribed('', $folder);
+ }
}
else {
- $a_folders = $this->conn->listMailboxes('', $folder);
+ // It's possible we already called LIST command, check LIST data
+ if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
+ $a_folders = array($folder);
+ }
+ else {
+ $a_folders = $this->conn->listMailboxes('', $folder);
+ }
}
if (is_array($a_folders) && in_array($folder, $a_folders)) {
$this->icache[$key][] = $folder;
return true;
}
return false;
}
/**
* Returns the namespace where the folder is in
*
* @param string $folder Folder name
*
* @return string One of 'personal', 'other' or 'shared'
*/
public function folder_namespace($folder)
{
if ($folder == 'INBOX') {
return 'personal';
}
foreach ($this->namespace as $type => $namespace) {
if (is_array($namespace)) {
foreach ($namespace as $ns) {
if ($len = strlen($ns[0])) {
if (($len > 1 && $folder == substr($ns[0], 0, -1))
|| strpos($folder, $ns[0]) === 0
) {
return $type;
}
}
}
}
}
return 'personal';
}
/**
* Modify folder name according to namespace.
* For output it removes prefix of the personal namespace if it's possible.
* For input it adds the prefix. Use it before creating a folder in root
* of the folders tree.
*
* @param string $folder Folder name
* @param string $mode Mode name (out/in)
*
* @return string Folder name
*/
public function mod_folder($folder, $mode = 'out')
{
if (!strlen($folder)) {
return $folder;
}
$prefix = $this->namespace['prefix']; // see set_env()
$prefix_len = strlen($prefix);
if (!$prefix_len) {
return $folder;
}
// remove prefix for output
if ($mode == 'out') {
if (substr($folder, 0, $prefix_len) === $prefix) {
return substr($folder, $prefix_len);
}
}
// add prefix for input (e.g. folder creation)
else {
return $prefix . $folder;
}
return $folder;
}
/**
* Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
*
* @param string $folder Folder name
* @param bool $force Set to True if attributes should be refreshed
*
* @return array Options list
*/
public function folder_attributes($folder, $force=false)
{
// get attributes directly from LIST command
if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
$opts = $this->conn->data['LIST'][$folder];
}
// get cached folder attributes
else if (!$force) {
$opts = $this->get_cache('mailboxes.attributes');
$opts = $opts[$folder];
}
if (!is_array($opts)) {
if (!$this->check_connection()) {
return array();
}
$this->conn->listMailboxes('', $folder);
$opts = $this->conn->data['LIST'][$folder];
}
return is_array($opts) ? $opts : array();
}
/**
* Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
* PERMANENTFLAGS, UIDNEXT, UNSEEN
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_data($folder)
{
if (!strlen($folder)) {
$folder = $this->folder !== null ? $this->folder : 'INBOX';
}
if ($this->conn->selected != $folder) {
if (!$this->check_connection()) {
return array();
}
if ($this->conn->select($folder)) {
$this->folder = $folder;
}
else {
return null;
}
}
$data = $this->conn->data;
// add (E)SEARCH result for ALL UNDELETED query
if (!empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$data['UNDELETED'] = $this->icache['undeleted_idx'];
}
return $data;
}
/**
* Returns extended information about the folder
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_info($folder)
{
if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
return $this->icache['options'];
}
// get cached metadata
$cache_key = 'mailboxes.folder-info.' . $folder;
$cached = $this->get_cache($cache_key);
if (is_array($cached)) {
return $cached;
}
$acl = $this->get_capability('ACL');
$namespace = $this->get_namespace();
$options = array();
// check if the folder is a namespace prefix
if (!empty($namespace)) {
$mbox = $folder . $this->delimiter;
foreach ($namespace as $ns) {
if (!empty($ns)) {
foreach ($ns as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break 2;
}
}
}
}
}
// check if the folder is other user virtual-root
if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
$parts = explode($this->delimiter, $folder);
if (count($parts) == 2) {
$mbox = $parts[0] . $this->delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break;
}
}
}
}
$options['name'] = $folder;
$options['attributes'] = $this->folder_attributes($folder, true);
$options['namespace'] = $this->folder_namespace($folder);
- $options['special'] = in_array($folder, $this->default_folders);
+ $options['special'] = $this->is_special_folder($folder);
// Set 'noselect' flag
if (is_array($options['attributes'])) {
foreach ($options['attributes'] as $attrib) {
$attrib = strtolower($attrib);
if ($attrib == '\noselect' || $attrib == '\nonexistent') {
$options['noselect'] = true;
}
}
}
else {
$options['noselect'] = true;
}
// Get folder rights (MYRIGHTS)
if ($acl && ($rights = $this->my_rights($folder))) {
$options['rights'] = $rights;
}
// Set 'norename' flag
if (!empty($options['rights'])) {
$options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
if (!$options['noselect']) {
$options['noselect'] = !in_array('r', $options['rights']);
}
}
else {
$options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
}
// update caches
$this->icache['options'] = $options;
$this->update_cache($cache_key, $options);
return $options;
}
/**
* Synchronizes messages cache.
*
* @param string $folder Folder name
*/
public function folder_sync($folder)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->synchronize($folder);
}
}
/**
* Get message header names for rcube_imap_generic::fetchHeader(s)
*
* @return string Space-separated list of header names
*/
protected function get_fetch_headers()
{
if (!empty($this->options['fetch_headers'])) {
$headers = explode(' ', $this->options['fetch_headers']);
}
else {
$headers = array();
}
if ($this->messages_caching || $this->options['all_headers']) {
$headers = array_merge($headers, $this->all_headers);
}
return $headers;
}
/* -----------------------------------------
* ACL and METADATA/ANNOTATEMORE methods
* ----------------------------------------*/
/**
* Changes the ACL on the specified folder (SETACL)
*
* @param string $folder Folder name
* @param string $user User name
* @param string $acl ACL string
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function set_acl($folder, $user, $acl)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.folder-info.' . $folder);
return $this->conn->setACL($folder, $user, $acl);
}
/**
* Removes any <identifier,rights> pair for the
* specified user from the ACL for the specified
* folder (DELETEACL)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function delete_acl($folder, $user)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
return $this->conn->deleteACL($folder, $user);
}
/**
* Returns the access control list for folder (GETACL)
*
* @param string $folder Folder name
*
* @return array User-rights array on success, NULL on error
* @since 0.5-beta
*/
public function get_acl($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->getACL($folder);
}
/**
* Returns information about what rights can be granted to the
* user (identifier) in the ACL for the folder (LISTRIGHTS)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return array List of user rights
* @since 0.5-beta
*/
public function list_rights($folder, $user)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->listRights($folder, $user);
}
/**
* Returns the set of rights that the current user has to
* folder (MYRIGHTS)
*
* @param string $folder Folder name
*
* @return array MYRIGHTS response on success, NULL on error
* @since 0.5-beta
*/
public function my_rights($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->myRights($folder);
}
/**
* Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function set_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->setMetadata($folder, $entries);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $entry => $value) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$entry] = array($ent, $attr, $value);
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function delete_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->deleteMetadata($folder, $entries);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $idx => $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$idx] = array($ent, $attr, NULL);
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
*
* @return array Metadata entry-value hash array on success, NULL on error
* @since 0.5-beta
*/
public function get_metadata($folder, $entries, $options=array())
{
$entries = (array)$entries;
// create cache key
// @TODO: this is the simplest solution, but we do the same with folders list
// maybe we should store data per-entry and merge on request
sort($options);
sort($entries);
$cache_key = 'mailboxes.metadata.' . $folder;
$cache_key .= '.' . md5(serialize($options).serialize($entries));
// get cached data
$cached_data = $this->get_cache($cache_key);
if (is_array($cached_data)) {
return $cached_data;
}
if (!$this->check_connection()) {
return null;
}
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
$res = $this->conn->getMetadata($folder, $entries, $options);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
$queries = array();
$res = array();
// Convert entry names
foreach ($entries as $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$queries[$attr][] = $ent;
}
// @TODO: Honor MAXSIZE and DEPTH options
foreach ($queries as $attrib => $entry) {
if ($result = $this->conn->getAnnotation($folder, $entry, $attrib)) {
- $res = array_merge_recursive($res, $result);
+ foreach ($result as $fldr => $data) {
+ $res[$fldr] = array_merge((array) $res[$fldr], $data);
+ }
}
}
}
if (isset($res)) {
$this->update_cache($cache_key, $res);
return $res;
}
return null;
}
/**
* Converts the METADATA extension entry name into the correct
* entry-attrib names for older ANNOTATEMORE version.
*
* @param string $entry Entry name
*
* @return array Entry-attribute list, NULL if not supported (?)
*/
protected function md2annotate($entry)
{
if (substr($entry, 0, 7) == '/shared') {
return array(substr($entry, 7), 'value.shared');
}
else if (substr($entry, 0, 8) == '/private') {
return array(substr($entry, 8), 'value.priv');
}
// @TODO: log error
return null;
}
/* --------------------------------
* internal caching methods
* --------------------------------*/
/**
* Enable or disable indexes caching
*
* @param string $type Cache type (@see rcube::get_cache)
*/
public function set_caching($type)
{
if ($type) {
$this->caching = $type;
}
else {
if ($this->cache) {
$this->cache->close();
}
$this->cache = null;
$this->caching = false;
}
}
/**
* Getter for IMAP cache object
*/
protected function get_cache_engine()
{
if ($this->caching && !$this->cache) {
$rcube = rcube::get_instance();
$ttl = $rcube->config->get('imap_cache_ttl', '10d');
$this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
}
return $this->cache;
}
/**
* Returns cached value
*
* @param string $key Cache key
*
* @return mixed
*/
public function get_cache($key)
{
if ($cache = $this->get_cache_engine()) {
return $cache->get($key);
}
}
/**
* Update cache
*
* @param string $key Cache key
* @param mixed $data Data
*/
public function update_cache($key, $data)
{
if ($cache = $this->get_cache_engine()) {
$cache->set($key, $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
*/
public function clear_cache($key = null, $prefix_mode = false)
{
if ($cache = $this->get_cache_engine()) {
$cache->remove($key, $prefix_mode);
}
}
/* --------------------------------
* message caching methods
* --------------------------------*/
/**
* Enable or disable messages caching
*
* @param boolean $set Flag
* @param int $mode Cache mode
*/
public function set_messages_caching($set, $mode = null)
{
if ($set) {
$this->messages_caching = true;
if ($mode && ($cache = $this->get_mcache_engine())) {
$cache->set_mode($mode);
}
}
else {
if ($this->mcache) {
$this->mcache->close();
}
$this->mcache = null;
$this->messages_caching = false;
}
}
/**
* Getter for messages cache object
*/
protected function get_mcache_engine()
{
if ($this->messages_caching && !$this->mcache) {
$rcube = rcube::get_instance();
if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
$ttl = $rcube->config->get('messages_cache_ttl', '10d');
$threshold = $rcube->config->get('messages_cache_threshold', 50);
$this->mcache = new rcube_imap_cache(
$dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
}
}
return $this->mcache;
}
/**
* Clears the messages cache.
*
* @param string $folder Folder name
* @param array $uids Optional message UIDs to remove from cache
*/
protected function clear_message_cache($folder = null, $uids = null)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->clear($folder, $uids);
}
}
/**
* Delete outdated cache entries
*/
function cache_gc()
{
rcube_imap_cache::gc();
}
/* --------------------------------
* protected methods
* --------------------------------*/
/**
* Validate the given input and save to local properties
*
* @param string $sort_field Sort column
* @param string $sort_order Sort order
*/
protected function set_sort_order($sort_field, $sort_order)
{
if ($sort_field != null) {
$this->sort_field = asciiwords($sort_field);
}
if ($sort_order != null) {
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
}
/**
* Sort folders first by default folders and then in alphabethical order
*
* @param array $a_folders Folders list
* @param bool $skip_default Skip default folders handling
*
* @return array Sorted list
*/
public function sort_folder_list($a_folders, $skip_default = false)
{
- $a_out = $a_defaults = $folders = array();
-
- $delimiter = $this->get_hierarchy_delimiter();
+ $specials = array_merge(array('INBOX'), array_values($this->get_special_folders()));
+ $folders = array();
- // find default folders and skip folders starting with '.'
+ // convert names to UTF-8 and skip folders starting with '.'
foreach ($a_folders as $folder) {
- if ($folder[0] == '.') {
- continue;
- }
-
- if (!$skip_default && ($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
- $a_defaults[$p] = $folder;
- }
- else {
- $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP');
+ if ($folder[0] != '.') {
+ // for better performance skip encoding conversion
+ // if the string does not look like UTF7-IMAP
+ $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
}
}
- // sort folders and place defaults on the top
- asort($folders, SORT_LOCALE_STRING);
- ksort($a_defaults);
- $folders = array_merge($a_defaults, array_keys($folders));
+ // sort folders
+ // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
+ uasort($folders, array($this, 'sort_folder_comparator'));
- // finally we must rebuild the list to move
- // subfolders of default folders to their place...
- // ...also do this for the rest of folders because
- // asort() is not properly sorting case sensitive names
- while (list($key, $folder) = each($folders)) {
- // set the type of folder name variable (#1485527)
- $a_out[] = (string) $folder;
- unset($folders[$key]);
- $this->rsort($folder, $delimiter, $folders, $a_out);
+ $folders = array_keys($folders);
+
+ if ($skip_default) {
+ return $folders;
}
- return $a_out;
- }
+ // force the type of folder name variable (#1485527)
+ $folders = array_map('strval', $folders);
+ $out = array();
+
+ // finally we must put special folders on top and rebuild the list
+ // to move their subfolders where they belong...
+ $specials = array_unique(array_intersect($specials, $folders));
+ $folders = array_merge($specials, array_diff($folders, $specials));
+ $this->sort_folder_specials(null, $folders, $specials, $out);
+
+ return $out;
+ }
/**
- * Recursive method for sorting folders
+ * Recursive function to put subfolders of special folders in place
*/
- protected function rsort($folder, $delimiter, &$list, &$out)
+ protected function sort_folder_specials($folder, &$list, &$specials, &$out)
{
while (list($key, $name) = each($list)) {
- if (strpos($name, $folder.$delimiter) === 0) {
- // set the type of folder name variable (#1485527)
- $out[] = (string) $name;
+ if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
+ $out[] = $name;
unset($list[$key]);
- $this->rsort($name, $delimiter, $list, $out);
+
+ if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
+ unset($specials[$found]);
+ $this->sort_folder_specials($name, $list, $specials, $out);
+ }
}
}
+
reset($list);
}
+ /**
+ * Callback for uasort() that implements correct
+ * locale-aware case-sensitive sorting
+ */
+ protected function sort_folder_comparator($str1, $str2)
+ {
+ $path1 = explode($this->delimiter, $str1);
+ $path2 = explode($this->delimiter, $str2);
+
+ foreach ($path1 as $idx => $folder1) {
+ $folder2 = $path2[$idx];
+
+ if ($folder1 === $folder2) {
+ continue;
+ }
+
+ return strcoll($folder1, $folder2);
+ }
+ }
+
/**
* Find UID of the specified message sequence ID
*
* @param int $id Message (sequence) ID
* @param string $folder Folder name
*
* @return int Message UID
*/
public function id2uid($id, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if ($uid = array_search($id, (array)$this->uid_id_map[$folder])) {
return $uid;
}
if (!$this->check_connection()) {
return null;
}
$uid = $this->conn->ID2UID($folder, $id);
$this->uid_id_map[$folder][$uid] = $id;
return $uid;
}
/**
* Subscribe/unsubscribe a list of folders and update local cache
*/
protected function change_subscription($folders, $mode)
{
$updated = false;
if (!empty($folders)) {
if (!$this->check_connection()) {
return false;
}
foreach ((array)$folders as $i => $folder) {
$folders[$i] = $folder;
if ($mode == 'subscribe') {
$updated = $this->conn->subscribe($folder);
}
else if ($mode == 'unsubscribe') {
$updated = $this->conn->unsubscribe($folder);
}
}
}
// clear cached folders list(s)
if ($updated) {
$this->clear_cache('mailboxes', true);
}
return $updated;
}
/**
* Increde/decrese messagecount for a specific folder
*/
protected function set_messagecount($folder, $mode, $increment)
{
if (!is_numeric($increment)) {
return false;
}
$mode = strtoupper($mode);
$a_folder_cache = $this->get_cache('messagecount');
if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
return false;
}
// add incremental value to messagecount
$a_folder_cache[$folder][$mode] += $increment;
// there's something wrong, delete from cache
if ($a_folder_cache[$folder][$mode] < 0) {
unset($a_folder_cache[$folder][$mode]);
}
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
return true;
}
/**
* Remove messagecount of a specific folder from cache
*/
protected function clear_messagecount($folder, $mode=null)
{
$a_folder_cache = $this->get_cache('messagecount');
if (is_array($a_folder_cache[$folder])) {
if ($mode) {
unset($a_folder_cache[$folder][$mode]);
}
else {
unset($a_folder_cache[$folder]);
}
$this->update_cache('messagecount', $a_folder_cache);
}
}
/**
* Converts date string/object into IMAP date/time format
*/
protected function date_format($date)
{
if (empty($date)) {
return null;
}
if (!is_object($date) || !is_a($date, 'DateTime')) {
try {
$timestamp = rcube_utils::strtotime($date);
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return null;
}
}
return $date->format('d-M-Y H:i:s O');
}
/**
* This is our own debug handler for the IMAP connection
* @access public
*/
public function debug_handler(&$imap, $message)
{
rcube::write_log('imap', $message);
}
/**
* Deprecated methods (to be removed)
*/
public function decode_address_list($input, $max = null, $decode = true, $fallback = null)
{
return rcube_mime::decode_address_list($input, $max, $decode, $fallback);
}
public function decode_header($input, $fallback = null)
{
return rcube_mime::decode_mime_string((string)$input, $fallback);
}
public static function decode_mime_string($input, $fallback = null)
{
return rcube_mime::decode_mime_string($input, $fallback);
}
public function mime_decode($input, $encoding = '7bit')
{
return rcube_mime::decode($input, $encoding);
}
public static function explode_header_string($separator, $str, $remove_comments = false)
{
return rcube_mime::explode_header_string($separator, $str, $remove_comments);
}
public function select_mailbox($mailbox)
{
// do nothing
}
public function set_mailbox($folder)
{
$this->set_folder($folder);
}
public function get_mailbox_name()
{
return $this->get_folder();
}
public function list_headers($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
return $this->list_messages($folder, $page, $sort_field, $sort_order, $slice);
}
public function get_headers($uid, $folder = null, $force = false)
{
return $this->get_message_headers($uid, $folder, $force);
}
public function mailbox_status($folder = null)
{
return $this->folder_status($folder);
}
public function message_index($folder = '', $sort_field = NULL, $sort_order = NULL)
{
return $this->index($folder, $sort_field, $sort_order);
}
public function message_index_direct($folder, $sort_field = null, $sort_order = null)
{
return $this->index_direct($folder, $sort_field, $sort_order);
}
public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
{
return $this->list_folders_subscribed($root, $name, $filter, $rights, $skip_sort);
}
public function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
{
return $this->list_folders($root, $name, $filter, $rights, $skip_sort);
}
public function get_mailbox_size($folder)
{
return $this->folder_size($folder);
}
public function create_mailbox($folder, $subscribe=false)
{
return $this->create_folder($folder, $subscribe);
}
public function rename_mailbox($folder, $new_name)
{
return $this->rename_folder($folder, $new_name);
}
function delete_mailbox($folder)
{
return $this->delete_folder($folder);
}
function clear_mailbox($folder = null)
{
return $this->clear_folder($folder);
}
public function mailbox_exists($folder, $subscription=false)
{
return $this->folder_exists($folder, $subscription);
}
public function mailbox_namespace($folder)
{
return $this->folder_namespace($folder);
}
public function mod_mailbox($folder, $mode = 'out')
{
return $this->mod_folder($folder, $mode);
}
public function mailbox_attributes($folder, $force=false)
{
return $this->folder_attributes($folder, $force);
}
public function mailbox_data($folder)
{
return $this->folder_data($folder);
}
public function mailbox_info($folder)
{
return $this->folder_info($folder);
}
public function mailbox_sync($folder)
{
return $this->folder_sync($folder);
}
public function expunge($folder='', $clear_cache=true)
{
return $this->expunge_folder($folder, $clear_cache);
}
}
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index e49e778..81df076 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -1,1309 +1,1321 @@
<?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: |
| 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 Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_imap_cache
{
const MODE_INDEX = 1;
const MODE_MESSAGE = 2;
/**
* Instance of rcube_imap
*
* @var rcube_imap
*/
private $imap;
/**
* Instance of rcube_db
*
* @var rcube_db
*/
private $db;
/**
* User ID
*
* @var int
*/
private $userid;
/**
* Expiration time in seconds
*
* @var int
*/
private $ttl;
/**
* Maximum cached message size
*
* @var int
*/
private $threshold;
/**
* Internal (in-memory) cache
*
* @var array
*/
private $icache = array();
private $skip_deleted = false;
private $mode;
/**
* 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.
*
* @param rcube_db $db DB handler
* @param rcube_imap $imap IMAP handler
* @param int $userid User identifier
* @param bool $skip_deleted skip_deleted flag
* @param string $ttl Expiration time of memcache/apc items
* @param int $threshold Maximum cached message size
*/
function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
{
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->db = $db;
$this->imap = $imap;
$this->userid = $userid;
$this->skip_deleted = $skip_deleted;
$this->ttl = $ttl;
$this->threshold = $threshold;
// cache all possible information by default
$this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
+
+ // database tables
+ $this->index_table = $db->table_name('cache_index', true);
+ $this->thread_table = $db->table_name('cache_thread', true);
+ $this->messages_table = $db->table_name('cache_messages', true);
}
/**
* Cleanup actions (on shutdown).
*/
public function close()
{
$this->save_icache();
$this->icache = null;
}
/**
* Set cache mode
*
* @param int $mode Cache mode
*/
public function set_mode($mode)
{
$this->mode = $mode;
}
/**
* Return (sorted) messages index (UIDs).
* If index doesn't exist or is invalid, will be updated.
*
* @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 (empty($this->icache[$mailbox]['index']['validated'])) {
$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(
'validated' => true,
'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
*
* @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);
// Get THREADS result
$index['object'] = $this->get_thread_data($mailbox, $mbox_data);
// insert/update
$this->add_thread_row($mailbox, $index['object'], $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();
}
$result = array();
if ($this->mode & self::MODE_MESSAGE) {
// Fetch messages from cache
$sql_result = $this->db->query(
- "SELECT uid, data, flags"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
+ "SELECT `uid`, `data`, `flags`"
+ ." FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` IN (".$this->db->array2list($msgs, 'integer').")",
$this->userid, $mailbox);
$msgs = array_flip($msgs);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$uid = intval($sql_arr['uid']);
$result[$uid] = $this->build_message($sql_arr);
if (!empty($result[$uid])) {
// save memory, we don't need message body here (?)
$result[$uid]->body = null;
unset($msgs[$uid]);
}
}
$msgs = array_flip($msgs);
}
// Fetch not found messages from IMAP server
if (!empty($msgs)) {
$messages = $this->imap->fetch_headers($mailbox, $msgs, false, true);
// Insert to DB and add to result list
if (!empty($messages)) {
foreach ($messages as $msg) {
if ($this->mode & self::MODE_MESSAGE) {
$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'];
}
if ($this->mode & self::MODE_MESSAGE) {
$sql_result = $this->db->query(
- "SELECT flags, data"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
+ "SELECT `flags`, `data`"
+ ." FROM {$this->messages_table}"
+ ." 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
}
if (!($this->mode & self::MODE_MESSAGE)) {
return $message;
}
// Save the message in internal cache, will be written to DB in close()
// Common scenario: user opens unseen message
// - get message (SELECT)
// - 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;
}
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
$flags = 0;
$msg = clone $message;
if (!empty($message->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($message->flags[$flag])) {
$flags += $idx;
}
}
}
unset($msg->flags);
$msg = $this->db->encode($msg, true);
// update cache record (even if it exists, the update
// here will work as select, assume row exist if affected_rows=0)
if (!$force) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET flags = ?, data = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
+ "UPDATE {$this->messages_table}"
+ ." SET `flags` = ?, `data` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
// insert new record
$res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_messages')
- ." (user_id, mailbox, uid, flags, expires, data)"
+ "INSERT INTO {$this->messages_table}"
+ ." (`user_id`, `mailbox`, `uid`, `flags`, `expires`, `data`)"
." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
$this->userid, $mailbox, (int) $message->uid, $flags, $msg);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if ($force && !$res && !$this->db->is_error($res)) {
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- .", flags = ?, data = ?"
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?",
+ "UPDATE {$this->messages_table}"
+ ." SET `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ .", `flags` = ?, `data` = ?"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ ." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* 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)
{
if (empty($uids)) {
return;
}
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
$flag = strtoupper($flag);
$idx = (int) array_search($flag, $this->flags);
$uids = (array) $uids;
if (!$idx) {
return;
}
// Internal cache update
if (($message = $this->icache['__message'])
&& $message['mailbox'] === $mailbox
&& in_array($message['object']->uid, $uids)
) {
$message['object']->flags[$flag] = $enabled;
if (count($uids) == 1) {
return;
}
}
+ $binary_check = $this->db->db_provider == 'oracle' ? "BITAND(`flags`, %d)" : "(`flags` & %d)";
+
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_messages')
- ." SET expires = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "")
- ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
+ "UPDATE {$this->messages_table}"
+ ." SET `expires` = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ .", `flags` = `flags` ".($enabled ? "+ $idx" : "- $idx")
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?"
+ .(!empty($uids) ? " AND `uid` IN (".$this->db->array2list($uids, 'integer').")" : "")
+ ." AND " . sprintf($binary_check, $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 (!($this->mode & self::MODE_MESSAGE)) {
return;
}
if (!strlen($mailbox)) {
$this->db->query(
- "DELETE FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?",
+ "DELETE FROM {$this->messages_table}"
+ ." WHERE `user_id` = ?",
$this->userid);
}
else {
// Remove the message from internal cache
if (!empty($uids) && ($message = $this->icache['__message'])
&& $message['mailbox'] === $mailbox
&& in_array($message['object']->uid, (array)$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').")" : ""),
+ "DELETE FROM {$this->messages_table}"
+ ." 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) : ""),
+ "DELETE FROM {$this->index_table}"
+ ." 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) : ""),
+ "UPDATE {$this->index_table}"
+ ." 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) : ""),
+ "DELETE FROM {$this->thread_table}"
+ ." 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 expired cache entries
*/
static function gc()
{
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
+ $now = $db->now();
- $db->query("DELETE FROM ".$db->table_name('cache_messages')
- ." WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache_messages', true)
+ ." WHERE `expires` < $now");
- $db->query("DELETE FROM ".$db->table_name('cache_index')
- ." WHERE expires < " . $db->now());
+ $db->query("DELETE FROM " . $db->table_name('cache_index', true)
+ ." WHERE `expires` < $now");
- $db->query("DELETE FROM ".$db->table_name('cache_thread')
- ." WHERE expires < " . $db->now());
+ $db->query("DELETE FROM ".$db->table_name('cache_thread', true)
+ ." WHERE `expires` < $now");
}
/**
* 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 = ?",
+ "SELECT `data`, `valid`"
+ ." FROM {$this->index_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$index = $this->db->decode($data[0], true);
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 = ?",
+ "SELECT `data`"
+ ." FROM {$this->thread_table}"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$thread = $this->db->decode($data[0], true);
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(
$this->db->encode($data, true),
$sort_field,
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
$modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
);
- $data = implode('@', $data);
+
+ $data = implode('@', $data);
+ $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_index')
- ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->index_table}"
+ ." SET `data` = ?, `valid` = 1, `expires` = $expires"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_index')
- ." (user_id, mailbox, valid, expires, data)"
- ." VALUES (?, ?, 1, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .", ?)",
+ "INSERT INTO {$this->index_table}"
+ ." (`user_id`, `mailbox`, `valid`, `expires`, `data`)"
+ ." VALUES (?, ?, 1, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_index')
- ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->index_table}"
+ ." SET `data` = ?, `valid` = 1, `expires` = $expires"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Saves thread data into database
*/
private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
{
$data = array(
$this->db->encode($data, true),
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
);
- $data = implode('@', $data);
- $expires = ($this->ttl ? $this->db->now($this->ttl) : 'NULL');
+ $data = implode('@', $data);
+ $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
- "UPDATE ".$this->db->table_name('cache_thread')
- ." SET data = ?, expires = $expires"
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->thread_table}"
+ ." SET `data` = ?, `expires` = $expires"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
- "INSERT INTO ".$this->db->table_name('cache_thread')
- ." (user_id, mailbox, expires, data)"
+ "INSERT INTO {$this->thread_table}"
+ ." (`user_id`, `mailbox`, `expires`, `data`)"
." VALUES (?, ?, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$this->db->query(
- "UPDATE ".$this->db->table_name('cache_thread')
- ." SET expires = $expires, data = ?"
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "UPDATE {$this->thread_table}"
+ ." SET `expires` = $expires, `data` = ?"
+ ." WHERE `user_id` = ?"
+ ." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Checks index/thread validity
*/
private function validate($mailbox, $index, &$exists = true)
{
$object = $index['object'];
$is_thread = is_a($object, 'rcube_result_thread');
// sanity check
if (empty($object)) {
return false;
}
$index['validated'] = true;
// 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
if ($this->mode & self::MODE_MESSAGE) {
$sql_result = $this->db->query(
- "SELECT uid"
- ." FROM ".$this->db->table_name('cache_messages')
- ." WHERE user_id = ?"
- ." AND mailbox = ?",
+ "SELECT `uid`"
+ ." FROM {$this->messages_table}"
+ ." 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 $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 = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
- ." WHERE user_id = ?"
- ." AND mailbox = ?"
- ." AND uid = ?"
- ." AND flags <> ?",
+ "UPDATE {$this->messages_table}"
+ ." SET `flags` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+ ." 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);
}
}
$sort_field = $index['sort_field'];
$sort_order = $index['object']->get_parameters('ORDER');
$exists = true;
// Validate index
if (!$this->validate($mailbox, $index, $exists)) {
// Invalidate (remove) thread index
// if $exists=false it was already removed in validate()
if ($exists) {
$this->remove_thread($mailbox);
}
// Update index
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
}
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($sql_arr['data'], true);
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
$this->message_object_prepare($message['object']);
// calculate current md5 sum
$md5sum = md5(serialize($message['object']));
if ($message['md5sum'] != $md5sum) {
$this->add_message($message['mailbox'], $message['object'], !$message['exists']);
}
$this->icache['__message']['md5sum'] = $md5sum;
}
}
/**
* Prepares message object to be stored in database.
*
* @param rcube_message_header|rcube_message_part
*/
private function message_object_prepare(&$msg, &$size = 0)
{
// Remove body too big
- if ($msg->body && ($length = strlen($msg->body))) {
- $size += $length;
+ if (isset($msg->body)) {
+ $length = strlen($msg->body);
- if ($size > $this->threshold * 1024) {
- $size -= $length;
+ if ($msg->body_modified || $size + $length > $this->threshold * 1024) {
unset($msg->body);
}
+ else {
+ $size += $length;
+ }
}
// 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);
}
unset($msg->replaces);
if (is_object($msg->structure)) {
$this->message_object_prepare($msg->structure, $size);
}
if (is_array($msg->parts)) {
foreach ($msg->parts as $part) {
$this->message_object_prepare($part, $size);
}
}
}
/**
* 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;
}
/**
* Fetches thread data from IMAP server
*/
private function get_thread_data($mailbox, $mbox_data = array())
{
if (empty($mbox_data)) {
$mbox_data = $this->imap->folder_data($mailbox);
}
if ($mbox_data['EXISTS']) {
// get all threads (default sort order)
return $this->imap->threads_direct($mailbox);
}
return new rcube_result_thread($mailbox, '* THREAD');
}
}
// for backward compat.
class rcube_mail_header extends rcube_message_header { }
diff --git a/lib/ext/Roundcube/rcube_imap_generic.php b/lib/ext/Roundcube/rcube_imap_generic.php
index f465ac1..d78b526 100644
--- a/lib/ext/Roundcube/rcube_imap_generic.php
+++ b/lib/ext/Roundcube/rcube_imap_generic.php
@@ -1,3835 +1,3925 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-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: |
| Provide alternative IMAP library that doesn't rely on the standard |
| C-Client based version. This allows to function regardless |
| of whether or not the PHP build it's running on has IMAP |
| functionality built-in. |
| |
| Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Ryo Chijiiwa <Ryo@IlohaMail.org> |
+-----------------------------------------------------------------------+
*/
/**
* PHP based wrapper class to connect to an IMAP server
*
* @package Framework
* @subpackage Storage
*/
class rcube_imap_generic
{
public $error;
public $errornum;
public $result;
public $resultcode;
public $selected;
public $data = array();
public $flags = array(
'SEEN' => '\\Seen',
'DELETED' => '\\Deleted',
'ANSWERED' => '\\Answered',
'DRAFT' => '\\Draft',
'FLAGGED' => '\\Flagged',
'FORWARDED' => '$Forwarded',
'MDNSENT' => '$MDNSent',
'*' => '\\*',
);
public static $mupdate;
- private $fp;
- private $host;
- private $logged = false;
- private $capability = array();
- private $capability_readed = false;
- private $prefs;
- private $cmd_tag;
- private $cmd_num = 0;
- private $resourceid;
- private $_debug = false;
- private $_debug_handler = false;
+ protected $fp;
+ protected $host;
+ protected $logged = false;
+ protected $capability = array();
+ protected $capability_readed = false;
+ protected $prefs;
+ protected $cmd_tag;
+ protected $cmd_num = 0;
+ protected $resourceid;
+ protected $_debug = false;
+ protected $_debug_handler = false;
const ERROR_OK = 0;
const ERROR_NO = -1;
const ERROR_BAD = -2;
const ERROR_BYE = -3;
const ERROR_UNKNOWN = -4;
const ERROR_COMMAND = -5;
const ERROR_READONLY = -6;
const COMMAND_NORESPONSE = 1;
const COMMAND_CAPABILITY = 2;
const COMMAND_LASTLINE = 4;
const COMMAND_ANONYMIZED = 8;
const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
/**
* Object constructor
*/
function __construct()
{
}
/**
* Send simple (one line) command to the connection stream
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
* @param bool $anonymized Don't write the given data to log but a placeholder
*
* @param int Number of bytes sent, False on error
*/
function putLine($string, $endln=true, $anonymized=false)
{
if (!$this->fp)
return false;
if ($this->_debug) {
// anonymize the sent command for logging
$cut = $endln ? 2 : 0;
if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
$log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
}
else if ($anonymized) {
$log = sprintf('****** [%d]', strlen($string) - $cut);
}
else {
$log = rtrim($string);
}
$this->debug('C: ' . $log);
}
$res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
if ($res === false) {
@fclose($this->fp);
$this->fp = null;
}
return $res;
}
/**
* Send command to the connection stream with Command Continuation
* Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
* @param bool $anonymized Don't write the given data to log but a placeholder
*
* @return int|bool Number of bytes sent, False on error
*/
function putLineC($string, $endln=true, $anonymized=false)
{
if (!$this->fp) {
return false;
}
if ($endln) {
$string .= "\r\n";
}
$res = 0;
if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) {
// LITERAL+ support
if ($this->prefs['literal+']) {
$parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
}
$bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
if ($bytes === false)
return false;
$res += $bytes;
// don't wait if server supports LITERAL+ capability
if (!$this->prefs['literal+']) {
$line = $this->readLine(1000);
// handle error in command
if ($line[0] != '+')
return false;
}
$i++;
}
else {
$bytes = $this->putLine($parts[$i], false, $anonymized);
if ($bytes === false)
return false;
$res += $bytes;
}
}
}
return $res;
}
/**
* Reads line from the connection stream
*
* @param int $size Buffer size
*
* @return string Line of text response
*/
function readLine($size=1024)
{
$line = '';
if (!$size) {
$size = 1024;
}
do {
if ($this->eof()) {
return $line ? $line : NULL;
}
$buffer = fgets($this->fp, $size);
if ($buffer === false) {
$this->closeSocket();
break;
}
if ($this->_debug) {
$this->debug('S: '. rtrim($buffer));
}
$line .= $buffer;
} while (substr($buffer, -1) != "\n");
return $line;
}
/**
* Reads more data from the connection stream when provided
* data contain string literal
*
* @param string $line Response text
* @param bool $escape Enables escaping
*
* @return string Line of text response
*/
function multLine($line, $escape = false)
{
$line = rtrim($line);
if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
$out = '';
$str = substr($line, 0, -strlen($m[0]));
$bytes = $m[1];
while (strlen($out) < $bytes) {
$line = $this->readBytes($bytes);
if ($line === NULL)
break;
$out .= $line;
}
$line = $str . ($escape ? $this->escape($out) : $out);
}
return $line;
}
/**
* Reads specified number of bytes from the connection stream
*
* @param int $bytes Number of bytes to get
*
* @return string Response text
*/
function readBytes($bytes)
{
$data = '';
$len = 0;
while ($len < $bytes && !$this->eof())
{
$d = fread($this->fp, $bytes-$len);
if ($this->_debug) {
$this->debug('S: '. $d);
}
$data .= $d;
$data_len = strlen($data);
if ($len == $data_len) {
break; // nothing was read -> exit to avoid apache lockups
}
$len = $data_len;
}
return $data;
}
/**
* Reads complete response to the IMAP command
*
* @param array $untagged Will be filled with untagged response lines
*
* @return string Response text
*/
function readReply(&$untagged=null)
{
do {
$line = trim($this->readLine(1024));
// store untagged response lines
if ($line[0] == '*')
$untagged[] = $line;
} while ($line[0] == '*');
if ($untagged)
$untagged = join("\n", $untagged);
return $line;
}
/**
* Response parser.
*
* @param string $string Response text
* @param string $err_prefix Error message prefix
*
* @return int Response status
*/
function parseResult($string, $err_prefix='')
{
if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
$res = strtoupper($matches[1]);
$str = trim($matches[2]);
if ($res == 'OK') {
$this->errornum = self::ERROR_OK;
} else if ($res == 'NO') {
$this->errornum = self::ERROR_NO;
} else if ($res == 'BAD') {
$this->errornum = self::ERROR_BAD;
} else if ($res == 'BYE') {
$this->closeSocket();
$this->errornum = self::ERROR_BYE;
}
if ($str) {
$str = trim($str);
// get response string and code (RFC5530)
if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
$this->resultcode = strtoupper($m[1]);
$str = trim(substr($str, strlen($m[1]) + 2));
}
else {
$this->resultcode = null;
// parse response for [APPENDUID 1204196876 3456]
if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) {
$this->data['APPENDUID'] = $m[1];
}
// parse response for [COPYUID 1204196876 3456:3457 123:124]
else if (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) {
$this->data['COPYUID'] = array($m[1], $m[2]);
}
}
$this->result = $str;
if ($this->errornum != self::ERROR_OK) {
$this->error = $err_prefix ? $err_prefix.$str : $str;
}
}
return $this->errornum;
}
return self::ERROR_UNKNOWN;
}
/**
* Checks connection stream state.
*
* @return bool True if connection is closed
*/
- private function eof()
+ protected function eof()
{
if (!is_resource($this->fp)) {
return true;
}
// If a connection opened by fsockopen() wasn't closed
// by the server, feof() will hang.
$start = microtime(true);
if (feof($this->fp) ||
($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
) {
$this->closeSocket();
return true;
}
return false;
}
/**
* Closes connection stream.
*/
- private function closeSocket()
+ protected function closeSocket()
{
@fclose($this->fp);
$this->fp = null;
}
/**
* Error code/message setter.
*/
function setError($code, $msg='')
{
$this->errornum = $code;
$this->error = $msg;
}
/**
* Checks response status.
* Checks if command response line starts with specified prefix (or * BYE/BAD)
*
* @param string $string Response text
* @param string $match Prefix to match with (case-sensitive)
* @param bool $error Enables BYE/BAD checking
* @param bool $nonempty Enables empty response checking
*
* @return bool True any check is true or connection is closed.
*/
function startsWith($string, $match, $error=false, $nonempty=false)
{
if (!$this->fp) {
return true;
}
if (strncmp($string, $match, strlen($match)) == 0) {
return true;
}
if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
if (strtoupper($m[1]) == 'BYE') {
$this->closeSocket();
}
return true;
}
if ($nonempty && !strlen($string)) {
return true;
}
return false;
}
- private function hasCapability($name)
+ protected function hasCapability($name)
{
if (empty($this->capability) || $name == '') {
return false;
}
if (in_array($name, $this->capability)) {
return true;
}
else if (strpos($name, '=')) {
return false;
}
$result = array();
foreach ($this->capability as $cap) {
$entry = explode('=', $cap);
if ($entry[0] == $name) {
$result[] = $entry[1];
}
}
return !empty($result) ? $result : false;
}
/**
* Capabilities checker
*
* @param string $name Capability name
*
* @return mixed Capability values array for key=value pairs, true/false for others
*/
function getCapability($name)
{
$result = $this->hasCapability($name);
if (!empty($result)) {
return $result;
}
else if ($this->capability_readed) {
return false;
}
// get capabilities (only once) because initial
// optional CAPABILITY response may differ
$result = $this->execute('CAPABILITY');
if ($result[0] == self::ERROR_OK) {
$this->parseCapability($result[1]);
}
$this->capability_readed = true;
return $this->hasCapability($name);
}
function clearCapability()
{
$this->capability = array();
$this->capability_readed = false;
}
/**
* DIGEST-MD5/CRAM-MD5/PLAIN Authentication
*
* @param string $user
* @param string $pass
* @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
*
* @return resource Connection resourse on success, error code on error
*/
function authenticate($user, $pass, $type='PLAIN')
{
if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
$this->setError(self::ERROR_BYE,
"The Auth_SASL package is required for DIGEST-MD5 authentication");
return self::ERROR_BAD;
}
$this->putLine($this->nextTag() . " AUTHENTICATE $type");
$line = trim($this->readReply());
if ($line[0] == '+') {
$challenge = substr($line, 2);
}
else {
return $this->parseResult($line);
}
if ($type == 'CRAM-MD5') {
// RFC2195: CRAM-MD5
$ipad = '';
$opad = '';
// initialize ipad, opad
for ($i=0; $i<64; $i++) {
$ipad .= chr(0x36);
$opad .= chr(0x5C);
}
// pad $pass so it's 64 bytes
$padLen = 64 - strlen($pass);
for ($i=0; $i<$padLen; $i++) {
$pass .= chr(0);
}
// generate hash
$hash = md5($this->_xor($pass, $opad) . pack("H*",
md5($this->_xor($pass, $ipad) . base64_decode($challenge))));
$reply = base64_encode($user . ' ' . $hash);
// send result
$this->putLine($reply, true, true);
}
else {
// RFC2831: DIGEST-MD5
// proxy authorization
if (!empty($this->prefs['auth_cid'])) {
$authc = $this->prefs['auth_cid'];
$pass = $this->prefs['auth_pw'];
}
else {
$authc = $user;
$user = '';
}
$auth_sasl = Auth_SASL::factory('digestmd5');
$reply = base64_encode($auth_sasl->getResponse($authc, $pass,
base64_decode($challenge), $this->host, 'imap', $user));
// send result
$this->putLine($reply, true, true);
$line = trim($this->readReply());
if ($line[0] == '+') {
$challenge = substr($line, 2);
}
else {
return $this->parseResult($line);
}
// check response
$challenge = base64_decode($challenge);
if (strpos($challenge, 'rspauth=') === false) {
$this->setError(self::ERROR_BAD,
"Unexpected response from server to DIGEST-MD5 response");
return self::ERROR_BAD;
}
$this->putLine('');
}
$line = $this->readReply();
$result = $this->parseResult($line);
}
else { // PLAIN
// proxy authorization
if (!empty($this->prefs['auth_cid'])) {
$authc = $this->prefs['auth_cid'];
$pass = $this->prefs['auth_pw'];
}
else {
$authc = $user;
$user = '';
}
$reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
// RFC 4959 (SASL-IR): save one round trip
if ($this->getCapability('SASL-IR')) {
list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
}
else {
$this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
$line = trim($this->readReply());
if ($line[0] != '+') {
return $this->parseResult($line);
}
// send result, get reply and process it
$this->putLine($reply, true, true);
$line = $this->readReply();
$result = $this->parseResult($line);
}
}
if ($result == self::ERROR_OK) {
// optional CAPABILITY response
if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
$this->parseCapability($matches[1], true);
}
return $this->fp;
}
else {
$this->setError($result, "AUTHENTICATE $type: $line");
}
return $result;
}
/**
* LOGIN Authentication
*
* @param string $user
* @param string $pass
*
* @return resource Connection resourse on success, error code on error
*/
function login($user, $password)
{
list($code, $response) = $this->execute('LOGIN', array(
$this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
// re-set capabilities list if untagged CAPABILITY response provided
if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
$this->parseCapability($matches[1], true);
}
if ($code == self::ERROR_OK) {
return $this->fp;
}
return $code;
}
/**
* Detects hierarchy delimiter
*
* @return string The delimiter
*/
function getHierarchyDelimiter()
{
if ($this->prefs['delimiter']) {
return $this->prefs['delimiter'];
}
// try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
list($code, $response) = $this->execute('LIST',
array($this->escape(''), $this->escape('')));
if ($code == self::ERROR_OK) {
$args = $this->tokenizeResponse($response, 4);
$delimiter = $args[3];
if (strlen($delimiter) > 0) {
return ($this->prefs['delimiter'] = $delimiter);
}
}
return NULL;
}
/**
* NAMESPACE handler (RFC 2342)
*
* @return array Namespace data hash (personal, other, shared)
*/
function getNamespace()
{
if (array_key_exists('namespace', $this->prefs)) {
return $this->prefs['namespace'];
}
if (!$this->getCapability('NAMESPACE')) {
return self::ERROR_BAD;
}
list($code, $response) = $this->execute('NAMESPACE');
if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
$data = $this->tokenizeResponse(substr($response, 11));
}
if (!is_array($data)) {
return $code;
}
$this->prefs['namespace'] = array(
'personal' => $data[0],
'other' => $data[1],
'shared' => $data[2],
);
return $this->prefs['namespace'];
}
/**
* Connects to IMAP server and authenticates.
*
* @param string $host Server hostname or IP
* @param string $user User name
* @param string $password Password
* @param array $options Connection and class options
*
* @return bool True on success, False on failure
*/
function connect($host, $user, $password, $options=null)
{
// configure
$this->set_prefs($options);
- $auth_method = $this->prefs['auth_type'];
- $result = false;
-
- // initialize connection
- $this->error = '';
- $this->errornum = self::ERROR_OK;
- $this->selected = null;
- $this->user = $user;
$this->host = $host;
+ $this->user = $user;
$this->logged = false;
+ $this->selected = null;
// check input
if (empty($host)) {
$this->setError(self::ERROR_BAD, "Empty host");
return false;
}
+
if (empty($user)) {
$this->setError(self::ERROR_NO, "Empty user");
return false;
}
+
if (empty($password)) {
$this->setError(self::ERROR_NO, "Empty password");
return false;
}
- if (!$this->prefs['port']) {
- $this->prefs['port'] = 143;
- }
- // check for SSL
- if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
- $host = $this->prefs['ssl_mode'] . '://' . $host;
- }
-
- if ($this->prefs['timeout'] <= 0) {
- $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
- }
-
// Connect
- $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
-
- if (!$this->fp) {
- if (!$errstr) {
- $errstr = "Unknown reason (fsockopen() function disabled?)";
- }
- $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
- return false;
- }
-
- if ($this->prefs['timeout'] > 0) {
- stream_set_timeout($this->fp, $this->prefs['timeout']);
- }
-
- $line = trim(fgets($this->fp, 8192));
-
- if ($this->_debug) {
- // set connection identifier for debug output
- preg_match('/#([0-9]+)/', (string)$this->fp, $m);
- $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
-
- if ($line)
- $this->debug('S: '. $line);
- }
-
- // Connected to wrong port or connection error?
- if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
- if ($line)
- $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
- else
- $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
-
- $this->setError(self::ERROR_BAD, $error);
- $this->closeConnection();
+ if (!$this->_connect($host)) {
return false;
}
- // RFC3501 [7.1] optional CAPABILITY response
- if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
- $this->parseCapability($matches[1], true);
- }
-
- // TLS connection
- if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
- $res = $this->execute('STARTTLS');
-
- if ($res[0] != self::ERROR_OK) {
- $this->closeConnection();
- return false;
- }
-
- if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
- $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
- $this->closeConnection();
- return false;
- }
-
- // Now we're secure, capabilities need to be reread
- $this->clearCapability();
- }
-
// Send ID info
if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
$this->id($this->prefs['ident']);
}
+ $auth_method = $this->prefs['auth_type'];
$auth_methods = array();
$result = null;
// check for supported auth methods
if ($auth_method == 'CHECK') {
if ($auth_caps = $this->getCapability('AUTH')) {
$auth_methods = $auth_caps;
}
// RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
$login_disabled = $this->getCapability('LOGINDISABLED');
if (($key = array_search('LOGIN', $auth_methods)) !== false) {
if ($login_disabled) {
unset($auth_methods[$key]);
}
}
else if (!$login_disabled) {
$auth_methods[] = 'LOGIN';
}
// Use best (for security) supported authentication method
foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
if (in_array($auth_method, $auth_methods)) {
break;
}
}
}
else {
// Prevent from sending credentials in plain text when connection is not secure
if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
$this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
$this->closeConnection();
return false;
}
// replace AUTH with CRAM-MD5 for backward compat.
if ($auth_method == 'AUTH') {
$auth_method = 'CRAM-MD5';
}
}
// pre-login capabilities can be not complete
$this->capability_readed = false;
// Authenticate
switch ($auth_method) {
case 'CRAM_MD5':
$auth_method = 'CRAM-MD5';
case 'CRAM-MD5':
case 'DIGEST-MD5':
case 'PLAIN':
$result = $this->authenticate($user, $password, $auth_method);
break;
case 'LOGIN':
$result = $this->login($user, $password);
break;
default:
$this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
}
// Connected and authenticated
if (is_resource($result)) {
if ($this->prefs['force_caps']) {
$this->clearCapability();
}
$this->logged = true;
return true;
}
$this->closeConnection();
return false;
}
+ /**
+ * Connects to IMAP server.
+ *
+ * @param string $host Server hostname or IP
+ *
+ * @return bool True on success, False on failure
+ */
+ protected function _connect($host)
+ {
+ // initialize connection
+ $this->error = '';
+ $this->errornum = self::ERROR_OK;
+
+ if (!$this->prefs['port']) {
+ $this->prefs['port'] = 143;
+ }
+
+ // check for SSL
+ if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
+ $host = $this->prefs['ssl_mode'] . '://' . $host;
+ }
+
+ if ($this->prefs['timeout'] <= 0) {
+ $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
+ }
+
+ if (!empty($this->prefs['socket_options'])) {
+ $context = stream_context_create($this->prefs['socket_options']);
+ $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr,
+ $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context);
+ }
+ else {
+ $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
+ }
+
+ if (!$this->fp) {
+ $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s",
+ $host, $this->prefs['port'], $errstr ?: "Unknown reason"));
+
+ return false;
+ }
+
+ if ($this->prefs['timeout'] > 0) {
+ stream_set_timeout($this->fp, $this->prefs['timeout']);
+ }
+
+ $line = trim(fgets($this->fp, 8192));
+
+ if ($this->_debug) {
+ // set connection identifier for debug output
+ preg_match('/#([0-9]+)/', (string) $this->fp, $m);
+ $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
+
+ if ($line) {
+ $this->debug('S: '. $line);
+ }
+ }
+
+ // Connected to wrong port or connection error?
+ if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
+ if ($line)
+ $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
+ else
+ $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
+
+ $this->setError(self::ERROR_BAD, $error);
+ $this->closeConnection();
+ return false;
+ }
+
+ // RFC3501 [7.1] optional CAPABILITY response
+ if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
+ $this->parseCapability($matches[1], true);
+ }
+
+ // TLS connection
+ if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
+ $res = $this->execute('STARTTLS');
+
+ if ($res[0] != self::ERROR_OK) {
+ $this->closeConnection();
+ return false;
+ }
+
+ if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
+ $this->closeConnection();
+ return false;
+ }
+
+ // Now we're secure, capabilities need to be reread
+ $this->clearCapability();
+ }
+
+ return true;
+ }
+
/**
* Initializes environment
*/
protected function set_prefs($prefs)
{
// set preferences
if (is_array($prefs)) {
$this->prefs = $prefs;
}
// set auth method
if (!empty($this->prefs['auth_type'])) {
$this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
}
else {
$this->prefs['auth_type'] = 'CHECK';
}
// disabled capabilities
if (!empty($this->prefs['disabled_caps'])) {
$this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
}
// additional message flags
if (!empty($this->prefs['message_flags'])) {
$this->flags = array_merge($this->flags, $this->prefs['message_flags']);
unset($this->prefs['message_flags']);
}
}
/**
* Checks connection status
*
* @return bool True if connection is active and user is logged in, False otherwise.
*/
function connected()
{
return ($this->fp && $this->logged) ? true : false;
}
/**
* Closes connection with logout.
*/
function closeConnection()
{
if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) {
$this->readReply();
}
$this->closeSocket();
}
/**
* Executes SELECT command (if mailbox is already not in selected state)
*
* @param string $mailbox Mailbox name
* @param array $qresync_data QRESYNC data (RFC5162)
*
* @return boolean True on success, false on error
*/
function select($mailbox, $qresync_data = null)
{
if (!strlen($mailbox)) {
return false;
}
if ($this->selected === $mailbox) {
return true;
}
/*
Temporary commented out because Courier returns \Noselect for INBOX
Requires more investigation
if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
if (in_array('\\Noselect', $opts)) {
return false;
}
}
*/
$params = array($this->escape($mailbox));
// QRESYNC data items
// 0. the last known UIDVALIDITY,
// 1. the last known modification sequence,
// 2. the optional set of known UIDs, and
// 3. an optional parenthesized list of known sequence ranges and their
// corresponding UIDs.
if (!empty($qresync_data)) {
if (!empty($qresync_data[2]))
$qresync_data[2] = self::compressMessageSet($qresync_data[2]);
$params[] = array('QRESYNC', $qresync_data);
}
list($code, $response) = $this->execute('SELECT', $params);
if ($code == self::ERROR_OK) {
$response = explode("\r\n", $response);
foreach ($response as $line) {
if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) {
$this->data[strtoupper($m[2])] = (int) $m[1];
}
else if (preg_match('/^\* OK \[/i', $line, $match)) {
$line = substr($line, 6);
if (preg_match('/^(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)/i', $line, $match)) {
$this->data[strtoupper($match[1])] = (int) $match[2];
}
else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) {
$this->data[strtoupper($match[1])] = (string) $match[2];
}
else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) {
$this->data[strtoupper($match[1])] = true;
}
else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) {
$this->data['PERMANENTFLAGS'] = explode(' ', $match[1]);
}
}
// QRESYNC FETCH response (RFC5162)
else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$fetch_data = $this->tokenizeResponse($line, 1);
$data = array('id' => $match[1]);
for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
$data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
}
$this->data['QRESYNC'][$data['uid']] = $data;
}
// QRESYNC VANISHED response (RFC5162)
else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$v_data = $this->tokenizeResponse($line, 1);
$this->data['VANISHED'] = $v_data;
}
}
$this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
$this->selected = $mailbox;
return true;
}
return false;
}
/**
* Executes STATUS command
*
* @param string $mailbox Mailbox name
* @param array $items Additional requested item names. By default
* MESSAGES and UNSEEN are requested. Other defined
* in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
*
* @return array Status item-value hash
* @since 0.5-beta
*/
function status($mailbox, $items=array())
{
if (!strlen($mailbox)) {
return false;
}
if (!in_array('MESSAGES', $items)) {
$items[] = 'MESSAGES';
}
if (!in_array('UNSEEN', $items)) {
$items[] = 'UNSEEN';
}
list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
'(' . implode(' ', (array) $items) . ')'));
if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) {
$result = array();
$response = substr($response, 9); // remove prefix "* STATUS "
list($mbox, $items) = $this->tokenizeResponse($response, 2);
// Fix for #1487859. Some buggy server returns not quoted
// folder name with spaces. Let's try to handle this situation
if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
$response = substr($response, $pos);
$items = $this->tokenizeResponse($response, 1);
if (!is_array($items)) {
return $result;
}
}
for ($i=0, $len=count($items); $i<$len; $i += 2) {
$result[$items[$i]] = $items[$i+1];
}
$this->data['STATUS:'.$mailbox] = $result;
return $result;
}
return false;
}
/**
* Executes EXPUNGE command
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UIDs to expunge
*
* @return boolean True on success, False on error
*/
function expunge($mailbox, $messages=NULL)
{
if (!$this->select($mailbox)) {
return false;
}
if (!$this->data['READ-WRITE']) {
$this->setError(self::ERROR_READONLY, "Mailbox is read-only");
return false;
}
// Clear internal status cache
unset($this->data['STATUS:'.$mailbox]);
if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
$messages = self::compressMessageSet($messages);
$result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
}
else {
$result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
}
if ($result == self::ERROR_OK) {
$this->selected = null; // state has changed, need to reselect
return true;
}
return false;
}
/**
* Executes CLOSE command
*
* @return boolean True on success, False on error
* @since 0.5
*/
function close()
{
$result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
if ($result == self::ERROR_OK) {
$this->selected = null;
return true;
}
return false;
}
/**
* Folder subscription (SUBSCRIBE)
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
function subscribe($mailbox)
{
$result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Folder unsubscription (UNSUBSCRIBE)
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
function unsubscribe($mailbox)
{
$result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Folder creation (CREATE)
*
* @param string $mailbox Mailbox name
+ * @param array $types Optional folder types (RFC 6154)
*
* @return bool True on success, False on error
*/
- function createFolder($mailbox)
+ function createFolder($mailbox, $types = null)
{
- $result = $this->execute('CREATE', array($this->escape($mailbox)),
- self::COMMAND_NORESPONSE);
+ $args = array($this->escape($mailbox));
+
+ // RFC 6154: CREATE-SPECIAL-USE
+ if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
+ $args[] = '(USE (' . implode(' ', $types) . '))';
+ }
+
+ $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Folder renaming (RENAME)
*
* @param string $mailbox Mailbox name
*
* @return bool True on success, False on error
*/
function renameFolder($from, $to)
{
$result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Executes DELETE command
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
function deleteFolder($mailbox)
{
$result = $this->execute('DELETE', array($this->escape($mailbox)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Removes all messages in a folder
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
function clearFolder($mailbox)
{
$num_in_trash = $this->countMessages($mailbox);
if ($num_in_trash > 0) {
$res = $this->flag($mailbox, '1:*', 'DELETED');
}
if ($res) {
if ($this->selected === $mailbox)
$res = $this->close();
else
$res = $this->expunge($mailbox);
}
return $res;
}
/**
* Returns list of mailboxes
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
- * @param array $status_opts (see self::_listMailboxes)
+ * @param array $return_opts (see self::_listMailboxes)
* @param array $select_opts (see self::_listMailboxes)
*
- * @return array List of mailboxes or hash of options if $status_opts argument
- * is non-empty.
+ * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+ * is requested, False on error.
*/
- function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
+ function listMailboxes($ref, $mailbox, $return_opts=array(), $select_opts=array())
{
- return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
+ return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts);
}
/**
* Returns list of subscribed mailboxes
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
- * @param array $status_opts (see self::_listMailboxes)
+ * @param array $return_opts (see self::_listMailboxes)
*
- * @return array List of mailboxes or hash of options if $status_opts argument
- * is non-empty.
+ * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+ * is requested, False on error.
*/
- function listSubscribed($ref, $mailbox, $status_opts=array())
+ function listSubscribed($ref, $mailbox, $return_opts=array())
{
- return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
+ return $this->_listMailboxes($ref, $mailbox, true, $return_opts, NULL);
}
/**
* IMAP LIST/LSUB command
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param bool $subscribed Enables returning subscribed mailboxes only
- * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS)
- * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
+ * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
+ * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
+ * MYRIGHTS, SUBSCRIBED, CHILDREN
* @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
- * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
+ * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
+ * SPECIAL-USE (RFC6154)
*
- * @return array List of mailboxes or hash of options if $status_ops argument
- * is non-empty.
+ * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+ * is requested, False on error.
*/
- private function _listMailboxes($ref, $mailbox, $subscribed=false,
- $status_opts=array(), $select_opts=array())
+ protected function _listMailboxes($ref, $mailbox, $subscribed=false,
+ $return_opts=array(), $select_opts=array())
{
if (!strlen($mailbox)) {
$mailbox = '*';
}
$args = array();
+ $rets = array();
if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
$select_opts = (array) $select_opts;
$args[] = '(' . implode(' ', $select_opts) . ')';
}
$args[] = $this->escape($ref);
$args[] = $this->escape($mailbox);
- if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
- $status_opts = (array) $status_opts;
- $lstatus = true;
+ if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
+ $ext_opts = array('SUBSCRIBED', 'CHILDREN');
+ $rets = array_intersect($return_opts, $ext_opts);
+ $return_opts = array_diff($return_opts, $rets);
+ }
- $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
+ if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
+ $lstatus = true;
+ $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN');
+ $opts = array_diff($return_opts, $status_opts);
+ $status_opts = array_diff($return_opts, $opts);
+
+ if (!empty($status_opts)) {
+ $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
+ }
+
+ if (!empty($opts)) {
+ $rets = array_merge($rets, $opts);
+ }
+ }
+
+ if (!empty($rets)) {
+ $args[] = 'RETURN (' . implode(' ', $rets) . ')';
}
list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
if ($code == self::ERROR_OK) {
$folders = array();
$last = 0;
$pos = 0;
$response .= "\r\n";
while ($pos = strpos($response, "\r\n", $pos+1)) {
// literal string, not real end-of-command-line
if ($response[$pos-1] == '}') {
continue;
}
$line = substr($response, $last, $pos - $last);
$last = $pos + 2;
- if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) {
+ if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) {
continue;
}
+
$cmd = strtoupper($m[1]);
$line = substr($line, strlen($m[0]));
// * LIST (<options>) <delimiter> <mailbox>
if ($cmd == 'LIST' || $cmd == 'LSUB') {
list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
// Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879)
if ($delim) {
$mailbox = rtrim($mailbox, $delim);
}
// Add to result array
if (!$lstatus) {
$folders[] = $mailbox;
}
else {
$folders[$mailbox] = array();
}
// store folder options
if ($cmd == 'LIST') {
// Add to options array
if (empty($this->data['LIST'][$mailbox]))
$this->data['LIST'][$mailbox] = $opts;
else if (!empty($opts))
$this->data['LIST'][$mailbox] = array_unique(array_merge(
$this->data['LIST'][$mailbox], $opts));
}
}
- // * STATUS <mailbox> (<result>)
- else if ($cmd == 'STATUS') {
- list($mailbox, $status) = $this->tokenizeResponse($line, 2);
-
- for ($i=0, $len=count($status); $i<$len; $i += 2) {
- list($name, $value) = $this->tokenizeResponse($status, 2);
- $folders[$mailbox][$name] = $value;
+ else if ($lstatus) {
+ // * STATUS <mailbox> (<result>)
+ if ($cmd == 'STATUS') {
+ list($mailbox, $status) = $this->tokenizeResponse($line, 2);
+
+ for ($i=0, $len=count($status); $i<$len; $i += 2) {
+ list($name, $value) = $this->tokenizeResponse($status, 2);
+ $folders[$mailbox][$name] = $value;
+ }
+ }
+ // * MYRIGHTS <mailbox> <acl>
+ else if ($cmd == 'MYRIGHTS') {
+ list($mailbox, $acl) = $this->tokenizeResponse($line, 2);
+ $folders[$mailbox]['MYRIGHTS'] = $acl;
}
}
}
return $folders;
}
return false;
}
/**
* Returns count of all messages in a folder
*
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
*/
function countMessages($mailbox, $refresh = false)
{
if ($refresh) {
$this->selected = null;
}
if ($this->selected === $mailbox) {
return $this->data['EXISTS'];
}
// Check internal cache
$cache = $this->data['STATUS:'.$mailbox];
if (!empty($cache) && isset($cache['MESSAGES'])) {
return (int) $cache['MESSAGES'];
}
// Try STATUS (should be faster than SELECT)
$counts = $this->status($mailbox);
if (is_array($counts)) {
return (int) $counts['MESSAGES'];
}
return false;
}
/**
* Returns count of messages with \Recent flag in a folder
*
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
*/
function countRecent($mailbox)
{
if (!strlen($mailbox)) {
$mailbox = 'INBOX';
}
$this->select($mailbox);
if ($this->selected === $mailbox) {
return $this->data['RECENT'];
}
return false;
}
/**
* Returns count of messages without \Seen flag in a specified folder
*
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
*/
function countUnseen($mailbox)
{
// Check internal cache
$cache = $this->data['STATUS:'.$mailbox];
if (!empty($cache) && isset($cache['UNSEEN'])) {
return (int) $cache['UNSEEN'];
}
// Try STATUS (should be faster than SELECT+SEARCH)
$counts = $this->status($mailbox);
if (is_array($counts)) {
return (int) $counts['UNSEEN'];
}
// Invoke SEARCH as a fallback
$index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
if (!$index->is_error()) {
return $index->count();
}
return false;
}
/**
* Executes ID command (RFC2971)
*
* @param array $items Client identification information key/value hash
*
* @return array Server identification information key/value hash
* @since 0.6
*/
function id($items=array())
{
if (is_array($items) && !empty($items)) {
foreach ($items as $key => $value) {
$args[] = $this->escape($key, true);
$args[] = $this->escape($value, true);
}
}
list($code, $response) = $this->execute('ID', array(
!empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
));
if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
$response = substr($response, 5); // remove prefix "* ID "
$items = $this->tokenizeResponse($response, 1);
$result = null;
for ($i=0, $len=count($items); $i<$len; $i += 2) {
$result[$items[$i]] = $items[$i+1];
}
return $result;
}
return false;
}
/**
* Executes ENABLE command (RFC5161)
*
* @param mixed $extension Extension name to enable (or array of names)
*
* @return array|bool List of enabled extensions, False on error
* @since 0.6
*/
function enable($extension)
{
if (empty($extension)) {
return false;
}
if (!$this->hasCapability('ENABLE')) {
return false;
}
if (!is_array($extension)) {
$extension = array($extension);
}
if (!empty($this->extensions_enabled)) {
// check if all extensions are already enabled
$diff = array_diff($extension, $this->extensions_enabled);
if (empty($diff)) {
return $extension;
}
// Make sure the mailbox isn't selected, before enabling extension(s)
if ($this->selected !== null) {
$this->close();
}
}
list($code, $response) = $this->execute('ENABLE', $extension);
if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) {
$response = substr($response, 10); // remove prefix "* ENABLED "
$result = (array) $this->tokenizeResponse($response);
$this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result));
return $this->extensions_enabled;
}
return false;
}
/**
* Executes SORT command
*
* @param string $mailbox Mailbox name
* @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
- * @param string $add Searching criteria
+ * @param string $criteria Searching criteria
* @param bool $return_uid Enables UID SORT usage
* @param string $encoding Character set
*
* @return rcube_result_index Response data
*/
- function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
+ function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
{
- $field = strtoupper($field);
+ $old_sel = $this->selected;
+ $supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO');
+ $field = strtoupper($field);
+
if ($field == 'INTERNALDATE') {
$field = 'ARRIVAL';
}
- $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
- 'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
-
- if (!$fields[$field]) {
+ if (!in_array($field, $supported)) {
return new rcube_result_index($mailbox);
}
if (!$this->select($mailbox)) {
return new rcube_result_index($mailbox);
}
+ // return empty result when folder is empty and we're just after SELECT
+ if ($old_sel != $mailbox && !$this->data['EXISTS']) {
+ return new rcube_result_index($mailbox, '* SORT');
+ }
+
// RFC 5957: SORT=DISPLAY
if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
$field = 'DISPLAY' . $field;
}
- // message IDs
- if (!empty($add)) {
- $add = $this->compressMessageSet($add);
- }
+ $encoding = $encoding ? trim($encoding) : 'US-ASCII';
+ $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL';
list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
- array("($field)", $encoding, !empty($add) ? $add : 'ALL'));
+ array("($field)", $encoding, $criteria));
if ($code != self::ERROR_OK) {
$response = null;
}
return new rcube_result_index($mailbox, $response);
}
/**
* Executes THREAD command
*
* @param string $mailbox Mailbox name
* @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
* @param string $criteria Searching criteria
* @param bool $return_uid Enables UIDs in result instead of sequence numbers
* @param string $encoding Character set
*
* @return rcube_result_thread Thread data
*/
function thread($mailbox, $algorithm='REFERENCES', $criteria='', $return_uid=false, $encoding='US-ASCII')
{
$old_sel = $this->selected;
if (!$this->select($mailbox)) {
return new rcube_result_thread($mailbox);
}
// return empty result when folder is empty and we're just after SELECT
if ($old_sel != $mailbox && !$this->data['EXISTS']) {
- return new rcube_result_thread($mailbox);
+ return new rcube_result_thread($mailbox, '* THREAD');
}
$encoding = $encoding ? trim($encoding) : 'US-ASCII';
$algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
$criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
$data = '';
list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD',
array($algorithm, $encoding, $criteria));
if ($code != self::ERROR_OK) {
$response = null;
}
return new rcube_result_thread($mailbox, $response);
}
/**
* Executes SEARCH command
*
* @param string $mailbox Mailbox name
* @param string $criteria Searching criteria
* @param bool $return_uid Enable UID in result instead of sequence ID
* @param array $items Return items (MIN, MAX, COUNT, ALL)
*
* @return rcube_result_index Result data
*/
function search($mailbox, $criteria, $return_uid=false, $items=array())
{
$old_sel = $this->selected;
if (!$this->select($mailbox)) {
return new rcube_result_index($mailbox);
}
// return empty result when folder is empty and we're just after SELECT
if ($old_sel != $mailbox && !$this->data['EXISTS']) {
return new rcube_result_index($mailbox, '* SEARCH');
}
// If ESEARCH is supported always use ALL
// but not when items are specified or using simple id2uid search
if (empty($items) && preg_match('/[^0-9]/', $criteria)) {
$items = array('ALL');
}
$esearch = empty($items) ? false : $this->getCapability('ESEARCH');
$criteria = trim($criteria);
$params = '';
// RFC4731: ESEARCH
if (!empty($items) && $esearch) {
$params .= 'RETURN (' . implode(' ', $items) . ')';
}
if (!empty($criteria)) {
$params .= ($params ? ' ' : '') . $criteria;
}
else {
$params .= 'ALL';
}
list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
array($params));
if ($code != self::ERROR_OK) {
$response = null;
}
return new rcube_result_index($mailbox, $response);
}
/**
* Simulates SORT command by using FETCH and sorting.
*
* @param string $mailbox Mailbox name
* @param string|array $message_set Searching criteria (list of messages to return)
* @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param bool $skip_deleted Makes that DELETED messages will be skipped
* @param bool $uidfetch Enables UID FETCH usage
* @param bool $return_uid Enables returning UIDs instead of IDs
*
* @return rcube_result_index Response data
*/
function index($mailbox, $message_set, $index_field='', $skip_deleted=true,
$uidfetch=false, $return_uid=false)
{
$msg_index = $this->fetchHeaderIndex($mailbox, $message_set,
$index_field, $skip_deleted, $uidfetch, $return_uid);
if (!empty($msg_index)) {
asort($msg_index); // ASC
$msg_index = array_keys($msg_index);
$msg_index = '* SEARCH ' . implode(' ', $msg_index);
}
else {
$msg_index = is_array($msg_index) ? '* SEARCH' : null;
}
return new rcube_result_index($mailbox, $msg_index);
}
function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true,
$uidfetch=false, $return_uid=false)
{
if (is_array($message_set)) {
if (!($message_set = $this->compressMessageSet($message_set)))
return false;
} else {
list($from_idx, $to_idx) = explode(':', $message_set);
if (empty($message_set) ||
(isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) {
return false;
}
}
$index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
$fields_a['DATE'] = 1;
$fields_a['INTERNALDATE'] = 4;
$fields_a['ARRIVAL'] = 4;
$fields_a['FROM'] = 1;
$fields_a['REPLY-TO'] = 1;
$fields_a['SENDER'] = 1;
$fields_a['TO'] = 1;
$fields_a['CC'] = 1;
$fields_a['SUBJECT'] = 1;
$fields_a['UID'] = 2;
$fields_a['SIZE'] = 2;
$fields_a['SEEN'] = 3;
$fields_a['RECENT'] = 3;
$fields_a['DELETED'] = 3;
if (!($mode = $fields_a[$index_field])) {
return false;
}
/* Do "SELECT" command */
if (!$this->select($mailbox)) {
return false;
}
// build FETCH command string
$key = $this->nextTag();
$cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
$fields = array();
if ($return_uid)
$fields[] = 'UID';
if ($skip_deleted)
$fields[] = 'FLAGS';
if ($mode == 1) {
if ($index_field == 'DATE')
$fields[] = 'INTERNALDATE';
$fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]";
}
else if ($mode == 2) {
if ($index_field == 'SIZE')
$fields[] = 'RFC822.SIZE';
else if (!$return_uid || $index_field != 'UID')
$fields[] = $index_field;
}
else if ($mode == 3 && !$skip_deleted)
$fields[] = 'FLAGS';
else if ($mode == 4)
$fields[] = 'INTERNALDATE';
$request = "$key $cmd $message_set (" . implode(' ', $fields) . ")";
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
return false;
}
$result = array();
do {
$line = rtrim($this->readLine(200));
$line = $this->multLine($line);
if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
$id = $m[1];
$flags = NULL;
if ($return_uid) {
if (preg_match('/UID ([0-9]+)/', $line, $matches))
$id = (int) $matches[1];
else
continue;
}
if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
$flags = explode(' ', strtoupper($matches[1]));
if (in_array('\\DELETED', $flags)) {
continue;
}
}
if ($mode == 1 && $index_field == 'DATE') {
if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
$value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
$value = trim($value);
$result[$id] = $this->strToTime($value);
}
// non-existent/empty Date: header, use INTERNALDATE
if (empty($result[$id])) {
if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches))
$result[$id] = $this->strToTime($matches[1]);
else
$result[$id] = 0;
}
} else if ($mode == 1) {
if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
$value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
$result[$id] = trim($value);
} else {
$result[$id] = '';
}
} else if ($mode == 2) {
if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) {
$result[$id] = trim($matches[1]);
} else {
$result[$id] = 0;
}
} else if ($mode == 3) {
if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
$flags = explode(' ', $matches[1]);
}
$result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0;
} else if ($mode == 4) {
if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
$result[$id] = $this->strToTime($matches[1]);
} else {
$result[$id] = 0;
}
}
}
} while (!$this->startsWith($line, $key, true, true));
return $result;
}
/**
* Returns message sequence identifier
*
* @param string $mailbox Mailbox name
* @param int $uid Message unique identifier (UID)
*
* @return int Message sequence identifier
*/
function UID2ID($mailbox, $uid)
{
if ($uid > 0) {
$index = $this->search($mailbox, "UID $uid");
if ($index->count() == 1) {
$arr = $index->get();
return (int) $arr[0];
}
}
return null;
}
/**
* Returns message unique identifier (UID)
*
* @param string $mailbox Mailbox name
* @param int $uid Message sequence identifier
*
* @return int Message unique identifier
*/
function ID2UID($mailbox, $id)
{
if (empty($id) || $id < 0) {
return null;
}
if (!$this->select($mailbox)) {
return null;
}
$index = $this->search($mailbox, $id, true);
if ($index->count() == 1) {
$arr = $index->get();
return (int) $arr[0];
}
return null;
}
/**
* Sets flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
*
* @return bool True on success, False on failure
*/
function flag($mailbox, $messages, $flag) {
return $this->modFlag($mailbox, $messages, $flag, '+');
}
/**
* Unsets flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
*
* @return bool True on success, False on failure
*/
function unflag($mailbox, $messages, $flag) {
return $this->modFlag($mailbox, $messages, $flag, '-');
}
/**
* Changes flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
* @param string $mod Modifier [+|-]. Default: "+".
*
* @return bool True on success, False on failure
*/
- private function modFlag($mailbox, $messages, $flag, $mod = '+')
+ protected function modFlag($mailbox, $messages, $flag, $mod = '+')
{
if (!$this->select($mailbox)) {
return false;
}
if (!$this->data['READ-WRITE']) {
$this->setError(self::ERROR_READONLY, "Mailbox is read-only");
return false;
}
if ($this->flags[strtoupper($flag)]) {
$flag = $this->flags[strtoupper($flag)];
}
- if (!$flag || !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
- || !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
+ if (!$flag) {
+ return false;
+ }
+
+ // if PERMANENTFLAGS is not specified all flags are allowed
+ if (!empty($this->data['PERMANENTFLAGS'])
+ && !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
+ && !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
) {
return false;
}
// Clear internal status cache
if ($flag == 'SEEN') {
unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
}
if ($mod != '+' && $mod != '-') {
$mod = '+';
}
$result = $this->execute('UID STORE', array(
$this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Copies message(s) from one folder to another
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
* @param string $to Destination mailbox name
*
* @return bool True on success, False on failure
*/
function copy($messages, $from, $to)
{
// Clear last COPYUID data
unset($this->data['COPYUID']);
if (!$this->select($from)) {
return false;
}
// Clear internal status cache
unset($this->data['STATUS:'.$to]);
$result = $this->execute('UID COPY', array(
$this->compressMessageSet($messages), $this->escape($to)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Moves message(s) from one folder to another.
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
* @param string $to Destination mailbox name
*
* @return bool True on success, False on failure
*/
function move($messages, $from, $to)
{
if (!$this->select($from)) {
return false;
}
if (!$this->data['READ-WRITE']) {
$this->setError(self::ERROR_READONLY, "Mailbox is read-only");
return false;
}
// use MOVE command (RFC 6851)
if ($this->hasCapability('MOVE')) {
// Clear last COPYUID data
unset($this->data['COPYUID']);
// Clear internal status cache
unset($this->data['STATUS:'.$to]);
unset($this->data['STATUS:'.$from]);
$result = $this->execute('UID MOVE', array(
$this->compressMessageSet($messages), $this->escape($to)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
// use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
$result = $this->copy($messages, $from, $to);
if ($result) {
// Clear internal status cache
unset($this->data['STATUS:'.$from]);
$result = $this->flag($from, $messages, 'DELETED');
if ($messages == '*') {
// CLOSE+SELECT should be faster than EXPUNGE
$this->close();
}
else {
$this->expunge($from, $messages);
}
}
return $result;
}
/**
* FETCH command (RFC3501)
*
* @param string $mailbox Mailbox name
* @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
* @param bool $is_uid True if $message_set contains UIDs
* @param array $query_items FETCH command data items
* @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query
* @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
*
* @return array List of rcube_message_header elements, False on error
* @since 0.6
*/
function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(),
$mod_seq = null, $vanished = false)
{
if (!$this->select($mailbox)) {
return false;
}
$message_set = $this->compressMessageSet($message_set);
$result = array();
$key = $this->nextTag();
$request = $key . ($is_uid ? ' UID' : '') . " FETCH $message_set ";
$request .= "(" . implode(' ', $query_items) . ")";
if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
$request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")";
}
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
return false;
}
do {
$line = $this->readLine(4096);
if (!$line)
break;
// Sample reply line:
// * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
// INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
// BODY[HEADER.FIELDS ...
if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
$id = intval($m[1]);
$result[$id] = new rcube_message_header;
$result[$id]->id = $id;
$result[$id]->subject = '';
$result[$id]->messageID = 'mid:' . $id;
$headers = null;
$lines = array();
$line = substr($line, strlen($m[0]) + 2);
$ln = 0;
// get complete entry
while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
$bytes = $m[1];
$out = '';
while (strlen($out) < $bytes) {
$out = $this->readBytes($bytes);
if ($out === NULL)
break;
$line .= $out;
}
$str = $this->readLine(4096);
if ($str === false)
break;
$line .= $str;
}
// Tokenize response and assign to object properties
while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
if ($name == 'UID') {
$result[$id]->uid = intval($value);
}
else if ($name == 'RFC822.SIZE') {
$result[$id]->size = intval($value);
}
else if ($name == 'RFC822.TEXT') {
$result[$id]->body = $value;
}
else if ($name == 'INTERNALDATE') {
$result[$id]->internaldate = $value;
$result[$id]->date = $value;
$result[$id]->timestamp = $this->StrToTime($value);
}
else if ($name == 'FLAGS') {
if (!empty($value)) {
foreach ((array)$value as $flag) {
$flag = str_replace(array('$', '\\'), '', $flag);
$flag = strtoupper($flag);
$result[$id]->flags[$flag] = true;
}
}
}
else if ($name == 'MODSEQ') {
$result[$id]->modseq = $value[0];
}
else if ($name == 'ENVELOPE') {
$result[$id]->envelope = $value;
}
else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
$value = array($value);
}
$result[$id]->bodystructure = $value;
}
else if ($name == 'RFC822') {
$result[$id]->body = $value;
}
else if (stripos($name, 'BODY[') === 0) {
$name = str_replace(']', '', substr($name, 5));
if ($name == 'HEADER.FIELDS') {
// skip ']' after headers list
$this->tokenizeResponse($line, 1);
$headers = $this->tokenizeResponse($line, 1);
}
else if (strlen($name))
$result[$id]->bodypart[$name] = $value;
else
$result[$id]->body = $value;
}
}
// create array with header field:data
if (!empty($headers)) {
$headers = explode("\n", trim($headers));
foreach ($headers as $resln) {
if (ord($resln[0]) <= 32) {
$lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
} else {
$lines[++$ln] = trim($resln);
}
}
foreach ($lines as $str) {
list($field, $string) = explode(':', $str, 2);
$field = strtolower($field);
$string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
switch ($field) {
case 'date';
$result[$id]->date = $string;
$result[$id]->timestamp = $this->strToTime($string);
break;
case 'from':
$result[$id]->from = $string;
break;
case 'to':
$result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
break;
case 'subject':
$result[$id]->subject = $string;
break;
case 'reply-to':
$result[$id]->replyto = $string;
break;
case 'cc':
$result[$id]->cc = $string;
break;
case 'bcc':
$result[$id]->bcc = $string;
break;
case 'content-transfer-encoding':
$result[$id]->encoding = $string;
break;
case 'content-type':
$ctype_parts = preg_split('/[; ]+/', $string);
$result[$id]->ctype = strtolower(array_shift($ctype_parts));
if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
$result[$id]->charset = $regs[1];
}
break;
case 'in-reply-to':
$result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
break;
case 'references':
$result[$id]->references = $string;
break;
case 'return-receipt-to':
case 'disposition-notification-to':
case 'x-confirm-reading-to':
$result[$id]->mdn_to = $string;
break;
case 'message-id':
$result[$id]->messageID = $string;
break;
case 'x-priority':
if (preg_match('/^(\d+)/', $string, $matches)) {
$result[$id]->priority = intval($matches[1]);
}
break;
default:
if (strlen($field) < 3) {
break;
}
if ($result[$id]->others[$field]) {
$string = array_merge((array)$result[$id]->others[$field], (array)$string);
}
$result[$id]->others[$field] = $string;
}
}
}
}
// VANISHED response (QRESYNC RFC5162)
// Sample: * VANISHED (EARLIER) 300:310,405,411
else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$v_data = $this->tokenizeResponse($line, 1);
$this->data['VANISHED'] = $v_data;
}
} while (!$this->startsWith($line, $key, true));
return $result;
}
/**
* Returns message(s) data (flags, headers, etc.)
*
* @param string $mailbox Mailbox name
* @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
* @param bool $is_uid True if $message_set contains UIDs
* @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
* @param array $add_headers List of additional headers
*
* @return bool|array List of rcube_message_header elements, False on error
*/
function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array())
{
$query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
$headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO',
'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY');
if (!empty($add_headers)) {
$add_headers = array_map('strtoupper', $add_headers);
$headers = array_unique(array_merge($headers, $add_headers));
}
if ($bodystr) {
$query_items[] = 'BODYSTRUCTURE';
}
$query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]';
$result = $this->fetch($mailbox, $message_set, $is_uid, $query_items);
return $result;
}
/**
* Returns message data (flags, headers, etc.)
*
* @param string $mailbox Mailbox name
* @param int $id Message sequence identifier or UID
* @param bool $is_uid True if $id is an UID
* @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
* @param array $add_headers List of additional headers
*
* @return bool|rcube_message_header Message data, False on error
*/
function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array())
{
$a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers);
if (is_array($a)) {
return array_shift($a);
}
return false;
}
function sortHeaders($a, $field, $flag)
{
if (empty($field)) {
$field = 'uid';
}
else {
$field = strtolower($field);
}
if ($field == 'date' || $field == 'internaldate') {
$field = 'timestamp';
}
if (empty($flag)) {
$flag = 'ASC';
} else {
$flag = strtoupper($flag);
}
$c = count($a);
if ($c > 0) {
// Strategy:
// First, we'll create an "index" array.
// Then, we'll use sort() on that array,
// and use that to sort the main array.
// create "index" array
$index = array();
reset($a);
while (list($key, $val) = each($a)) {
if ($field == 'timestamp') {
$data = $this->strToTime($val->date);
if (!$data) {
$data = $val->timestamp;
}
} else {
$data = $val->$field;
if (is_string($data)) {
$data = str_replace('"', '', $data);
if ($field == 'subject') {
$data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
}
$data = strtoupper($data);
}
}
$index[$key] = $data;
}
// sort index
if ($flag == 'ASC') {
asort($index);
} else {
arsort($index);
}
// form new array based on index
$result = array();
reset($index);
while (list($key, $val) = each($index)) {
$result[$key] = $a[$key];
}
}
return $result;
}
function fetchMIMEHeaders($mailbox, $uid, $parts, $mime=true)
{
if (!$this->select($mailbox)) {
return false;
}
$result = false;
$parts = (array) $parts;
$key = $this->nextTag();
$peeks = array();
$type = $mime ? 'MIME' : 'HEADER';
// format request
foreach ($parts as $part) {
$peeks[] = "BODY.PEEK[$part.$type]";
}
$request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
// send request
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
return false;
}
do {
$line = $this->readLine(1024);
if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
$idx = $matches[1];
$headers = '';
// get complete entry
if (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
$bytes = $m[1];
$out = '';
while (strlen($out) < $bytes) {
$out = $this->readBytes($bytes);
if ($out === null)
break;
$headers .= $out;
}
}
$result[$idx] = trim($headers);
}
} while (!$this->startsWith($line, $key, true));
return $result;
}
function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL)
{
$part = empty($part) ? 'HEADER' : $part.'.MIME';
return $this->handlePartBody($mailbox, $id, $is_uid, $part);
}
function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL, $formatted=false, $max_bytes=0)
{
if (!$this->select($mailbox)) {
return false;
}
- switch ($encoding) {
- case 'base64':
- $mode = 1;
- break;
- case 'quoted-printable':
- $mode = 2;
- break;
- case 'x-uuencode':
- case 'x-uue':
- case 'uue':
- case 'uuencode':
- $mode = 3;
- break;
- default:
- $mode = 0;
- }
-
- // Use BINARY extension when possible (and safe)
- $binary = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
- $fetch_mode = $binary ? 'BINARY' : 'BODY';
- $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
+ $binary = true;
- // format request
- $key = $this->nextTag();
- $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)";
- $result = false;
- $found = false;
+ do {
+ if (!$initiated) {
+ switch ($encoding) {
+ case 'base64':
+ $mode = 1;
+ break;
+ case 'quoted-printable':
+ $mode = 2;
+ break;
+ case 'x-uuencode':
+ case 'x-uue':
+ case 'uue':
+ case 'uuencode':
+ $mode = 3;
+ break;
+ default:
+ $mode = 0;
+ }
- // send request
- if (!$this->putLine($request)) {
- $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
- return false;
- }
+ // Use BINARY extension when possible (and safe)
+ $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
+ $fetch_mode = $binary ? 'BINARY' : 'BODY';
+ $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
+
+ // format request
+ $key = $this->nextTag();
+ $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)";
+ $result = false;
+ $found = false;
+ $initiated = true;
+
+ // send request
+ if (!$this->putLine($request)) {
+ $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+ return false;
+ }
- if ($binary) {
- // WARNING: Use $formatted argument with care, this may break binary data stream
- $mode = -1;
- }
+ if ($binary) {
+ // WARNING: Use $formatted argument with care, this may break binary data stream
+ $mode = -1;
+ }
+ }
- do {
$line = trim($this->readLine(1024));
if (!$line) {
break;
}
+ // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
+ if ($binary && !$found && preg_match('/^' . $key . ' NO \[UNKNOWN-CTE\]/i', $line)) {
+ $binary = $initiated = false;
+ continue;
+ }
+
// skip irrelevant untagged responses (we have a result already)
if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
continue;
}
$line = $m[2];
// handle one line response
if ($line[0] == '(' && substr($line, -1) == ')') {
// tokenize content inside brackets
// the content can be e.g.: (UID 9844 BODY[2.4] NIL)
$tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
for ($i=0; $i<count($tokens); $i+=2) {
if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
$result = $tokens[$i+1];
$found = true;
break;
}
}
if ($result !== false) {
if ($mode == 1) {
$result = base64_decode($result);
}
else if ($mode == 2) {
$result = quoted_printable_decode($result);
}
else if ($mode == 3) {
$result = convert_uudecode($result);
}
}
}
// response with string literal
else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
$bytes = (int) $m[1];
$prev = '';
$found = true;
// empty body
if (!$bytes) {
$result = '';
}
else while ($bytes > 0) {
$line = $this->readLine(8192);
if ($line === NULL) {
break;
}
$len = strlen($line);
if ($len > $bytes) {
$line = substr($line, 0, $bytes);
$len = strlen($line);
}
$bytes -= $len;
// BASE64
if ($mode == 1) {
- $line = rtrim($line, "\t\r\n\0\x0B");
+ $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line);
// create chunks with proper length for base64 decoding
$line = $prev.$line;
$length = strlen($line);
if ($length % 4) {
$length = floor($length / 4) * 4;
$prev = substr($line, $length);
$line = substr($line, 0, $length);
}
else {
$prev = '';
}
$line = base64_decode($line);
}
// QUOTED-PRINTABLE
else if ($mode == 2) {
$line = rtrim($line, "\t\r\0\x0B");
$line = quoted_printable_decode($line);
}
// UUENCODE
else if ($mode == 3) {
$line = rtrim($line, "\t\r\n\0\x0B");
if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) {
continue;
}
$line = convert_uudecode($line);
}
// default
else if ($formatted) {
$line = rtrim($line, "\t\r\n\0\x0B") . "\n";
}
if ($file) {
if (fwrite($file, $line) === false) {
break;
}
}
else if ($print) {
echo $line;
}
else {
$result .= $line;
}
}
}
- } while (!$this->startsWith($line, $key, true));
+ } while (!$this->startsWith($line, $key, true) || !$initiated);
if ($result !== false) {
if ($file) {
return fwrite($file, $result);
}
else if ($print) {
echo $result;
return true;
}
return $result;
}
return false;
}
/**
* Handler for IMAP APPEND command
*
* @param string $mailbox Mailbox name
* @param string|array $message The message source string or array (of strings and file pointers)
* @param array $flags Message flags
* @param string $date Message internal date
* @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
function append($mailbox, &$message, $flags = array(), $date = null, $binary = false)
{
unset($this->data['APPENDUID']);
if ($mailbox === null || $mailbox === '') {
return false;
}
$binary = $binary && $this->getCapability('BINARY');
$literal_plus = !$binary && $this->prefs['literal+'];
$len = 0;
$msg = is_array($message) ? $message : array(&$message);
$chunk_size = 512000;
for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
if (is_resource($msg[$i])) {
$stat = fstat($msg[$i]);
if ($stat === false) {
return false;
}
$len += $stat['size'];
}
else {
if (!$binary) {
$msg[$i] = str_replace("\r", '', $msg[$i]);
$msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
}
$len += strlen($msg[$i]);
}
}
if (!$len) {
return false;
}
// build APPEND command
$key = $this->nextTag();
$request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
if (!empty($date)) {
$request .= ' ' . $this->escape($date);
}
$request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
// send APPEND command
if ($this->putLine($request)) {
// Do not wait when LITERAL+ is supported
if (!$literal_plus) {
$line = $this->readReply();
if ($line[0] != '+') {
$this->parseResult($line, 'APPEND: ');
return false;
}
}
foreach ($msg as $msg_part) {
// file pointer
if (is_resource($msg_part)) {
rewind($msg_part);
while (!feof($msg_part) && $this->fp) {
$buffer = fread($msg_part, $chunk_size);
$this->putLine($buffer, false);
}
fclose($msg_part);
}
// string
else {
$size = strlen($msg_part);
// Break up the data by sending one chunk (up to 512k) at a time.
// This approach reduces our peak memory usage
for ($offset = 0; $offset < $size; $offset += $chunk_size) {
$chunk = substr($msg_part, $offset, $chunk_size);
if (!$this->putLine($chunk, false)) {
return false;
}
}
}
}
if (!$this->putLine('')) { // \r\n
return false;
}
do {
$line = $this->readLine();
} while (!$this->startsWith($line, $key, true, true));
// Clear internal status cache
unset($this->data['STATUS:'.$mailbox]);
if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
return false;
else if (!empty($this->data['APPENDUID']))
return $this->data['APPENDUID'];
else
return true;
}
else {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
}
return false;
}
/**
* Handler for IMAP APPEND command.
*
* @param string $mailbox Mailbox name
* @param string $path Path to the file with message body
* @param string $headers Message headers
* @param array $flags Message flags
* @param string $date Message internal date
* @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
{
// open message file
if (file_exists(realpath($path))) {
$fp = fopen($path, 'r');
}
if (!$fp) {
$this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
return false;
}
$message = array();
if ($headers) {
$message[] = trim($headers, "\r\n") . "\r\n\r\n";
}
$message[] = $fp;
return $this->append($mailbox, $message, $flags, $date, $binary);
}
/**
* Returns QUOTA information
*
+ * @param string $mailbox Mailbox name
+ *
* @return array Quota information
*/
- function getQuota()
- {
- /*
- * GETQUOTAROOT "INBOX"
- * QUOTAROOT INBOX user/rchijiiwa1
- * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
- * OK Completed
- */
- $result = false;
- $quota_lines = array();
- $key = $this->nextTag();
- $command = $key . ' GETQUOTAROOT INBOX';
-
- // get line(s) containing quota info
- if ($this->putLine($command)) {
- do {
- $line = rtrim($this->readLine(5000));
- if (preg_match('/^\* QUOTA /', $line)) {
- $quota_lines[] = $line;
- }
- } while (!$this->startsWith($line, $key, true, true));
- }
- else {
- $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
+ function getQuota($mailbox = null)
+ {
+ if ($mailbox === null || $mailbox === '') {
+ $mailbox = 'INBOX';
}
- // return false if not found, parse if found
+ // a0001 GETQUOTAROOT INBOX
+ // * QUOTAROOT INBOX user/sample
+ // * QUOTA user/sample (STORAGE 654 9765)
+ // a0001 OK Completed
+
+ list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox)));
+
+ $result = false;
$min_free = PHP_INT_MAX;
- foreach ($quota_lines as $key => $quota_line) {
- $quota_line = str_replace(array('(', ')'), '', $quota_line);
- $parts = explode(' ', $quota_line);
- $storage_part = array_search('STORAGE', $parts);
+ $all = array();
- if (!$storage_part) {
- continue;
- }
+ if ($code == self::ERROR_OK) {
+ foreach (explode("\n", $response) as $line) {
+ if (preg_match('/^\* QUOTA /', $line)) {
+ list(, , $quota_root) = $this->tokenizeResponse($line, 3);
+
+ while ($line) {
+ list($type, $used, $total) = $this->tokenizeResponse($line, 1);
+ $type = strtolower($type);
+
+ if ($type && $total) {
+ $all[$quota_root][$type]['used'] = intval($used);
+ $all[$quota_root][$type]['total'] = intval($total);
+ }
+ }
+
+ if (empty($all[$quota_root]['storage'])) {
+ continue;
+ }
- $used = intval($parts[$storage_part+1]);
- $total = intval($parts[$storage_part+2]);
- $free = $total - $used;
+ $used = $all[$quota_root]['storage']['used'];
+ $total = $all[$quota_root]['storage']['total'];
+ $free = $total - $used;
- // return lowest available space from all quotas
- if ($free < $min_free) {
- $min_free = $free;
- $result['used'] = $used;
- $result['total'] = $total;
- $result['percent'] = min(100, round(($used/max(1,$total))*100));
- $result['free'] = 100 - $result['percent'];
+ // calculate lowest available space from all storage quotas
+ if ($free < $min_free) {
+ $min_free = $free;
+ $result['used'] = $used;
+ $result['total'] = $total;
+ $result['percent'] = min(100, round(($used/max(1,$total))*100));
+ $result['free'] = 100 - $result['percent'];
+ }
+ }
}
}
+ if (!empty($result)) {
+ $result['all'] = $all;
+ }
+
return $result;
}
/**
* Send the SETACL command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
* @param mixed $acl ACL string or array
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
function setACL($mailbox, $user, $acl)
{
if (is_array($acl)) {
$acl = implode('', $acl);
}
$result = $this->execute('SETACL', array(
$this->escape($mailbox), $this->escape($user), strtolower($acl)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the DELETEACL command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
function deleteACL($mailbox, $user)
{
$result = $this->execute('DELETEACL', array(
$this->escape($mailbox), $this->escape($user)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the GETACL command (RFC4314)
*
* @param string $mailbox Mailbox name
*
* @return array User-rights array on success, NULL on error
* @since 0.5-beta
*/
function getACL($mailbox)
{
list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
// Parse server response (remove "* ACL ")
$response = substr($response, 6);
$ret = $this->tokenizeResponse($response);
$mbox = array_shift($ret);
$size = count($ret);
// Create user-rights hash array
// @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
// so we could return only standard rights defined in RFC4314,
// excluding 'c' and 'd' defined in RFC2086.
if ($size % 2 == 0) {
for ($i=0; $i<$size; $i++) {
$ret[$ret[$i]] = str_split($ret[++$i]);
unset($ret[$i-1]);
unset($ret[$i]);
}
return $ret;
}
$this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
return NULL;
}
return NULL;
}
/**
* Send the LISTRIGHTS command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
*
* @return array List of user rights
* @since 0.5-beta
*/
function listRights($mailbox, $user)
{
list($code, $response) = $this->execute('LISTRIGHTS', array(
$this->escape($mailbox), $this->escape($user)));
if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
// Parse server response (remove "* LISTRIGHTS ")
$response = substr($response, 13);
$ret_mbox = $this->tokenizeResponse($response, 1);
$ret_user = $this->tokenizeResponse($response, 1);
$granted = $this->tokenizeResponse($response, 1);
$optional = trim($response);
return array(
'granted' => str_split($granted),
'optional' => explode(' ', $optional),
);
}
return NULL;
}
/**
* Send the MYRIGHTS command (RFC4314)
*
* @param string $mailbox Mailbox name
*
* @return array MYRIGHTS response on success, NULL on error
* @since 0.5-beta
*/
function myRights($mailbox)
{
list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
// Parse server response (remove "* MYRIGHTS ")
$response = substr($response, 11);
$ret_mbox = $this->tokenizeResponse($response, 1);
$rights = $this->tokenizeResponse($response, 1);
return str_split($rights);
}
return NULL;
}
/**
* Send the SETMETADATA command (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
function setMetadata($mailbox, $entries)
{
if (!is_array($entries) || empty($entries)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
return false;
}
foreach ($entries as $name => $value) {
$entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true);
}
$entries = implode(' ', $entries);
$result = $this->execute('SETMETADATA', array(
$this->escape($mailbox), '(' . $entries . ')'),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the SETMETADATA command with NIL values (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
function deleteMetadata($mailbox, $entries)
{
if (!is_array($entries) && !empty($entries)) {
$entries = explode(' ', $entries);
}
if (empty($entries)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
return false;
}
foreach ($entries as $entry) {
$data[$entry] = NULL;
}
return $this->setMetadata($mailbox, $data);
}
/**
* Send the GETMETADATA command (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
*
* @return array GETMETADATA result on success, NULL on error
*
* @since 0.5-beta
*/
function getMetadata($mailbox, $entries, $options=array())
{
if (!is_array($entries)) {
$entries = array($entries);
}
// create entries string
foreach ($entries as $idx => $name) {
$entries[$idx] = $this->escape($name);
}
$optlist = '';
$entlist = '(' . implode(' ', $entries) . ')';
// create options string
if (is_array($options)) {
$options = array_change_key_case($options, CASE_UPPER);
$opts = array();
if (!empty($options['MAXSIZE'])) {
$opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
}
if (!empty($options['DEPTH'])) {
$opts[] = 'DEPTH '.intval($options['DEPTH']);
}
if ($opts) {
$optlist = '(' . implode(' ', $opts) . ')';
}
}
$optlist .= ($optlist ? ' ' : '') . $entlist;
list($code, $response) = $this->execute('GETMETADATA', array(
$this->escape($mailbox), $optlist));
if ($code == self::ERROR_OK) {
$result = array();
$data = $this->tokenizeResponse($response);
// The METADATA response can contain multiple entries in a single
// response or multiple responses for each entry or group of entries
if (!empty($data) && ($size = count($data))) {
for ($i=0; $i<$size; $i++) {
if (isset($mbox) && is_array($data[$i])) {
$size_sub = count($data[$i]);
- for ($x=0; $x<$size_sub; $x++) {
- $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
+ for ($x=0; $x<$size_sub; $x+=2) {
+ if ($data[$i][$x+1] !== null)
+ $result[$mbox][$data[$i][$x]] = $data[$i][$x+1];
}
unset($data[$i]);
}
else if ($data[$i] == '*') {
if ($data[$i+1] == 'METADATA') {
$mbox = $data[$i+2];
unset($data[$i]); // "*"
unset($data[++$i]); // "METADATA"
unset($data[++$i]); // Mailbox
}
// get rid of other untagged responses
else {
unset($mbox);
unset($data[$i]);
}
}
else if (isset($mbox)) {
- $result[$mbox][$data[$i]] = $data[++$i];
+ if ($data[++$i] !== null)
+ $result[$mbox][$data[$i-1]] = $data[$i];
unset($data[$i]);
unset($data[$i-1]);
}
else {
unset($data[$i]);
}
}
}
return $result;
}
return NULL;
}
/**
* Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $data Data array where each item is an array with
* three elements: entry name, attribute name, value
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
function setAnnotation($mailbox, $data)
{
if (!is_array($data) || empty($data)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
return false;
}
foreach ($data as $entry) {
// Workaround cyrus-murder bug, the entry[2] string needs to be escaped
if (self::$mupdate) {
$entry[2] = addcslashes($entry[2], '\\"');
}
// ANNOTATEMORE drafts before version 08 require quoted parameters
$entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
$this->escape($entry[1], true), $this->escape($entry[2], true));
}
$entries = implode(' ', $entries);
$result = $this->execute('SETANNOTATION', array(
$this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $data Data array where each item is an array with
* two elements: entry name and attribute name
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
function deleteAnnotation($mailbox, $data)
{
if (!is_array($data) || empty($data)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
return false;
}
return $this->setAnnotation($mailbox, $data);
}
/**
* Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $entries Entries names
* @param array $attribs Attribs names
*
* @return array Annotations result on success, NULL on error
*
* @since 0.5-beta
*/
function getAnnotation($mailbox, $entries, $attribs)
{
if (!is_array($entries)) {
$entries = array($entries);
}
// create entries string
// ANNOTATEMORE drafts before version 08 require quoted parameters
foreach ($entries as $idx => $name) {
$entries[$idx] = $this->escape($name, true);
}
$entries = '(' . implode(' ', $entries) . ')';
if (!is_array($attribs)) {
$attribs = array($attribs);
}
// create entries string
foreach ($attribs as $idx => $name) {
$attribs[$idx] = $this->escape($name, true);
}
$attribs = '(' . implode(' ', $attribs) . ')';
list($code, $response) = $this->execute('GETANNOTATION', array(
$this->escape($mailbox), $entries, $attribs));
if ($code == self::ERROR_OK) {
$result = array();
$data = $this->tokenizeResponse($response);
// Here we returns only data compatible with METADATA result format
if (!empty($data) && ($size = count($data))) {
for ($i=0; $i<$size; $i++) {
$entry = $data[$i];
if (isset($mbox) && is_array($entry)) {
$attribs = $entry;
$entry = $last_entry;
}
else if ($entry == '*') {
if ($data[$i+1] == 'ANNOTATION') {
$mbox = $data[$i+2];
unset($data[$i]); // "*"
unset($data[++$i]); // "ANNOTATION"
unset($data[++$i]); // Mailbox
}
// get rid of other untagged responses
else {
unset($mbox);
unset($data[$i]);
}
continue;
}
else if (isset($mbox)) {
$attribs = $data[++$i];
}
else {
unset($data[$i]);
continue;
}
if (!empty($attribs)) {
for ($x=0, $len=count($attribs); $x<$len;) {
$attr = $attribs[$x++];
$value = $attribs[$x++];
- if ($attr == 'value.priv') {
+ if ($attr == 'value.priv' && $value !== null) {
$result[$mbox]['/private' . $entry] = $value;
}
- else if ($attr == 'value.shared') {
+ else if ($attr == 'value.shared' && $value !== null) {
$result[$mbox]['/shared' . $entry] = $value;
}
}
}
$last_entry = $entry;
unset($data[$i]);
}
}
return $result;
}
return NULL;
}
/**
* Returns BODYSTRUCTURE for the specified message.
*
* @param string $mailbox Folder name
* @param int $id Message sequence number or UID
* @param bool $is_uid True if $id is an UID
*
* @return array/bool Body structure array or False on error.
* @since 0.6
*/
function getStructure($mailbox, $id, $is_uid = false)
{
$result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
if (is_array($result)) {
$result = array_shift($result);
return $result->bodystructure;
}
return false;
}
/**
* Returns data of a message part according to specified structure.
*
* @param array $structure Message structure (getStructure() result)
* @param string $part Message part identifier
*
* @return array Part data as hash array (type, encoding, charset, size)
*/
static function getStructurePartData($structure, $part)
{
$part_a = self::getStructurePartArray($structure, $part);
$data = array();
if (empty($part_a)) {
return $data;
}
// content-type
if (is_array($part_a[0])) {
$data['type'] = 'multipart';
}
else {
$data['type'] = strtolower($part_a[0]);
// encoding
$data['encoding'] = strtolower($part_a[5]);
// charset
if (is_array($part_a[2])) {
while (list($key, $val) = each($part_a[2])) {
if (strcasecmp($val, 'charset') == 0) {
$data['charset'] = $part_a[2][$key+1];
break;
}
}
}
}
// size
$data['size'] = intval($part_a[6]);
return $data;
}
static function getStructurePartArray($a, $part)
{
if (!is_array($a)) {
return false;
}
if (empty($part)) {
return $a;
}
$ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : '';
if (strcasecmp($ctype, 'message/rfc822') == 0) {
$a = $a[8];
}
if (strpos($part, '.') > 0) {
$orig_part = $part;
$pos = strpos($part, '.');
$rest = substr($orig_part, $pos+1);
$part = substr($orig_part, 0, $pos);
return self::getStructurePartArray($a[$part-1], $rest);
}
else if ($part > 0) {
return (is_array($a[$part-1])) ? $a[$part-1] : $a;
}
}
/**
* Creates next command identifier (tag)
*
* @return string Command identifier
* @since 0.5-beta
*/
function nextTag()
{
$this->cmd_num++;
$this->cmd_tag = sprintf('A%04d', $this->cmd_num);
return $this->cmd_tag;
}
/**
* Sends IMAP command and parses result
*
* @param string $command IMAP command
* @param array $arguments Command arguments
* @param int $options Execution options
*
* @return mixed Response code or list of response code and data
* @since 0.5-beta
*/
function execute($command, $arguments=array(), $options=0)
{
$tag = $this->nextTag();
$query = $tag . ' ' . $command;
$noresp = ($options & self::COMMAND_NORESPONSE);
$response = $noresp ? null : '';
if (!empty($arguments)) {
foreach ($arguments as $arg) {
$query .= ' ' . self::r_implode($arg);
}
}
// Send command
if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
}
// Parse response
do {
$line = $this->readLine(4096);
if ($response !== null) {
$response .= $line;
}
} while (!$this->startsWith($line, $tag . ' ', true, true));
$code = $this->parseResult($line, $command . ': ');
// Remove last line from response
if ($response) {
$line_len = min(strlen($response), strlen($line) + 2);
$response = substr($response, 0, -$line_len);
}
// optional CAPABILITY response
if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
&& preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
) {
$this->parseCapability($matches[1], true);
}
// return last line only (without command tag, result and response code)
if ($line && ($options & self::COMMAND_LASTLINE)) {
$response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
}
return $noresp ? $code : array($code, $response);
}
/**
* Splits IMAP response into string tokens
*
* @param string &$str The IMAP's server response
* @param int $num Number of tokens to return
*
* @return mixed Tokens array or string if $num=1
* @since 0.5-beta
*/
static function tokenizeResponse(&$str, $num=0)
{
$result = array();
while (!$num || count($result) < $num) {
// remove spaces from the beginning of the string
$str = ltrim($str);
switch ($str[0]) {
// String literal
case '{':
if (($epos = strpos($str, "}\r\n", 1)) == false) {
// error
}
if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
// error
}
$result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
// Advance the string
$str = substr($str, $epos + 3 + $bytes);
break;
// Quoted string
case '"':
$len = strlen($str);
for ($pos=1; $pos<$len; $pos++) {
if ($str[$pos] == '"') {
break;
}
if ($str[$pos] == "\\") {
if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
$pos++;
}
}
}
if ($str[$pos] != '"') {
// error
}
// we need to strip slashes for a quoted string
$result[] = stripslashes(substr($str, 1, $pos - 1));
$str = substr($str, $pos + 1);
break;
// Parenthesized list
case '(':
$str = substr($str, 1);
$result[] = self::tokenizeResponse($str);
break;
case ')':
$str = substr($str, 1);
return $result;
break;
// String atom, number, astring, NIL, *, %
default:
// empty string
if ($str === '' || $str === null) {
break 2;
}
// excluded chars: SP, CTL, ), DEL
// we do not exclude [ and ] (#1489223)
if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
$result[] = $m[1] == 'NIL' ? NULL : $m[1];
$str = substr($str, strlen($m[1]));
}
break;
}
}
return $num == 1 ? $result[0] : $result;
}
static function r_implode($element)
{
$string = '';
if (is_array($element)) {
reset($element);
foreach ($element as $value) {
$string .= ' ' . self::r_implode($value);
}
}
else {
return $element;
}
return '(' . trim($string) . ')';
}
/**
* Converts message identifiers array into sequence-set syntax
*
* @param array $messages Message identifiers
* @param bool $force Forces compression of any size
*
* @return string Compressed sequence-set
*/
static function compressMessageSet($messages, $force=false)
{
// given a comma delimited list of independent mid's,
// compresses by grouping sequences together
if (!is_array($messages)) {
// if less than 255 bytes long, let's not bother
if (!$force && strlen($messages)<255) {
return $messages;
}
// see if it's already been compressed
if (strpos($messages, ':') !== false) {
return $messages;
}
// separate, then sort
$messages = explode(',', $messages);
}
sort($messages);
$result = array();
$start = $prev = $messages[0];
foreach ($messages as $id) {
$incr = $id - $prev;
if ($incr > 1) { // found a gap
if ($start == $prev) {
$result[] = $prev; // push single id
} else {
$result[] = $start . ':' . $prev; // push sequence as start_id:end_id
}
$start = $id; // start of new sequence
}
$prev = $id;
}
// handle the last sequence/id
if ($start == $prev) {
$result[] = $prev;
} else {
$result[] = $start.':'.$prev;
}
// return as comma separated string
return implode(',', $result);
}
/**
* Converts message sequence-set into array
*
* @param string $messages Message identifiers
*
* @return array List of message identifiers
*/
static function uncompressMessageSet($messages)
{
if (empty($messages)) {
return array();
}
$result = array();
$messages = explode(',', $messages);
foreach ($messages as $idx => $part) {
$items = explode(':', $part);
$max = max($items[0], $items[1]);
for ($x=$items[0]; $x<=$max; $x++) {
$result[] = (int)$x;
}
unset($messages[$idx]);
}
return $result;
}
- private function _xor($string, $string2)
+ protected function _xor($string, $string2)
{
$result = '';
$size = strlen($string);
for ($i=0; $i<$size; $i++) {
$result .= chr(ord($string[$i]) ^ ord($string2[$i]));
}
return $result;
}
/**
* Converts flags array into string for inclusion in IMAP command
*
* @param array $flags Flags (see self::flags)
*
* @return string Space-separated list of flags
*/
- private function flagsToStr($flags)
+ protected function flagsToStr($flags)
{
foreach ((array)$flags as $idx => $flag) {
if ($flag = $this->flags[strtoupper($flag)]) {
$flags[$idx] = $flag;
}
}
return implode(' ', (array)$flags);
}
/**
* Converts datetime string into unix timestamp
*
* @param string $date Date string
*
* @return int Unix timestamp
*/
static function strToTime($date)
{
// Clean malformed data
$date = preg_replace(
array(
'/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
'/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters
'/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
),
array(
'\\1',
'',
'',
), $date);
$date = trim($date);
// if date parsing fails, we have a date in non-rfc format
// remove token from the end and try again
while (($ts = intval(@strtotime($date))) <= 0) {
$d = explode(' ', $date);
array_pop($d);
if (empty($d)) {
break;
}
$date = implode(' ', $d);
}
return $ts < 0 ? 0 : $ts;
}
/**
* CAPABILITY response parser
*/
- private function parseCapability($str, $trusted=false)
+ protected function parseCapability($str, $trusted=false)
{
$str = preg_replace('/^\* CAPABILITY /i', '', $str);
$this->capability = explode(' ', strtoupper($str));
if (!empty($this->prefs['disabled_caps'])) {
$this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
}
if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
$this->prefs['literal+'] = true;
}
if (preg_match('/(\[| )MUPDATE=.*/', $str)) {
self::$mupdate = true;
}
if ($trusted) {
$this->capability_readed = true;
}
}
/**
* Escapes a string when it contains special characters (RFC3501)
*
* @param string $string IMAP string
* @param boolean $force_quotes Forces string quoting (for atoms)
*
* @return string String atom, quoted-string or string literal
* @todo lists
*/
static function escape($string, $force_quotes=false)
{
if ($string === null) {
return 'NIL';
}
if ($string === '') {
return '""';
}
// atom-string (only safe characters)
if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
return $string;
}
// quoted-string
if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
return '"' . addcslashes($string, '\\"') . '"';
}
// literal-string
return sprintf("{%d}\r\n%s", strlen($string), $string);
}
/**
* Set the value of the debugging flag.
*
* @param boolean $debug New value for the debugging flag.
* @param callback $handler Logging handler function
*
* @since 0.5-stable
*/
function setDebug($debug, $handler = null)
{
$this->_debug = $debug;
$this->_debug_handler = $handler;
}
/**
* Write the given debug text to the current debug output handler.
*
* @param string $message Debug mesage text.
*
* @since 0.5-stable
*/
- private function debug($message)
+ protected function debug($message)
{
if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$message = substr($message, 0, self::DEBUG_LINE_LENGTH)
. "... [truncated $diff bytes]";
}
if ($this->resourceid) {
$message = sprintf('[%s] %s', $this->resourceid, $message);
}
if ($this->_debug_handler) {
call_user_func_array($this->_debug_handler, array(&$this, $message));
} else {
echo "DEBUG: $message\n";
}
}
}
diff --git a/lib/ext/Roundcube/rcube_imap_search.php b/lib/ext/Roundcube/rcube_imap_search.php
new file mode 100644
index 0000000..365d78f
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_imap_search.php
@@ -0,0 +1,231 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) 2013, The Roundcube Dev Team |
+ | Copyright (C) 2014, 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: |
+ | Execute (multi-threaded) searches in multiple IMAP folders |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class to control search jobs on multiple IMAP folders.
+ *
+ * @package Framework
+ * @subpackage Storage
+ * @author Thomas Bruederli <roundcube@gmail.com>
+ */
+class rcube_imap_search
+{
+ public $options = array();
+
+ protected $jobs = array();
+ protected $timelimit = 0;
+ protected $results;
+ protected $conn;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($options, $conn)
+ {
+ $this->options = $options;
+ $this->conn = $conn;
+ }
+
+ /**
+ * Invoke search request to IMAP server
+ *
+ * @param array $folders List of IMAP folders to search in
+ * @param string $str Search criteria
+ * @param string $charset Search charset
+ * @param string $sort_field Header field to sort by
+ * @param boolean $threading True if threaded listing is active
+ */
+ public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null)
+ {
+ $start = floor(microtime(true));
+ $results = new rcube_result_multifolder($folders);
+
+ // start a search job for every folder to search in
+ foreach ($folders as $folder) {
+ // a complete result for this folder already exists
+ $result = $this->results ? $this->results->get_set($folder) : false;
+ if ($result && !$result->incomplete) {
+ $results->add($result);
+ }
+ else {
+ $search = is_array($str) && $str[$folder] ? $str[$folder] : $str;
+ $job = new rcube_imap_search_job($folder, $search, $charset, $sort_field, $threading);
+ $job->worker = $this;
+ $this->jobs[] = $job;
+ }
+ }
+
+ // execute jobs and gather results
+ foreach ($this->jobs as $job) {
+ // only run search if within the configured time limit
+ // TODO: try to estimate the required time based on folder size and previous search performance
+ if (!$this->timelimit || floor(microtime(true)) - $start < $this->timelimit) {
+ $job->run();
+ }
+
+ // add result (may have ->incomplete flag set)
+ $results->add($job->get_result());
+ }
+
+ return $results;
+ }
+
+ /**
+ * Setter for timelimt property
+ */
+ public function set_timelimit($seconds)
+ {
+ $this->timelimit = $seconds;
+ }
+
+ /**
+ * Setter for previous (potentially incomplete) search results
+ */
+ public function set_results($res)
+ {
+ $this->results = $res;
+ }
+
+ /**
+ * Get connection to the IMAP server
+ * (used for single-thread mode)
+ */
+ public function get_imap()
+ {
+ return $this->conn;
+ }
+}
+
+
+/**
+ * Stackable item to run the search on a specific IMAP folder
+ */
+class rcube_imap_search_job /* extends Stackable */
+{
+ private $folder;
+ private $search;
+ private $charset;
+ private $sort_field;
+ private $threading;
+ private $searchset;
+ private $result;
+ private $pagesize = 100;
+
+ public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false)
+ {
+ $this->folder = $folder;
+ $this->search = $str;
+ $this->charset = $charset;
+ $this->sort_field = $sort_field;
+ $this->threading = $threading;
+
+ $this->result = new rcube_result_index($folder);
+ $this->result->incomplete = true;
+ }
+
+ public function run()
+ {
+ $this->result = $this->search_index();
+ }
+
+ /**
+ * Copy of rcube_imap::search_index()
+ */
+ protected function search_index()
+ {
+ $criteria = $this->search;
+ $charset = $this->charset;
+ $imap = $this->worker->get_imap();
+
+ if (!$imap->connected()) {
+ trigger_error("No IMAP connection for $this->folder", E_USER_WARNING);
+
+ if ($this->threading) {
+ return new rcube_result_thread($this->folder);
+ }
+ else {
+ return new rcube_result_index($this->folder);
+ }
+ }
+
+ if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
+ $criteria = 'UNDELETED '.$criteria;
+ }
+
+ // unset CHARSET if criteria string is ASCII, this way
+ // SEARCH won't be re-sent after "unsupported charset" response
+ if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
+ $charset = 'US-ASCII';
+ }
+
+ if ($this->threading) {
+ $threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset);
+
+ // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+ // but I've seen that Courier doesn't support UTF-8)
+ if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
+ $threads = $imap->thread($this->folder, $this->threading,
+ rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+ }
+
+ return $threads;
+ }
+
+ if ($this->sort_field) {
+ $messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset);
+
+ // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+ // but I've seen Courier with disabled UTF-8 support)
+ if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+ $messages = $imap->sort($this->folder, $this->sort_field,
+ rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+ }
+ }
+
+ if (!$messages || $messages->is_error()) {
+ $messages = $imap->search($this->folder,
+ ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
+
+ // Error, try with US-ASCII (some servers may support only US-ASCII)
+ if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+ $messages = $imap->search($this->folder,
+ rcube_imap::convert_criteria($criteria, $charset), true);
+ }
+ }
+
+ return $messages;
+ }
+
+ public function get_search_set()
+ {
+ return array(
+ $this->search,
+ $this->result,
+ $this->charset,
+ $this->sort_field,
+ $this->threading,
+ );
+ }
+
+ public function get_result()
+ {
+ return $this->result;
+ }
+}
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index b3872e2..6805c49 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -1,2009 +1,2061 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2013, The Roundcube Dev Team |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| 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 Framework
* @subpackage 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();
public $export_groups = false;
// private properties
protected $ldap;
protected $prop = array();
protected $fieldmap = array();
protected $filter = '';
protected $sub_filter;
protected $result;
protected $ldap_result;
protected $mail_domain = '';
protected $debug = false;
/**
* Group objectclass (lowercase) to member attribute mapping
*
* @var array
*/
private $group_types = array(
'group' => 'member',
'groupofnames' => 'member',
'kolabgroupofnames' => 'member',
'groupofuniquenames' => 'uniqueMember',
'kolabgroupofuniquenames' => 'uniqueMember',
'univentiongroup' => 'uniqueMember',
'groupofurls' => null,
);
private $base_dn = '';
private $groups_base_dn = '';
private $group_url;
+ private $group_data;
+ private $group_search_cache;
private $cache;
/**
* 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;
$fetch_attributes = array('objectClass');
// 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';
// extend group objectclass => member attribute mapping
if (!empty($this->prop['groups']['class_member_attr']))
$this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']);
// add group name attrib to the list of attributes to be fetched
$fetch_attributes[] = $this->prop['groups']['name_attr'];
}
- if (is_array($p['group_filters']) && count($p['group_filters'])) {
- $this->groups = true;
+ if (is_array($p['group_filters'])) {
+ $this->groups = $this->groups || count($p['group_filters']);
foreach ($p['group_filters'] as $k => $group_filter) {
// set default name attribute to cn
if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr']))
$this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn';
if ($group_filter['name_attr'])
$fetch_attributes[] = $group_filter['name_attr'];
}
}
// 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, 'attributes' => array($lf));
}
elseif ($type) {
$this->coltypes[$col]['subtypes'][] = $type;
$this->coltypes[$col]['attributes'][] = $lf;
$this->coltypes[$col]['limit'] += $limit;
}
if ($delim)
$this->coltypes[$col]['serialized'][$type] = $delim;
$this->fieldmap[$colv] = $lf;
}
// support for composite address
if ($this->coltypes['street'] && $this->coltypes['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->coltypes[$childcol]) {
$this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
unset($this->coltypes[$childcol]); // remove address child col from global coltypes list
}
}
// at least one address type must be specified
if (empty($this->coltypes['address']['subtypes'])) {
$this->coltypes['address']['subtypes'] = array('home');
}
}
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'])
&& !in_array($this->prop['LDAP_rdn'], array_keys((array)$this->prop['autovalues']))) {
$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 $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();
if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
$cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
$cache_name = 'LDAP.' . asciiwords($this->prop['name']);
$this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
}
// determine which attributes to fetch
$this->prop['list_attributes'] = array_unique($fetch_attributes);
$this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
foreach ($rcube->config->get('contactlist_fields') as $col) {
$this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
}
// initialize ldap wrapper object
$this->ldap = new rcube_ldap_generic($this->prop);
- $this->ldap->set_cache($this->cache);
- $this->ldap->set_debug($this->debug);
+ $this->ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
$this->_connect();
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
$rcube = rcube::get_instance();
if ($this->ready)
return true;
if (!is_array($this->prop['hosts']))
$this->prop['hosts'] = array($this->prop['hosts']);
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ($this->prop['hosts'] as $host) {
// skip host if connection failed
if (!$this->ldap->connect($host)) {
continue;
}
// See if the directory is writeable.
if ($this->prop['writable']) {
$this->readonly = 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->get_user_password();
}
// Get the pieces needed for variable replacement.
if ($fu = $rcube->get_user_email())
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);
// Search for the dn to use to authenticate
if ($this->prop['search_base_dn'] && $this->prop['search_filter']
&& (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn'))
) {
$search_attribs = array('uid');
if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) {
foreach ($search_bind_attrib as $r => $attr) {
$search_attribs[] = $attr;
$replaces[$r] = '';
}
}
$search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
$search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
$search_filter = strtr($this->prop['search_filter'], $replaces);
$cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:"
.$this->prop['search_bind_pw']);
if ($this->cache && ($dn = $this->cache->get($cache_key))) {
$replaces['%dn'] = $dn;
}
else {
$ldap = $this->ldap;
if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) {
// To protect from "Critical extension is unavailable" error
// we need to use a separate LDAP connection
if (!empty($this->prop['vlv'])) {
$ldap = new rcube_ldap_generic($this->prop);
- $ldap->set_debug($this->debug);
- $ldap->set_cache($this->cache);
+ $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
if (!$ldap->connect($host)) {
continue;
}
}
if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) {
continue; // bind failed, try next host
}
}
$res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
if ($res) {
$res->rewind();
- $replaces['%dn'] = $res->get_dn();
+ $replaces['%dn'] = key($res->entries(TRUE));
// add more replacements from 'search_bind_attrib' config
if ($search_bind_attrib) {
$res = $res->current();
foreach ($search_bind_attrib as $r => $attr) {
$replaces[$r] = $res[$attr][0];
}
}
}
if ($ldap != $this->ldap) {
$ldap->close();
}
}
// 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);
continue;
}
}
if ($this->cache && !empty($replaces['%dn'])) {
$this->cache->set($cache_key, $replaces['%dn']);
}
}
// 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);
// replace placeholders in filter settings
if (!empty($this->prop['filter']))
$this->prop['filter'] = strtr($this->prop['filter'], $replaces);
foreach (array('base_dn','filter','member_filter') as $k) {
if (!empty($this->prop['groups'][$k]))
$this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces);
}
- if (!empty($this->prop['group_filters'])) {
+ if (is_array($this->prop['group_filters'])) {
foreach ($this->prop['group_filters'] as $i => $gf) {
if (!empty($gf['base_dn']))
$this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
if (!empty($gf['filter']))
$this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces);
}
}
if (empty($bind_user)) {
$bind_user = $u;
}
}
if (empty($bind_pass)) {
$this->ready = true;
}
else {
if (!empty($bind_dn)) {
$this->ready = $this->ldap->bind($bind_dn, $bind_pass);
}
else if (!empty($this->prop['auth_cid'])) {
$this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
}
else {
$this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
}
}
// connection established, we're done here
if ($this->ready) {
break;
}
} // end foreach hosts
if (!is_resource($this->ldap->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to any LDAP server, last tried $host"), true);
return false;
}
return $this->ready;
}
/**
* Close connection to LDAP server
*/
function close()
{
if ($this->ldap) {
$this->ldap->close();
}
}
/**
* Returns address book name
*
* @return string Address book name
*/
function get_name()
{
return $this->prop['name'];
}
/**
* Set internal list page
*
* @param number Page number to list
*/
function set_page($page)
{
$this->list_page = (int)$page;
$this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
/**
* Set internal page size
*
* @param number Number of records to display on one page
*/
function set_pagesize($size)
{
$this->page_size = (int)$size;
$this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
/**
* 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->coltypes[$sort_col]['attributes'])
$this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
}
/**
* 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 {
$prop = $this->group_id ? $this->group_data : $this->prop;
$base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn;
// use global search filter
if (!empty($this->filter))
$prop['filter'] = $this->filter;
// exec LDAP search if no result resource is stored
if ($this->ready && !$this->ldap_result)
$this->ldap_result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
// 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 && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
$this->ldap_result->sort($this->sort_col);
// get all entries from the ldap server
$entries = $this->ldap_result->entries();
}
} // end else
// start and end of the page
$start_row = $this->ldap->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 boolean Count only
* @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)) {
$attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types));
$entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
if ($entries === false) {
return $group_members;
}
}
for ($i=0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
$attrs = array();
foreach ((array)$entry['objectclass'] as $objectclass) {
if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
&& ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
) {
$members = $this->_list_group_members($dn, $entry, $member_attr, $count);
$group_members = array_merge($group_members, $members);
$attrs[] = $member_attr;
}
else if (!empty($entry['memberurl'])) {
$members = $this->_list_group_memberurl($dn, $entry, $count);
$group_members = array_merge($group_members, $members);
}
if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
break 2;
}
}
}
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
* @param boolean Count only
* @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','objectClass') : $this->prop['list_attributes'];
$attrib = array_merge($attrib, array_values($this->group_types));
$attrib[] = 'memberURL';
$filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
for ($i=0; $i < $entry[$attr]['count']; $i++) {
if (empty($entry[$attr][$i]))
continue;
$members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
if ($members == false) {
$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];
$attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
$entries = $result->entries();
for ($j = 0; $j < $entries['count']; $j++) {
if ($this->is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
$group_members = array_merge($group_members, $nested_group_members);
else
$group_members[] = $entries[$j];
}
}
}
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
$rcube = rcube::get_instance();
$list_fields = $rcube->config->get('contactlist_fields');
if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
$this->result = new rcube_result_set(0);
$search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
$ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
if ($ldap_data === false) {
return $this->result;
}
// get all entries of this page and post-filter those that really match the query
$search = mb_strtolower($value);
foreach ($ldap_data as $i => $entry) {
$rec = $this->_ldap2result($entry);
foreach ($fields as $f) {
foreach ((array)$rec[$f] as $val) {
if ($this->compare_search_value($f, $val, $search, $mode)) {
$this->result->add($rec);
$this->result->count++;
break 2;
}
}
}
}
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" . rcube_ldap_generic::quote_string($value) . "$ws)";
}
}
}
else {
foreach ((array)$fields as $idx => $field) {
$val = is_array($value) ? $value[$idx] : $value;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
$filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
}
}
$filter .= ')';
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
if (in_array($field, (array)$fields)) // required field is already in search filter
continue;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$req_filter .= '(|';
foreach ($attrs as $f)
$req_filter .= "($f=*)";
if (count($attrs) > 1)
$req_filter .= ')';
}
}
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);
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->ldap_result) {
$count = $this->ldap_result->count();
}
else if ($this->group_id && $this->group_data['dn']) {
$count = count($this->list_group_members($this->group_data['dn'], true));
}
// We have a connection but no result set, attempt to get one.
else if ($this->ready) {
$prop = $this->group_id ? $this->group_data : $this->prop;
$base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
if (!empty($this->filter)) { // Use global search filter
$prop['filter'] = $this->filter;
}
$count = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], array('dn'), $prop, 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->ready && $dn) {
$dn = self::dn_decode($dn);
if ($rec = $this->ldap->get_entry($dn)) {
$rec = array_change_key_case($rec, CASE_LOWER);
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
if ($entries = $this->ldap->list_entries($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(1);
$this->result->add($res);
}
}
return $assoc ? $res : $this->result;
}
+ /**
+ * Returns the last error occurred (e.g. when updating/inserting failed)
+ *
+ * @return array Hash array with the following fields: type, message
+ */
+ function get_error()
+ {
+ $err = $this->error;
+
+ // check ldap connection for errors
+ if (!$err && $this->ldap->get_error()) {
+ $err = array(self::ERROR_SEARCH, $this->ldap->get_error());
+ }
+
+ return $err;
+ }
+
/**
* 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
$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'];
// add automatically generated attributes
$this->add_autovalues($newentry);
// 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'].'='.rcube_ldap_generic::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 (array_keys($this->prop['sub_fields']) as $xf) {
if (!empty($newentry[$xf])) {
$xfields[$xf] = $newentry[$xf];
unset($newentry[$xf]);
}
}
}
- if (!$this->ldap->add($dn, $newentry)) {
+ if (!$this->ldap->add_entry($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
$xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
- $this->ldap->add($xdn, $xf);
+ $this->ldap->add_entry($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 $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'].'='
.rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->base_dn;
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
.rcube_ldap_generic::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.'='.rcube_ldap_generic::quote_string($val).','.$dn;
- if (!$this->ldap->delete($subdn)) {
+ if (!$this->ldap->delete_entry($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 (array_keys($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.'='.rcube_ldap_generic::quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
- $this->ldap->add($subdn, $xf);
+ $this->ldap->add_entry($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_entries($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
- if (!$this->ldap->delete($entry['dn'])) {
+ if (!$this->ldap->delete_entry($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
}
}
// Delete the record.
- if (!$this->ldap->delete($dn)) {
+ if (!$this->ldap->delete_entry($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- // remove contact from all groups where he was member
+ // remove contact from all groups where he was a member
if ($this->groups) {
$dn = self::dn_encode($dn);
$group_ids = $this->get_record_groups($dn);
foreach (array_keys($group_ids) as $group_id) {
$this->remove_from_group($group_id, $dn);
}
}
} // end foreach
return count($ids);
}
/**
* Remove all contact records
*
* @param bool $with_groups Delete also groups if enabled
*/
function delete_all($with_groups = false)
{
// searching for contact entries
$dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
$dn_list[$idx] = self::dn_encode($entry['dn']);
}
$this->delete($dn_list);
}
if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) {
foreach ($groups as $group) {
- $this->ldap->delete($group['dn']);
+ $this->ldap->delete_entry($group['dn']);
}
if ($this->cache) {
$this->cache->remove('groups');
}
}
}
/**
* Generate missing attributes as configured
*
* @param array LDAP record attributes
*/
protected function add_autovalues(&$attrs)
{
if (empty($this->prop['autovalues'])) {
return;
}
$attrvals = array();
foreach ($attrs as $k => $v) {
$attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v;
}
foreach ((array)$this->prop['autovalues'] as $lf => $templ) {
if (empty($attrs[$lf])) {
if (strpos($templ, '(') !== false) {
// replace {attr} placeholders with (escaped!) attribute values to be safely eval'd
$code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals)));
$fn = create_function('', "return ($code);");
if (!$fn) {
rcube::raise_error(array(
'code' => 505, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Expression parse error on: ($code)"), true, false);
continue;
}
$attrs[$lf] = $fn();
}
else {
// replace {attr} placeholders with concrete attribute values
$attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
}
}
}
}
/**
* Converts LDAP entry into an array
*/
private function _ldap2result($rec)
{
$out = array('_type' => 'person');
$fieldmap = $this->fieldmap;
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
// determine record type
if ($this->is_group_entry($rec)) {
$out['_type'] = 'group';
$out['readonly'] = true;
$fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
}
+ // assign object type from object class mapping
+ if (!empty($this->prop['class_type_map'])) {
+ foreach (array_map('strtolower', (array)$rec['objectclass']) as $objcls) {
+ if (!empty($this->prop['class_type_map'][$objcls])) {
+ $out['_type'] = $this->prop['class_type_map'][$objcls];
+ break;
+ }
+ }
+ }
+
foreach ($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 ($col == '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 LDAP attribute(s) for the given field
*/
private function _map_field($field)
{
return (array)$this->coltypes[$field]['attributes'];
}
/**
* 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) {
$values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
$save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
}
}
}
}
$ldap_data = array();
foreach ($this->fieldmap as $rf => $fld) {
$val = $save_cols[$rf];
// check for value in base field (eg.g email instead of email:foo)
list($col, $subtype) = explode(':', $rf);
if (!$val && !empty($save_cols[$col])) {
$val = $save_cols[$col];
unset($save_cols[$col]); // only use this value once
}
else if (!$val && !$subtype) { // extract values from subtype cols
$val = $this->get_col_values($col, $save_cols, true);
}
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;
}
/**
* Determines whether the given LDAP entry is a group record
*/
private function is_group_entry($entry)
{
$classes = array_map('strtolower', (array)$entry['objectclass']);
return count(array_intersect(array_keys($this->group_types), $classes)) > 0;
}
/**
* 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
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
if ($this->ldap) {
- $this->ldap->set_debug($dbg);
+ $this->ldap->config_set('debug', $dbg);
}
}
/**
* Setter for the current group
*/
function set_group($group_id)
{
if ($group_id) {
$this->group_id = $group_id;
$this->group_data = $this->get_group_entry($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
* @param int Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
if (!$this->groups) {
return array();
}
- $group_cache = $this->_fetch_groups();
+ $group_cache = $this->_fetch_groups($search, $mode);
$groups = array();
if ($search) {
foreach ($group_cache as $group) {
- if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
+ if ($this->compare_search_value('name', $group['name'], mb_strtolower($search), $mode)) {
$groups[] = $group;
}
}
}
else {
$groups = $group_cache;
}
return array_values($groups);
}
/**
* Fetch groups from server
*/
- private function _fetch_groups($vlv_page = null)
+ private function _fetch_groups($search = null, $mode = 0, $vlv_page = null)
{
+ // reset group search cache
+ if ($search !== null && $vlv_page === null) {
+ $this->group_search_cache = null;
+ }
+ // return in-memory cache from previous search results
+ else if (is_array($this->group_search_cache) && $vlv_page === null) {
+ return $this->group_search_cache;
+ }
+
// special case: list groups from 'group_filters' config
- if ($vlv_page === null && !empty($this->prop['group_filters'])) {
+ if ($vlv_page === null && $search === null && is_array($this->prop['group_filters'])) {
$groups = array();
$rcube = rcube::get_instance();
// list regular groups configuration as special filter
if (!empty($this->prop['groups']['filter'])) {
$id = '__groups__';
$groups[$id] = array('ID' => $id, 'name' => $rcube->gettext('groups'), 'virtual' => true) + $this->prop['groups'];
}
foreach ($this->prop['group_filters'] as $id => $prop) {
$groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
}
return $groups;
}
- if ($this->cache && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
+ if ($this->cache && $search === null && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
return $groups;
}
$base_dn = $this->groups_base_dn;
$filter = $this->prop['groups']['filter'];
$scope = $this->prop['groups']['scope'];
$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];
$ldap = $this->ldap;
// use vlv to list groups
if ($this->prop['groups']['vlv']) {
$page_size = 200;
if (!$this->prop['groups']['sort']) {
$this->prop['groups']['sort'] = $sort_attrs;
}
$ldap = clone $this->ldap;
- $ldap->set_config($this->prop['groups']);
+ $ldap->config_set($this->prop['groups']);
$ldap->set_vlv_page($vlv_page+1, $page_size);
}
- $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
- $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $this->prop['groups']);
+ $props = array('sort' => $this->prop['groups']['sort']);
+ $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
+
+ // add search filter
+ if ($search !== null) {
+ // set wildcards
+ $wp = $ws = '';
+ if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
+ $ws = '*';
+ $wp = !$mode ? '*' : '';
+ }
+ $filter = "(&$filter($name_attr=$wp" . rcube_ldap_generic::quote_string($search) . "$ws))";
+ $props['search'] = $wp . $search . $ws;
+ }
+
+ $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $props);
if ($ldap_data === false) {
return array();
}
$groups = array();
$group_sortnames = array();
$group_count = $ldap_data->count();
foreach ($ldap_data as $entry) {
if (!$entry['dn']) // DN is mandatory
$entry['dn'] = $ldap_data->get_dn();
$group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
$group_id = self::dn_encode($entry['dn']);
$groups[$group_id]['ID'] = $group_id;
$groups[$group_id]['dn'] = $entry['dn'];
$groups[$group_id]['name'] = $group_name;
$groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
// list email attributes of a group
for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
if (strpos($entry[$email_attr][$j], '@') > 0)
$groups[$group_id]['email'][] = $entry[$email_attr][$j];
}
$group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);
}
// recursive call can exit here
if ($vlv_page > 0) {
return $groups;
}
// call recursively until we have fetched all groups
while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
- $next_page = $this->_fetch_groups(++$vlv_page);
+ $next_page = $this->_fetch_groups($search, $mode, ++$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
- if ($this->cache) {
+ if ($this->cache && $search === null) {
$this->cache->set('groups', $groups);
}
+ else if ($search !== null) {
+ $this->group_search_cache = $groups;
+ }
return $groups;
}
/**
* Fetch a group entry from LDAP and save in local cache
*/
private function get_group_entry($group_id)
{
$group_cache = $this->_fetch_groups();
// add group record to cache if it isn't yet there
if (!isset($group_cache[$group_id])) {
$name_attr = $this->prop['groups']['name_attr'];
$dn = self::dn_decode($group_id);
if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
$entry = $list[0];
$group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
$group_cache[$group_id]['ID'] = $group_id;
$group_cache[$group_id]['dn'] = $dn;
$group_cache[$group_id]['name'] = $group_name;
$group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
}
else {
$group_cache[$group_id] = false;
}
if ($this->cache) {
$this->cache->set('groups', $group_cache);
}
}
return $group_cache[$group_id];
}
/**
* 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)
{
$group_data = $this->get_group_entry($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)
{
$new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
$new_gid = self::dn_encode($new_dn);
$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)) {
+ if (!$this->ldap->add_entry($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
if ($this->cache) {
$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)
{
$group_cache = $this->_fetch_groups();
$del_dn = $group_cache[$group_id]['dn'];
- if (!$this->ldap->delete($del_dn)) {
+ if (!$this->ldap->delete_entry($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
if ($this->cache) {
unset($group_cache[$group_id]);
$this->cache->set('groups', $group_cache);
}
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)
{
$group_cache = $this->_fetch_groups();
$old_dn = $group_cache[$group_id]['dn'];
$new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
$new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return $new_name;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $contact_ids)
{
$group_cache = $this->_fetch_groups();
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = $group_cache[$group_id]['dn'];
$new_attrs = array();
if (!is_array($contact_ids)) {
$contact_ids = explode(',', $contact_ids);
}
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;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return count($new_attrs[$member_attr]);
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $contact_ids)
{
$group_cache = $this->_fetch_groups();
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = $group_cache[$group_id]['dn'];
$del_attrs = array();
if (!is_array($contact_ids)) {
$contact_ids = explode(',', $contact_ids);
}
foreach ($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;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return count($del_attrs[$member_attr]);
}
/**
* 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('\\' => '\\\\'));
$ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
if ($res === false) {
return array();
}
$groups = array();
foreach ($ldap_data as $entry) {
if (!$entry['dn'])
$entry['dn'] = $ldap_data->get_dn();
$group_name = $entry[$name_attr][0];
$group_id = self::dn_encode($entry['dn']);
$groups[$group_id] = $group_name;
}
return $groups;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array(), $default = 'member')
{
if (empty($object_classes)) {
$object_classes = $this->prop['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
if ($attr = $this->group_types[strtolower($oc)]) {
return $attr;
}
}
}
if (!empty($this->prop['groups']['member_attr'])) {
return $this->prop['groups']['member_attr'];
}
return $default;
}
/**
* 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);
}
}
diff --git a/lib/ext/Roundcube/rcube_ldap_generic.php b/lib/ext/Roundcube/rcube_ldap_generic.php
index 1cd0b5a..a76ad6d 100644
--- a/lib/ext/Roundcube/rcube_ldap_generic.php
+++ b/lib/ext/Roundcube/rcube_ldap_generic.php
@@ -1,1060 +1,322 @@
<?php
/*
+-----------------------------------------------------------------------+
| Roundcube/rcube_ldap_generic.php |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
- | Copyright (C) 2012-2013, Kolab Systems AG |
+ | Copyright (C) 2006-2014, The Roundcube Dev Team |
+ | Copyright (C) 2012-2014, 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: |
| Provide basic functionality for accessing LDAP directories |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
-
-/*
- LDAP connection properties
- --------------------------
-
- $prop = array(
- 'host' => '<ldap-server-address>',
- // or
- 'hosts' => array('directory.verisign.com'),
- 'port' => 389,
- 'use_tls' => true|false,
- 'ldap_version' => 3, // using LDAPv3
- 'auth_method' => '', // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
- 'attributes' => array('dn'), // List of attributes to read from the server
- 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
- 'config_root_dn' => 'cn=config', // Root DN to read config (e.g. vlv indexes) from
- 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
- 'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
- 'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
- 'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
- 'referrals' => true|false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
- );
-*/
-
/**
* Model class to access an LDAP directories
*
* @package Framework
* @subpackage LDAP
*/
-class rcube_ldap_generic
+class rcube_ldap_generic extends Net_LDAP3
{
- const UPDATE_MOD_ADD = 1;
- const UPDATE_MOD_DELETE = 2;
- const UPDATE_MOD_REPLACE = 4;
- const UPDATE_MOD_FULL = 7;
-
- public $conn;
- public $vlv_active = false;
-
/** private properties */
protected $cache = null;
- protected $config = array();
protected $attributes = array('dn');
- protected $entries = null;
- protected $result = null;
- protected $debug = false;
- protected $list_page = 1;
- protected $page_size = 10;
- protected $vlv_config = null;
-
+ protected $error;
- /**
- * Object constructor
- *
- * @param array $p LDAP connection properties
- */
- function __construct($p)
+ function __construct($config = null)
{
- $this->config = $p;
+ parent::__construct($config);
- if (is_array($p['attributes']))
- $this->attributes = $p['attributes'];
-
- if (!is_array($p['hosts']) && !empty($p['host']))
- $this->config['hosts'] = array($p['host']);
+ $this->config_set('log_hook', array($this, 'log'));
}
/**
- * Activate/deactivate debug mode
- *
- * @param boolean $dbg True if LDAP commands should be logged
+ * Establish a connection to the LDAP server
*/
- public function set_debug($dbg = true)
+ public function connect($host = null)
{
- $this->debug = $dbg;
- }
+ // Net_LDAP3 does not support IDNA yet
+ // also parse_host() here is very Roundcube specific
+ $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
- /**
- * Set connection options
- *
- * @param mixed $opt Option name as string or hash array with multiple options
- * @param mixed $val Option value
- */
- public function set_config($opt, $val = null)
- {
- if (is_array($opt))
- $this->config = array_merge($this->config, $opt);
- else
- $this->config[$opt] = $value;
+ return parent::connect($host);
}
/**
- * Enable caching by passing an instance of rcube_cache to be used by this object
+ * Get a specific LDAP entry, identified by its DN
*
- * @param object rcube_cache Instance or False to disable caching
- */
- public function set_cache($cache_engine)
- {
- $this->cache = $cache_engine;
- }
-
- /**
- * Set properties for VLV-based paging
+ * @param string $dn Record identifier
*
- * @param number $page Page number to list (starting at 1)
- * @param number $size Number of entries to display on one page
+ * @return array Hash array
*/
- public function set_vlv_page($page, $size = 10)
+ function get_entry($dn)
{
- $this->list_page = $page;
- $this->page_size = $size;
+ return parent::get_entry($dn, $this->attributes);
}
/**
- * Establish a connection to the LDAP server
- */
- public function connect($host = null)
+ * Prints debug/error info to the log
+ */
+ public function log($level, $msg)
{
- 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);
- return false;
- }
-
- if (is_resource($this->conn) && $this->config['host'] == $host)
- return true;
+ $msg = implode("\n", $msg);
- if (empty($this->config['ldap_version']))
- $this->config['ldap_version'] = 3;
-
- // iterate over hosts if none specified
- if (!$host) {
- if (!is_array($this->config['hosts']))
- $this->config['hosts'] = array($this->config['hosts']);
-
- foreach ($this->config['hosts'] as $host) {
- if ($this->connect($host)) {
- return true;
- }
- }
-
- return false;
- }
-
- // open connection to the given $host
- $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
- $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : '');
-
- $this->_debug("C: Connect to $hostname [{$this->config['name']}]");
-
- if ($lc = @ldap_connect($host, $this->config['port'])) {
- if ($this->config['use_tls'] === true) {
- if (!ldap_start_tls($lc)) {
- return false;
- }
+ switch ($level) {
+ case LOG_DEBUG:
+ case LOG_INFO:
+ case LOG_NOTICE:
+ if ($this->config['debug']) {
+ rcube::write_log('ldap', $msg);
}
+ break;
- $this->_debug("S: OK");
-
- ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']);
- $this->config['host'] = $host;
- $this->conn = $lc;
-
- if (!empty($this->config['network_timeout']))
- ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']);
-
- if (isset($this->config['referrals']))
- ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']);
-
- if (isset($this->config['dereference']))
- ldap_set_option($lc, LDAP_OPT_DEREF, $this->config['dereference']);
- }
- else {
- $this->_debug("S: NOT OK");
- }
+ case LOG_EMERGE:
+ case LOG_ALERT:
+ case LOG_CRIT:
+ rcube::raise_error($msg, true, true);
+ break;
- 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;
+ case LOG_ERR:
+ case LOG_WARNING:
+ $this->error = $msg;
+ rcube::raise_error($msg, true, false);
+ break;
}
-
- return true;
}
/**
- * Bind connection with (SASL-) user and password
+ * Returns the last LDAP error occurred
*
- * @param string $authc Authentication user
- * @param string $pass Bind password
- * @param string $authz Autorization user
- *
- * @return boolean True on success, False on error
+ * @return mixed Error message string or null if no error occured
*/
- public function sasl_bind($authc, $pass, $authz=null)
+ function get_error()
{
- 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);
- return false;
- }
-
- if (!empty($authz)) {
- $authz = 'u:' . $authz;
- }
-
- if (!empty($this->config['auth_method'])) {
- $method = $this->config['auth_method'];
- }
- else {
- $method = 'DIGEST-MD5';
- }
-
- $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: **** [" . strlen($pass) . "]");
-
- if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
- $this->_debug("S: OK");
- 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' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)),
- true);
- return false;
+ return $this->error;
}
/**
- * Bind connection with DN and password
- *
- * @param string $dn Bind DN
- * @param string $pass Bind password
- *
- * @return boolean True on success, False on error
+ * @deprecated
*/
- public function bind($dn, $pass)
- {
- if (!$this->conn) {
- return false;
- }
-
- $this->_debug("C: Bind $dn, pass: **** [" . strlen($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
- */
- public function close()
+ public function set_debug($dbg = true)
{
- if ($this->conn) {
- $this->_debug("C: Close");
- ldap_unbind($this->conn);
- $this->conn = null;
- }
+ $this->config['debug'] = (bool) $dbg;
}
/**
- * Return the last result set
- *
- * @return object rcube_ldap_result Result object
+ * @deprecated
*/
- function get_result()
+ public function set_cache($cache_engine)
{
- return $this->result;
+ $this->config['cache'] = $cache_engine;
}
/**
- * Get a specific LDAP entry, identified by its DN
- *
- * @param string $dn Record identifier
- * @return array Hash array
+ * @deprecated
*/
- function get_entry($dn)
- {
- $rec = null;
-
- if ($this->conn && $dn) {
- $this->_debug("C: Read $dn [(objectclass=*)]");
-
- if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) {
- $this->_debug("S: OK");
-
- if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
- $rec = ldap_get_attributes($this->conn, $entry);
- }
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
- }
-
- if (!empty($rec)) {
- $rec['dn'] = $dn; // Add in the dn for the entry.
- }
- }
-
- return $rec;
- }
-
- /**
- * Execute the LDAP search based on the stored credentials
- *
- * @param string $base_dn The base DN to query
- * @param string $filter The LDAP filter for search
- * @param string $scope The LDAP scope (list|sub|base)
- * @param array $attrs List of entry attributes to read
- * @param array $prop Hash array with query configuration properties:
- * - sort: array of sort attributes (has to be in sync with the VLV index)
- * - search: search string used for VLV controls
- * @param boolean $count_only Set to true if only entry count is requested
- *
- * @return mixed rcube_ldap_result object or number of entries (if count_only=true) or false on error
- */
- public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false)
+ public static function scope2func($scope, &$ns_function = null)
{
- if (!$this->conn) {
- return false;
- }
-
- if (empty($filter)) {
- $filter = '(objectclass=*)';
- }
-
- $this->_debug("C: Search $base_dn for $filter");
-
- $function = self::scope2func($scope, $ns_function);
-
- // find available VLV index for this query
- if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) {
- // when using VLV, we get the total count by...
- // ...either reading numSubOrdinates attribute
- if (($sub_filter = $this->config['numsub_filter']) &&
- ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
- ) {
- $counts = ldap_get_entries($this->conn, $result_count);
- for ($vlv_count = $j = 0; $j < $counts['count']; $j++)
- $vlv_count += $counts[$j]['numsubordinates'][0];
- $this->_debug("D: total numsubordinates = " . $vlv_count);
- }
- // ...or by fetching all records dn and count them
- else if (!function_exists('ldap_parse_virtuallist_control')) {
- $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true);
- }
-
- $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']);
- }
- else {
- $this->vlv_active = false;
- }
-
- // only fetch dn for count (should keep the payload low)
- if ($ldap_result = @$function($this->conn, $base_dn, $filter,
- $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['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, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
- ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
- $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
- }
- else {
- $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
- }
- }
- else if ($this->debug) {
- $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
- }
-
- $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count);
-
- return $count_only ? $this->result->count() : $this->result;
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
- }
-
- return false;
+ return self::scope_to_function($scope, $ns_function);
}
/**
- * Modify an LDAP entry on the server
- *
- * @param string $dn Entry DN
- * @param array $params Hash array of entry attributes
- * @param int $mode Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE)
+ * @deprecated
*/
- public function modify($dn, $parms, $mode = 255)
+ public function set_config($opt, $val = null)
{
- // TODO: implement this
-
- return false;
+ $this->config_set($opt, $val);
}
/**
- * Wrapper for ldap_add()
- *
- * @see ldap_add()
+ * @deprecated
*/
public function add($dn, $entry)
{
- $this->_debug("C: Add $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;
+ return $this->add_entry($dn, $entry);
}
/**
- * Wrapper for ldap_delete()
- *
- * @see ldap_delete()
+ * @deprecated
*/
public function delete($dn)
{
- $this->_debug("C: Delete $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;
+ return $this->delete_entry($dn);
}
/**
* Wrapper for ldap_mod_replace()
*
* @see ldap_mod_replace()
*/
public function mod_replace($dn, $entry)
{
$this->_debug("C: Replace $dn: ".print_r($entry, true));
if (!ldap_mod_replace($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $this->_error("ldap_mod_replace() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_add()
*
* @see ldap_mod_add()
*/
public function mod_add($dn, $entry)
{
$this->_debug("C: Add $dn: ".print_r($entry, true));
if (!ldap_mod_add($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $this->_error("ldap_mod_add() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_del()
*
* @see ldap_mod_del()
*/
public function mod_del($dn, $entry)
{
$this->_debug("C: Delete $dn: ".print_r($entry, true));
if (!ldap_mod_del($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $this->_error("ldap_mod_del() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_rename()
*
* @see ldap_rename()
*/
public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
{
$this->_debug("C: Rename $dn to $newrdn");
if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $this->_error("ldap_rename() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_list() + ldap_get_entries()
*
* @see ldap_list()
* @see ldap_get_entries()
*/
public function list_entries($dn, $filter, $attributes = array('dn'))
{
$list = array();
$this->_debug("C: List $dn [{$filter}]");
if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) {
$list = ldap_get_entries($this->conn, $result);
if ($list === false) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $this->_error("ldap_get_entries() failed with " . 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));
+ $this->_error("ldap_list() failed with " . ldap_error($this->conn));
}
return $list;
}
/**
* Wrapper for ldap_read() + ldap_get_entries()
*
* @see ldap_read()
* @see ldap_get_entries()
*/
public function read_entries($dn, $filter, $attributes = null)
{
$this->_debug("C: Read $dn [{$filter}]");
if ($this->conn && $dn) {
- if (!$attributes)
- $attributes = $this->attributes;
-
$result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
if ($result === false) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $this->_error("ldap_read() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return ldap_get_entries($this->conn, $result);
}
return false;
}
- /**
- * Choose the right PHP function according to scope property
- *
- * @param string $scope The LDAP scope (sub|base|list)
- * @param string $ns_function Function to be used for numSubOrdinates queries
- * @return string PHP function to be used to query directory
- */
- public static 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;
- }
-
- /**
- * Convert the given scope integer value to a string representation
- */
- public static function scopeint2str($scope)
- {
- switch ($scope) {
- case 2: return 'sub';
- case 1: return 'one';
- case 0: return 'base';
- default: $this->_debug("Scope $scope is not a valid scope integer");
- }
-
- return '';
- }
-
- /**
- * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters.
- *
- * @param string $val Value to quote
- * @return string The escaped value
- */
- public static function escape_value($val)
- {
- return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29',
- '\\'=>'\5c', '/'=>'\2f'));
- }
-
- /**
- * Escapes a DN value according to RFC 2253
- *
- * @param string $dn DN value o quote
- * @return string The escaped value
- */
- public static function escape_dn($dn)
- {
- return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b',
- '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c',
- '"'=>'\22', '#'=>'\23'));
- }
-
- /**
- * Normalize a LDAP result by converting entry attributes arrays into single values
- *
- * @param array $result LDAP result set fetched with ldap_get_entries()
- * @return array Hash array with normalized entries, indexed by their DNs
- */
- public static function normalize_result($result)
- {
- if (!is_array($result)) {
- return array();
- }
-
- $entries = array();
- for ($i = 0; $i < $result['count']; $i++) {
- $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i;
- $entries[$key] = self::normalize_entry($result[$i]);
- }
-
- return $entries;
- }
-
/**
* Turn an LDAP entry into a regular PHP array with attributes as keys.
*
* @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
*
- * @return array Hash array with attributes as keys
+ * @return array Hash array with attributes as keys
*/
public static function normalize_entry($entry)
{
if (!isset($entry['count'])) {
return $entry;
}
$rec = array();
for ($i=0; $i < $entry['count']; $i++) {
$attr = $entry[$i];
if ($entry[$attr]['count'] == 1) {
switch ($attr) {
case 'objectclass':
$rec[$attr] = array(strtolower($entry[$attr][0]));
break;
default:
$rec[$attr] = $entry[$attr][0];
break;
}
}
else {
for ($j=0; $j < $entry[$attr]['count']; $j++) {
$rec[$attr][$j] = $entry[$attr][$j];
}
}
}
return $rec;
}
-
- /**
- * Set server controls for Virtual List View (paginated listing)
- */
- private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
- {
- $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => self::_sort_ber_encode((array)$sort));
- $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
-
- $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; $search)");
-
- 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;
- }
-
- /**
- * 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;
- }
-
- /**
- * Quotes attribute value string
- *
- * @param string $str Attribute value
- * @param bool $dn True if the attribute is a DN
- *
- * @return string Quoted string
- */
- public 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);
- }
-
- /**
- * Prints debug info to the log
- */
- private function _debug($str)
- {
- if ($this->debug && class_exists('rcube')) {
- rcube::write_log('ldap', $str);
- }
- }
-
-
- /***************** Virtual List View (VLV) related utility functions **************** */
-
- /**
- * Return the search string value to be used in VLV controls
- */
- private function _vlv_search($sort, $search)
- {
- foreach ($search as $attr => $value) {
- if (!in_array(strtolower($attr), $sort)) {
- $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
- return null;
- } else {
- return $value;
- }
- }
- }
-
- /**
- * Find a VLV index matching the given query attributes
- *
- * @return string Sort attribute or False if no match
- */
- private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
- {
- if (!$this->config['vlv'] || $scope == 'base') {
- return false;
- }
-
- // get vlv config
- $vlv_config = $this->_read_vlv_config();
-
- if ($vlv = $vlv_config[$base_dn]) {
- $this->_debug("D: Found a VLV for $base_dn");
-
- if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
- $this->_debug("D: Filter matches");
- if ($vlv['scope'] == $scope) {
- // Not passing any sort attributes means you don't care
- if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
- return $vlv['sort'][0];
- }
- }
- else {
- $this->_debug("D: Scope does not match");
- }
- }
- else {
- $this->_debug("D: Filter does not match");
- }
- }
- else {
- $this->_debug("D: No VLV for $base_dn");
- }
-
- return false;
- }
-
- /**
- * Return VLV indexes and searches including necessary configuration
- * details.
- */
- private function _read_vlv_config()
- {
- if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
- return array();
- }
- // return hard-coded VLV config
- else if (is_array($this->config['vlv'])) {
- return $this->config['vlv'];
- }
-
- // return cached result
- if (is_array($this->vlv_config)) {
- return $this->vlv_config;
- }
-
- if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
- $this->vlv_config = $cached_config;
- return $this->vlv_config;
- }
-
- $this->vlv_config = array();
-
- $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
- $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
-
- if ($vlv_searches->count() < 1) {
- $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
- return array();
- }
-
- foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
- // Multiple indexes may exist
- $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
- $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
-
- // Reset this one for each VLV search.
- $_vlv_sort = array();
- foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
- $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
- }
-
- $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
- 'scope' => self::scopeint2str($vlv_search_attrs['vlvscope']),
- 'filter' => strtolower($vlv_search_attrs['vlvfilter']),
- 'sort' => $_vlv_sort,
- );
- }
-
- // cache this
- if ($this->cache)
- $this->cache->set('vlvconfig', $this->vlv_config);
-
- $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
-
- return $this->vlv_config;
- }
-
- /**
- * 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 static 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
-
- with 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 static 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;
- }
-
}
+
+// for backward compat.
+class rcube_ldap_result extends Net_LDAP3_Result {}
diff --git a/lib/ext/Roundcube/rcube_message.php b/lib/ext/Roundcube/rcube_message.php
index f24ec3e..169d00c 100644
--- a/lib/ext/Roundcube/rcube_message.php
+++ b/lib/ext/Roundcube/rcube_message.php
@@ -1,825 +1,1006 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2010, The Roundcube Dev Team |
+ | Copyright (C) 2008-2014, 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 mail message with all its data |
| and related functions |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a mail message with all its data
* and related functions
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_message
{
/**
* Instace of framework class.
*
* @var rcube
*/
private $app;
/**
* Instance of storage class
*
* @var rcube_storage
*/
private $storage;
/**
* Instance of mime class
*
* @var rcube_mime
*/
private $mime;
private $opt = array();
private $parse_alternative = false;
public $uid;
public $folder;
public $headers;
public $parts = array();
public $mime_parts = array();
public $inline_parts = array();
public $attachments = array();
public $subject = '';
public $sender = null;
public $is_safe = false;
+ const BODY_MAX_SIZE = 1048576; // 1MB
+
/**
* __construct
*
* Provide a uid, and parse message structure.
*
* @param string $uid The message UID.
* @param string $folder Folder name
*
* @see self::$app, self::$storage, self::$opt, self::$parts
*/
function __construct($uid, $folder = null)
{
+ // decode combined UID-folder identifier
+ if (preg_match('/^\d+-.+/', $uid)) {
+ list($uid, $folder) = explode('-', $uid, 2);
+ }
+
$this->uid = $uid;
$this->app = rcube::get_instance();
$this->storage = $this->app->get_storage();
$this->folder = strlen($folder) ? $folder : $this->storage->get_folder();
$this->storage->set_options(array('all_headers' => true));
// Set current folder
$this->storage->set_folder($this->folder);
$this->headers = $this->storage->get_message($uid);
if (!$this->headers) {
return;
}
$this->mime = new rcube_mime($this->headers->charset);
$this->subject = $this->headers->get('subject');
list(, $this->sender) = each($this->mime->decode_address_list($this->headers->from, 1));
$this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$this->folder.':'.$uid]));
$this->opt = array(
'safe' => $this->is_safe,
'prefer_html' => $this->app->config->get('prefer_html'),
'get_url' => $this->app->url(array(
'action' => 'get',
'mbox' => $this->storage->get_folder(),
'uid' => $uid))
);
if (!empty($this->headers->structure)) {
$this->get_mime_numbers($this->headers->structure);
$this->parse_structure($this->headers->structure);
}
else {
$this->body = $this->storage->get_body($uid);
}
// notify plugins and let them analyze this structured message object
$this->app->plugins->exec_hook('message_load', array('object' => $this));
}
/**
* Return a (decoded) message header
*
* @param string $name Header name
* @param bool $row Don't mime-decode the value
* @return string Header value
*/
public function get_header($name, $raw = false)
{
if (empty($this->headers)) {
return null;
}
return $this->headers->get($name, !$raw);
}
/**
* Set is_safe var and session data
*
* @param bool $safe enable/disable
*/
public function set_safe($safe = true)
{
$_SESSION['safe_messages'][$this->folder.':'.$this->uid] = $this->is_safe = $safe;
}
/**
* Compose a valid URL for getting a message part
*
* @param string $mime_id Part MIME-ID
* @param mixed $embed Mimetype class for parts to be embedded
* @return string URL or false if part does not exist
*/
public function get_part_url($mime_id, $embed = false)
{
if ($this->mime_parts[$mime_id])
return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1&_mimeclass=' . $embed : '');
else
return false;
}
/**
* Get content of a specific part of this message
*
* @param string $mime_id Part MIME-ID
* @param resource $fp File pointer to save the message part
* @param boolean $skip_charset_conv Disables charset conversion
* @param int $max_bytes Only read this number of bytes
* @param boolean $formatted Enables formatting of text/* parts bodies
*
* @return string Part content
+ * @deprecated
*/
public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if ($part = $this->mime_parts[$mime_id]) {
// stored in message structure (winmail/inline-uuencode)
if (!empty($part->body) || $part->encoding == 'stream') {
if ($fp) {
fwrite($fp, $part->body);
}
return $fp ? true : $part->body;
}
// get from IMAP
$this->storage->set_folder($this->folder);
return $this->storage->get_message_part($this->uid, $mime_id, $part,
NULL, $fp, $skip_charset_conv, $max_bytes, $formatted);
}
}
+ /**
+ * Get content of a specific part of this message
+ *
+ * @param string $mime_id Part ID
+ * @param boolean $formatted Enables formatting of text/* parts bodies
+ * @param int $max_bytes Only return/read this number of bytes
+ * @param mixed $mode NULL to return a string, -1 to print body
+ * or file pointer to save the body into
+ *
+ * @return string|bool Part content or operation status
+ */
+ public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null)
+ {
+ if (!($part = $this->mime_parts[$mime_id])) {
+ return;
+ }
+
+ // only text parts can be formatted
+ $formatted = $formatted && $part->ctype_primary == 'text';
+
+ // part body not fetched yet... save in memory if it's small enough
+ if ($part->body === null && is_numeric($mime_id) && $part->size < self::BODY_MAX_SIZE) {
+ $this->storage->set_folder($this->folder);
+ // Warning: body here should be always unformatted
+ $part->body = $this->storage->get_message_part($this->uid, $mime_id, $part,
+ null, null, true, 0, false);
+ }
+
+ // body stored in message structure (winmail/inline-uuencode)
+ if ($part->body !== null || $part->encoding == 'stream') {
+ $body = $part->body;
+
+ if ($formatted && $body) {
+ $body = self::format_part_body($body, $part, $this->headers->charset);
+ }
+
+ if ($max_bytes && strlen($body) > $max_bytes) {
+ $body = substr($body, 0, $max_bytes);
+ }
+
+ if (is_resource($mode)) {
+ if ($body !== false) {
+ fwrite($mode, $body);
+ rewind($mode);
+ }
+
+ return $body !== false;
+ }
+
+ if ($mode === -1) {
+ if ($body !== false) {
+ print($body);
+ }
+
+ return $body !== false;
+ }
+
+ return $body;
+ }
+
+ // get the body from IMAP
+ $this->storage->set_folder($this->folder);
+
+ $body = $this->storage->get_message_part($this->uid, $mime_id, $part,
+ $mode === -1, is_resource($mode) ? $mode : null,
+ !($mode && $formatted), $max_bytes, $mode && $formatted);
+
+ if (is_resource($mode)) {
+ rewind($mode);
+ return $body !== false;
+ }
+
+ if (!$mode && $body && $formatted) {
+ $body = self::format_part_body($body, $part, $this->headers->charset);
+ }
+
+ return $body;
+ }
+
+
+ /**
+ * Format text message part for display
+ *
+ * @param string $body Part body
+ * @param rcube_message_part $part Part object
+ * @param string $default_charset Fallback charset if part charset is not specified
+ *
+ * @return string Formatted body
+ */
+ public static function format_part_body($body, $part, $default_charset = null)
+ {
+ // remove useless characters
+ $body = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $body);
+
+ // remove NULL characters if any (#1486189)
+ if (strpos($body, "\x00") !== false) {
+ $body = str_replace("\x00", '', $body);
+ }
+
+ // detect charset...
+ if (!$part->charset || strtoupper($part->charset) == 'US-ASCII') {
+ // try to extract charset information from HTML meta tag (#1488125)
+ if ($part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
+ $part->charset = strtoupper($m[1]);
+ }
+ else if ($default_charset) {
+ $part->charset = $default_charset;
+ }
+ else {
+ $rcube = rcube::get_instance();
+ $part->charset = $rcube->config->get('default_charset', RCUBE_CHARSET);
+ }
+ }
+
+ // ..convert charset encoding
+ $body = rcube_charset::convert($body, $part->charset);
+
+ return $body;
+ }
+
+
/**
* Determine if the message contains a HTML part. This must to be
* a real part not an attachment (or its part)
*
* @param bool $enriched Enables checking for text/enriched parts too
*
* @return bool True if a HTML is available, False if not
*/
function has_html_part($enriched = false)
{
// check all message parts
foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) {
// Skip if part is an attachment, don't use is_attachment() here
if ($part->filename) {
continue;
}
$level = explode('.', $part->mime_id);
$depth = count($level);
// Check if the part belongs to higher-level's multipart part
// this can be alternative/related/signed/encrypted or mixed
while (array_pop($level) !== null) {
$parent_depth = count($level);
if (!$parent_depth) {
return true;
}
$parent = $this->mime_parts[join('.', $level)];
if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $parent->mimetype)
|| ($parent->mimetype == 'multipart/mixed' && $parent_depth < $depth - 1)) {
continue 2;
}
}
if ($part->size) {
return true;
}
}
}
return false;
}
/**
* Determine if the message contains a text/plain part. This must to be
* a real part not an attachment (or its part)
*
* @return bool True if a plain text part is available, False if not
*/
function has_text_part()
{
// check all message parts
foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/plain') {
// Skip if part is an attachment, don't use is_attachment() here
if ($part->filename) {
continue;
}
$level = explode('.', $part->mime_id);
// Check if the part belongs to higher-level's alternative/related
while (array_pop($level) !== null) {
if (!count($level)) {
return true;
}
$parent = $this->mime_parts[join('.', $level)];
if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
continue 2;
}
}
if ($part->size) {
return true;
}
}
}
return false;
}
/**
* Return the first HTML part of this message
*
* @return string HTML message part content
*/
function first_html_part()
{
// check all message parts
foreach ($this->mime_parts as $pid => $part) {
if ($part->mimetype == 'text/html') {
- return $this->get_part_content($pid);
+ return $this->get_part_body($pid, true);
}
}
}
/**
* Return the first text part of this message
*
* @param rcube_message_part $part Reference to the part if found
* @return string Plain text message/part content
*/
function first_text_part(&$part=null)
{
// no message structure, return complete body
if (empty($this->parts))
return $this->body;
// check all message parts
foreach ($this->mime_parts as $mime_id => $part) {
if ($part->mimetype == 'text/plain') {
- return $this->get_part_content($mime_id);
+ return $this->get_part_body($mime_id, true);
}
else if ($part->mimetype == 'text/html') {
- $out = $this->get_part_content($mime_id);
+ $out = $this->get_part_body($mime_id, true);
// create instance of html2text class
$txt = new rcube_html2text($out);
return $txt->get_text();
}
}
$part = null;
return null;
}
/**
* Checks if part of the message is an attachment (or part of it)
*
* @param rcube_message_part $part Message part
*
* @return bool True if the part is an attachment part
*/
public function is_attachment($part)
{
foreach ($this->attachments as $att_part) {
if ($att_part->mime_id == $part->mime_id) {
return true;
}
// check if the part is a subpart of another attachment part (message/rfc822)
if ($att_part->mimetype == 'message/rfc822') {
if (in_array($part, (array)$att_part->parts)) {
return true;
}
}
}
return false;
}
/**
* Read the message structure returend by the IMAP server
* and build flat lists of content parts and attachments
*
* @param rcube_message_part $structure Message structure node
* @param bool $recursive True when called recursively
*/
private function parse_structure($structure, $recursive = false)
{
// real content-type of message/rfc822 part
if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) {
$mimetype = $structure->real_mimetype;
// parse headers from message/rfc822 part
if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
- list($headers, ) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768));
+ list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768));
$structure->headers = rcube_mime::parse_headers($headers);
}
}
else
$mimetype = $structure->mimetype;
// show message headers
if ($recursive && is_array($structure->headers) &&
(isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])) {
$c = new stdClass;
$c->type = 'headers';
$c->headers = $structure->headers;
$this->parts[] = $c;
}
// Allow plugins to handle message parts
$plugin = $this->app->plugins->exec_hook('message_part_structure',
array('object' => $this, 'structure' => $structure,
'mimetype' => $mimetype, 'recursive' => $recursive));
if ($plugin['abort'])
return;
$structure = $plugin['structure'];
list($message_ctype_primary, $message_ctype_secondary) = explode('/', $plugin['mimetype']);
// print body if message doesn't have multiple parts
if ($message_ctype_primary == 'text' && !$recursive) {
// parts with unsupported type add to attachments list
if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) {
$this->attachments[] = $structure;
return;
}
$structure->type = 'content';
$this->parts[] = $structure;
// Parse simple (plain text) message body
if ($message_ctype_secondary == 'plain') {
foreach ((array)$this->uu_decode($structure) as $uupart) {
$this->mime_parts[$uupart->mime_id] = $uupart;
$this->attachments[] = $uupart;
}
}
}
// the same for pgp signed messages
else if ($mimetype == 'application/pgp' && !$recursive) {
$structure->type = 'content';
$this->parts[] = $structure;
}
// message contains (more than one!) alternative parts
else if ($mimetype == 'multipart/alternative'
&& is_array($structure->parts) && count($structure->parts) > 1
) {
$plain_part = null;
$html_part = null;
$print_part = null;
$related_part = null;
$attach_part = null;
// get html/plaintext parts, other add to attachments list
foreach ($structure->parts as $p => $sub_part) {
$sub_mimetype = $sub_part->mimetype;
$is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype);
// skip empty text parts
if (!$sub_part->size && !$is_multipart) {
continue;
}
// We've encountered (malformed) messages with more than
// one text/plain or text/html part here. There's no way to choose
// which one is better, so we'll display first of them and add
// others as attachments (#1489358)
// check if sub part is
if ($is_multipart)
$related_part = $p;
else if ($sub_mimetype == 'text/plain' && !$plain_part)
$plain_part = $p;
else if ($sub_mimetype == 'text/html' && !$html_part)
$html_part = $p;
else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
$enriched_part = $p;
else {
// add unsupported/unrecognized parts to attachments list
$this->attachments[] = $sub_part;
}
}
// parse related part (alternative part could be in here)
if ($related_part !== null && !$this->parse_alternative) {
$this->parse_alternative = true;
$this->parse_structure($structure->parts[$related_part], true);
$this->parse_alternative = false;
// if plain part was found, we should unset it if html is preferred
if ($this->opt['prefer_html'] && count($this->parts))
$plain_part = null;
}
// choose html/plain part to print
if ($html_part !== null && $this->opt['prefer_html']) {
$print_part = $structure->parts[$html_part];
}
else if ($enriched_part !== null) {
$print_part = $structure->parts[$enriched_part];
}
else if ($plain_part !== null) {
$print_part = $structure->parts[$plain_part];
}
// add the right message body
if (is_object($print_part)) {
$print_part->type = 'content';
$this->parts[] = $print_part;
}
// show plaintext warning
else if ($html_part !== null && empty($this->parts)) {
$c = new stdClass;
$c->type = 'content';
$c->ctype_primary = 'text';
$c->ctype_secondary = 'plain';
$c->mimetype = 'text/plain';
$c->realtype = 'text/html';
$this->parts[] = $c;
}
}
// this is an ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'multipart/encrypted') {
$p = new stdClass;
$p->type = 'content';
$p->ctype_primary = 'text';
$p->ctype_secondary = 'plain';
$p->mimetype = 'text/plain';
$p->realtype = 'multipart/encrypted';
$this->parts[] = $p;
}
// this is an S/MIME ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'application/pkcs7-mime') {
$p = new stdClass;
$p->type = 'content';
$p->ctype_primary = 'text';
$p->ctype_secondary = 'plain';
$p->mimetype = 'text/plain';
$p->realtype = 'application/pkcs7-mime';
$this->parts[] = $p;
}
// message contains multiple parts
else if (is_array($structure->parts) && !empty($structure->parts)) {
// iterate over parts
for ($i=0; $i < count($structure->parts); $i++) {
$mail_part = &$structure->parts[$i];
$primary_type = $mail_part->ctype_primary;
$secondary_type = $mail_part->ctype_secondary;
// real content-type of message/rfc822
if ($mail_part->real_mimetype) {
$part_orig_mimetype = $mail_part->mimetype;
$part_mimetype = $mail_part->real_mimetype;
list($primary_type, $secondary_type) = explode('/', $part_mimetype);
}
else {
$part_mimetype = $part_orig_mimetype = $mail_part->mimetype;
}
// multipart/alternative
if ($primary_type == 'multipart') {
$this->parse_structure($mail_part, true);
// list message/rfc822 as attachment as well (mostly .eml)
if ($part_orig_mimetype == 'message/rfc822' && !empty($mail_part->filename))
$this->attachments[] = $mail_part;
}
// part text/[plain|html] or delivery status
else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') ||
in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification'))
) {
// Allow plugins to handle also this part
$plugin = $this->app->plugins->exec_hook('message_part_structure',
array('object' => $this, 'structure' => $mail_part,
'mimetype' => $part_mimetype, 'recursive' => true));
if ($plugin['abort'])
continue;
if ($part_mimetype == 'text/html' && $mail_part->size) {
$got_html_part = true;
}
$mail_part = $plugin['structure'];
list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']);
// add text part if it matches the prefs
if (!$this->parse_alternative ||
($secondary_type == 'html' && $this->opt['prefer_html']) ||
($secondary_type == 'plain' && !$this->opt['prefer_html'])
) {
$mail_part->type = 'content';
$this->parts[] = $mail_part;
}
// list as attachment as well
if (!empty($mail_part->filename)) {
$this->attachments[] = $mail_part;
}
}
// part message/*
else if ($primary_type == 'message') {
$this->parse_structure($mail_part, true);
// list as attachment as well (mostly .eml)
if (!empty($mail_part->filename))
$this->attachments[] = $mail_part;
}
// ignore "virtual" protocol parts
else if ($primary_type == 'protocol') {
continue;
}
// part is Microsoft Outlook TNEF (winmail.dat)
else if ($part_mimetype == 'application/ms-tnef') {
foreach ((array)$this->tnef_decode($mail_part) as $tpart) {
$this->mime_parts[$tpart->mime_id] = $tpart;
$this->attachments[] = $tpart;
}
}
// part is a file/attachment
else if (preg_match('/^(inline|attach)/', $mail_part->disposition) ||
$mail_part->headers['content-id'] ||
($mail_part->filename &&
(empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition)))
) {
// skip apple resource forks
if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile')
continue;
// part belongs to a related message and is linked
if (preg_match('/^multipart\/(related|relative)/', $mimetype)
&& ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) {
if ($mail_part->headers['content-id'])
$mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']);
if ($mail_part->headers['content-location'])
$mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location'];
$this->inline_parts[] = $mail_part;
}
// attachment encapsulated within message/rfc822 part needs further decoding (#1486743)
else if ($part_orig_mimetype == 'message/rfc822') {
$this->parse_structure($mail_part, true);
// list as attachment as well (mostly .eml)
if (!empty($mail_part->filename))
$this->attachments[] = $mail_part;
}
// regular attachment with valid content type
// (content-type name regexp according to RFC4288.4.2)
else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) {
$this->attachments[] = $mail_part;
}
// attachment with invalid content type
// replace malformed content type with application/octet-stream (#1487767)
else if ($mail_part->filename) {
$mail_part->ctype_primary = 'application';
$mail_part->ctype_secondary = 'octet-stream';
$mail_part->mimetype = 'application/octet-stream';
$this->attachments[] = $mail_part;
}
}
// attachment part as message/rfc822 (#1488026)
else if ($mail_part->mimetype == 'message/rfc822') {
$this->parse_structure($mail_part);
}
}
// if this was a related part try to resolve references
if (preg_match('/^multipart\/(related|relative)/', $mimetype) && sizeof($this->inline_parts)) {
$a_replaces = array();
$img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/';
foreach ($this->inline_parts as $inline_object) {
$part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary);
if (isset($inline_object->content_id))
$a_replaces['cid:'.$inline_object->content_id] = $part_url;
if ($inline_object->content_location) {
$a_replaces[$inline_object->content_location] = $part_url;
}
if (!empty($inline_object->filename)) {
// MS Outlook sends sometimes non-related attachments as related
// In this case multipart/related message has only one text part
// We'll add all such attachments to the attachments list
if (!isset($got_html_part) && empty($inline_object->content_id)) {
$this->attachments[] = $inline_object;
}
// MS Outlook sometimes also adds non-image attachments as related
// We'll add all such attachments to the attachments list
// Warning: some browsers support pdf in <img/>
else if (!preg_match($img_regexp, $inline_object->mimetype)) {
$this->attachments[] = $inline_object;
}
// @TODO: we should fetch HTML body and find attachment's content-id
// to handle also image attachments without reference in the body
// @TODO: should we list all image attachments in text mode?
}
}
// add replace array to each content part
// (will be applied later when part body is available)
foreach ($this->parts as $i => $part) {
if ($part->type == 'content')
$this->parts[$i]->replaces = $a_replaces;
}
}
}
// message is a single part non-text
else if ($structure->filename) {
$this->attachments[] = $structure;
}
// message is a single part non-text (without filename)
else if (preg_match('/application\//i', $mimetype)) {
$this->attachments[] = $structure;
}
}
/**
* Fill aflat array with references to all parts, indexed by part numbers
*
* @param rcube_message_part $part Message body structure
*/
private function get_mime_numbers(&$part)
{
if (strlen($part->mime_id))
$this->mime_parts[$part->mime_id] = &$part;
if (is_array($part->parts))
for ($i=0; $i<count($part->parts); $i++)
$this->get_mime_numbers($part->parts[$i]);
}
/**
* Decode a Microsoft Outlook TNEF part (winmail.dat)
*
* @param rcube_message_part $part Message part to decode
* @return array
*/
function tnef_decode(&$part)
{
- // @TODO: attachment may be huge, hadle it via file
- if (!isset($part->body)) {
- $this->storage->set_folder($this->folder);
- $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part);
- }
+ // @TODO: attachment may be huge, handle body via file
+ $body = $this->get_part_body($part->mime_id);
+ $tnef = new rcube_tnef_decoder;
+ $tnef_arr = $tnef->decompress($body);
+ $parts = array();
- $parts = array();
- $tnef = new tnef_decoder;
- $tnef_arr = $tnef->decompress($part->body);
+ unset($body);
foreach ($tnef_arr as $pid => $winatt) {
$tpart = new rcube_message_part;
- $tpart->filename = trim($winatt['name']);
+ $tpart->filename = $this->fix_attachment_name(trim($winatt['name']), $part);
$tpart->encoding = 'stream';
$tpart->ctype_primary = trim(strtolower($winatt['type']));
$tpart->ctype_secondary = trim(strtolower($winatt['subtype']));
$tpart->mimetype = $tpart->ctype_primary . '/' . $tpart->ctype_secondary;
$tpart->mime_id = 'winmail.' . $part->mime_id . '.' . $pid;
$tpart->size = $winatt['size'];
$tpart->body = $winatt['stream'];
$parts[] = $tpart;
unset($tnef_arr[$pid]);
}
return $parts;
}
/**
* Parse message body for UUencoded attachments bodies
*
* @param rcube_message_part $part Message part to decode
* @return array
*/
function uu_decode(&$part)
{
- // @TODO: messages may be huge, hadle body via file
- if (!isset($part->body)) {
- $this->storage->set_folder($this->folder);
- $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part);
- }
+ // @TODO: messages may be huge, handle body via file
+ $part->body = $this->get_part_body($part->mime_id);
+ $parts = array();
+ $pid = 0;
- $parts = array();
// FIXME: line length is max.65?
- $uu_regexp = '/begin [0-7]{3,4} ([^\n]+)\n/s';
+ $uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s';
+ $uu_regexp_end = '/`\r?\nend((\r?\n)|($))/s';
+
+ while (preg_match($uu_regexp_begin, $part->body, $matches, PREG_OFFSET_CAPTURE)) {
+ $startpos = $matches[0][1];
+
+ if (!preg_match($uu_regexp_end, $part->body, $m, PREG_OFFSET_CAPTURE, $startpos)) {
+ break;
+ }
+
+ $endpos = $m[0][1];
+ $begin_len = strlen($matches[0][0]);
+ $end_len = strlen($m[0][0]);
+
+ // extract attachment body
+ $filebody = substr($part->body, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1);
+ $filebody = str_replace("\r\n", "\n", $filebody);
- if (preg_match_all($uu_regexp, $part->body, $matches, PREG_SET_ORDER)) {
- // update message content-type
- $part->ctype_primary = 'multipart';
- $part->ctype_secondary = 'mixed';
- $part->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
- $uu_endstring = "`\nend\n";
+ // remove attachment body from the message body
+ $part->body = substr_replace($part->body, '', $startpos, $endpos + $end_len - $startpos);
+ // mark body as modified so it will not be cached by rcube_imap_cache
+ $part->body_modified = true;
// add attachments to the structure
- foreach ($matches as $pid => $att) {
- $startpos = strpos($part->body, $att[1]) + strlen($att[1]) + 1; // "\n"
- $endpos = strpos($part->body, $uu_endstring);
- $filebody = substr($part->body, $startpos, $endpos-$startpos);
+ $uupart = new rcube_message_part;
+ $uupart->filename = trim($matches[1][0]);
+ $uupart->encoding = 'stream';
+ $uupart->body = convert_uudecode($filebody);
+ $uupart->size = strlen($uupart->body);
+ $uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid;
+
+ $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
+ $uupart->mimetype = $ctype;
+ list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
+
+ $parts[] = $uupart;
+ $pid++;
+ }
- // remove attachments bodies from the message body
- $part->body = substr_replace($part->body, "", $startpos, $endpos+strlen($uu_endstring)-$startpos);
+ return $parts;
+ }
- $uupart = new rcube_message_part;
+ /**
+ * Fix attachment name encoding if needed/possible
+ */
+ protected function fix_attachment_name($name, $part)
+ {
+ if ($name == rcube_charset::clean($name)) {
+ return $name;
+ }
- $uupart->filename = trim($att[1]);
- $uupart->encoding = 'stream';
- $uupart->body = convert_uudecode($filebody);
- $uupart->size = strlen($uupart->body);
- $uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid;
+ // find charset from part or its parent(s)
+ if ($part->charset) {
+ $charsets[] = $part->charset;
+ }
+ else {
+ // check first part (common case)
+ $n = strpos($part->mime_id, '.') ? preg_replace('/\.[0-9]+$/', '', $part->mime_id) . '.1' : 1;
+ if (($_part = $this->mime_parts[$n]) && $_part->charset) {
+ $charsets[] = $_part->charset;
+ }
- $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
- $uupart->mimetype = $ctype;
- list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
+ // check parents' charset
+ $items = explode('.', $part->mime_id);
+ for ($i = count($items)-1; $i > 0; $i--) {
+ $last = array_pop($items);
+ $parent = $this->mime_parts[join('.', $items)];
- $parts[] = $uupart;
- unset($matches[$pid]);
+ if ($parent && $parent->charset) {
+ $charsets[] = $parent->charset;
+ }
}
+ }
- // remove attachments bodies from the message body
- $part->body = preg_replace($uu_regexp, '', $part->body);
+ if ($this->headers->charset) {
+ $charsets[] = $this->headers->charset;
}
- return $parts;
- }
+ if (empty($charsets)) {
+ $rcube = rcube::get_instance();
+ $charsets[] = rcube_charset::detect($name, $rcube->config->get('default_charset', RCUBE_CHARSET));
+ }
+
+ foreach (array_unique($charsets) as $charset) {
+ $_name = rcube_charset::convert($name, $charset);
+ if ($_name == rcube_charset::clean($_name)) {
+ if (!$part->charset) {
+ $part->charset = $charset;
+ }
+
+ return $_name;
+ }
+ }
+
+ return $name;
+ }
/**
* Deprecated methods (to be removed)
*/
public static function unfold_flowed($text)
{
return rcube_mime::unfold_flowed($text);
}
public static function format_flowed($text, $length = 72)
{
return rcube_mime::format_flowed($text, $length);
}
}
diff --git a/lib/ext/Roundcube/rcube_message_header.php b/lib/ext/Roundcube/rcube_message_header.php
index 2c5e2b6..2b795e5 100644
--- a/lib/ext/Roundcube/rcube_message_header.php
+++ b/lib/ext/Roundcube/rcube_message_header.php
@@ -1,308 +1,325 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-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: |
| E-mail message headers representation |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Struct representing an e-mail message header
*
* @package Framework
* @subpackage Storage
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_message_header
{
/**
* Message sequence number
*
* @var int
*/
public $id;
/**
* Message unique identifier
*
* @var int
*/
public $uid;
/**
* Message subject
*
* @var string
*/
public $subject;
/**
* Message sender (From)
*
* @var string
*/
public $from;
/**
* Message recipient (To)
*
* @var string
*/
public $to;
/**
* Message additional recipients (Cc)
*
* @var string
*/
public $cc;
/**
* Message Reply-To header
*
* @var string
*/
public $replyto;
/**
* Message In-Reply-To header
*
* @var string
*/
public $in_reply_to;
/**
* Message date (Date)
*
* @var string
*/
public $date;
/**
* Message identifier (Message-ID)
*
* @var string
*/
public $messageID;
/**
* Message size
*
* @var int
*/
public $size;
/**
* Message encoding
*
* @var string
*/
public $encoding;
/**
* Message charset
*
* @var string
*/
public $charset;
/**
* Message Content-type
*
* @var string
*/
public $ctype;
/**
* Message timestamp (based on message date)
*
* @var int
*/
public $timestamp;
/**
* IMAP bodystructure string
*
* @var string
*/
public $bodystructure;
/**
* IMAP internal date
*
* @var string
*/
public $internaldate;
/**
* Message References header
*
* @var string
*/
public $references;
/**
* Message priority (X-Priority)
*
* @var int
*/
public $priority;
/**
* Message receipt recipient
*
* @var string
*/
public $mdn_to;
+ /**
+ * IMAP folder this message is stored in
+ *
+ * @var string
+ */
+ public $folder;
+
/**
* Other message headers
*
* @var array
*/
public $others = array();
/**
* Message flags
*
* @var array
*/
public $flags = array();
// map header to rcube_message_header object property
private $obj_headers = array(
'date' => 'date',
'from' => 'from',
'to' => 'to',
'subject' => 'subject',
'reply-to' => 'replyto',
'cc' => 'cc',
'bcc' => 'bcc',
+ 'mbox' => 'folder',
+ 'folder' => 'folder',
'content-transfer-encoding' => 'encoding',
'in-reply-to' => 'in_reply_to',
'content-type' => 'ctype',
'charset' => 'charset',
'references' => 'references',
'return-receipt-to' => 'mdn_to',
'disposition-notification-to' => 'mdn_to',
'x-confirm-reading-to' => 'mdn_to',
'message-id' => 'messageID',
'x-priority' => 'priority',
);
/**
* Returns header value
*/
public function get($name, $decode = true)
{
$name = strtolower($name);
if (isset($this->obj_headers[$name])) {
$value = $this->{$this->obj_headers[$name]};
}
else {
$value = $this->others[$name];
}
if ($decode) {
- $value = rcube_mime::decode_header($value, $this->charset);
- $value = rcube_charset::clean($value);
+ if (is_array($value)) {
+ foreach ($value as $key => $val) {
+ $value[$key] = rcube_mime::decode_header($val, $this->charset);
+ $value[$key] = rcube_charset::clean($val);
+ }
+ }
+ else {
+ $value = rcube_mime::decode_header($value, $this->charset);
+ $value = rcube_charset::clean($value);
+ }
}
return $value;
}
/**
* Sets header value
*/
public function set($name, $value)
{
$name = strtolower($name);
if (isset($this->obj_headers[$name])) {
$this->{$this->obj_headers[$name]} = $value;
}
else {
$this->others[$name] = $value;
}
}
/**
* Factory method to instantiate headers from a data array
*
* @param array Hash array with header values
* @return object rcube_message_header instance filled with headers values
*/
public static function from_array($arr)
{
$obj = new rcube_message_header;
foreach ($arr as $k => $v)
$obj->set($k, $v);
return $obj;
}
}
/**
* Class for sorting an array of rcube_message_header objects in a predetermined order.
*
* @package Framework
* @subpackage Storage
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_message_header_sorter
{
private $uids = array();
/**
* Set the predetermined sort order.
*
* @param array $index Numerically indexed array of IMAP UIDs
*/
function set_index($index)
{
$index = array_flip($index);
$this->uids = $index;
}
/**
* Sort the array of header objects
*
* @param array $headers Array of rcube_message_header objects indexed by UID
*/
function sort_headers(&$headers)
{
uksort($headers, array($this, "compare_uids"));
}
/**
* Sort method called by uksort()
*
* @param int $a Array key (UID)
* @param int $b Array key (UID)
*/
function compare_uids($a, $b)
{
// then find each sequence number in my ordered list
$posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
$posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
// return the relative position as the comparison value
return $posa - $posb;
}
}
diff --git a/lib/ext/Roundcube/rcube_mime.php b/lib/ext/Roundcube/rcube_mime.php
index 4d43a89..3f2fcc3 100644
--- a/lib/ext/Roundcube/rcube_mime.php
+++ b/lib/ext/Roundcube/rcube_mime.php
@@ -1,908 +1,913 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | Copyright (C) 2011-2014, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| MIME message parsing utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for parsing MIME messages
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_mime
{
private static $default_charset;
/**
* Object constructor.
*/
function __construct($default_charset = null)
{
self::$default_charset = $default_charset;
}
-
/**
* Returns message/object character set name
*
* @return string Characted set name
*/
public static function get_charset()
{
if (self::$default_charset) {
return self::$default_charset;
}
if ($charset = rcube::get_instance()->config->get('default_charset')) {
return $charset;
}
return RCUBE_CHARSET;
}
-
/**
* Parse the given raw message source and return a structure
* of rcube_message_part objects.
*
* It makes use of the PEAR:Mail_mimeDecode library
*
* @param string The message source
* @return object rcube_message_part The message structure
*/
public static function parse_message($raw_body)
{
$mime = new Mail_mimeDecode($raw_body);
$struct = $mime->decode(array('include_bodies' => true, 'decode_bodies' => true));
return self::structure_part($struct);
}
-
/**
* Recursive method to convert a Mail_mimeDecode part into a rcube_message_part object
*
* @param object A message part struct
* @param int Part count
* @param string Parent MIME ID
*
* @return object rcube_message_part
*/
private static function structure_part($part, $count=0, $parent='')
{
$struct = new rcube_message_part;
- $struct->mime_id = $part->mime_id ? $part->mime_id : (empty($parent) ? (string)$count : "$parent.$count");
- $struct->headers = $part->headers;
- $struct->ctype_primary = $part->ctype_primary;
- $struct->ctype_secondary = $part->ctype_secondary;
- $struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
+ $struct->mime_id = $part->mime_id ?: (empty($parent) ? (string)$count : "$parent.$count");
+ $struct->headers = $part->headers;
+ $struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
+ $struct->ctype_primary = $part->ctype_primary;
+ $struct->ctype_secondary = $part->ctype_secondary;
$struct->ctype_parameters = $part->ctype_parameters;
- if ($part->headers['content-transfer-encoding'])
+ if ($part->headers['content-transfer-encoding']) {
$struct->encoding = $part->headers['content-transfer-encoding'];
- if ($part->ctype_parameters['charset'])
+ }
+
+ if ($part->ctype_parameters['charset']) {
$struct->charset = $part->ctype_parameters['charset'];
+ }
- $part_charset = $struct->charset ? $struct->charset : self::get_charset();
+ $part_charset = $struct->charset ?: self::get_charset();
// determine filename
if (($filename = $part->d_parameters['filename']) || ($filename = $part->ctype_parameters['name'])) {
$struct->filename = rcube_mime::decode_mime_string($filename, $part_charset);
}
- // copy part body and convert it to UTF-8 if necessary
- $struct->body = $part->ctype_primary == 'text' || !$part->ctype_parameters['charset'] ? rcube_charset::convert($part->body, $part_charset) : $part->body;
- $struct->size = strlen($part->body);
+ $struct->body = $part->body;
+ $struct->size = strlen($part->body);
$struct->disposition = $part->disposition;
foreach ((array)$part->parts as $child_part) {
$struct->parts[] = self::structure_part($child_part, ++$count, $struct->mime_id);
}
return $struct;
}
-
/**
* Split an address list into a structured array list
*
* @param string $input Input string
* @param int $max List only this number of addresses
* @param boolean $decode Decode address strings
* @param string $fallback Fallback charset if none specified
* @param boolean $addronly Return flat array with e-mail addresses only
*
* @return array Indexed list of addresses
*/
static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false)
{
$a = self::parse_address_list($input, $decode, $fallback);
$out = array();
$j = 0;
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
$special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
if (!is_array($a))
return $out;
foreach ($a as $val) {
$j++;
$address = trim($val['address']);
if ($addronly) {
$out[$j] = $address;
}
else {
$name = trim($val['name']);
if ($name && $address && $name != $address)
$string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
else if ($address)
$string = $address;
else if ($name)
$string = $name;
$out[$j] = array('name' => $name, 'mailto' => $address, 'string' => $string);
}
if ($max && $j==$max)
break;
}
return $out;
}
-
/**
* Decode a message header value
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_header($input, $fallback = null)
{
$str = self::decode_mime_string((string)$input, $fallback);
return $str;
}
-
/**
* Decode a mime-encoded string to internal charset
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_mime_string($input, $fallback = null)
{
$default_charset = !empty($fallback) ? $fallback : self::get_charset();
// rfc: all line breaks or other characters not found
// in the Base64 Alphabet must be ignored by decoding software
// delete all blanks between MIME-lines, differently we can
// receive unnecessary blanks and broken utf-8 symbols
$input = preg_replace("/\?=\s+=\?/", '?==?', $input);
// encoded-word regexp
$re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
// Find all RFC2047's encoded words
if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
// Initialize variables
$tmp = array();
$out = '';
$start = 0;
foreach ($matches as $idx => $m) {
$pos = $m[0][1];
$charset = $m[1][0];
$encoding = $m[2][0];
$text = $m[3][0];
$length = strlen($m[0][0]);
// Append everything that is before the text to be decoded
if ($start != $pos) {
$substr = substr($input, $start, $pos-$start);
$out .= rcube_charset::convert($substr, $default_charset);
$start = $pos;
}
$start += $length;
// Per RFC2047, each string part "MUST represent an integral number
// of characters . A multi-octet character may not be split across
// adjacent encoded-words." However, some mailers break this, so we
// try to handle characters spanned across parts anyway by iterating
// through and aggregating sequential encoded parts with the same
// character set and encoding, then perform the decoding on the
// aggregation as a whole.
$tmp[] = $text;
if ($next_match = $matches[$idx+1]) {
if ($next_match[0][1] == $start
&& $next_match[1][0] == $charset
&& $next_match[2][0] == $encoding
) {
continue;
}
}
$count = count($tmp);
$text = '';
// Decode and join encoded-word's chunks
if ($encoding == 'B' || $encoding == 'b') {
// base64 must be decoded a segment at a time
for ($i=0; $i<$count; $i++)
$text .= base64_decode($tmp[$i]);
}
else { //if ($encoding == 'Q' || $encoding == 'q') {
// quoted printable can be combined and processed at once
for ($i=0; $i<$count; $i++)
$text .= $tmp[$i];
$text = str_replace('_', ' ', $text);
$text = quoted_printable_decode($text);
}
$out .= rcube_charset::convert($text, $charset);
$tmp = array();
}
// add the last part of the input string
if ($start != strlen($input)) {
$out .= rcube_charset::convert(substr($input, $start), $default_charset);
}
// return the results
return $out;
}
// no encoding information, use fallback
return rcube_charset::convert($input, $default_charset);
}
-
/**
* Decode a mime part
*
* @param string $input Input string
* @param string $encoding Part encoding
+ *
* @return string Decoded string
*/
public static function decode($input, $encoding = '7bit')
{
switch (strtolower($encoding)) {
case 'quoted-printable':
return quoted_printable_decode($input);
case 'base64':
return base64_decode($input);
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
return convert_uudecode($input);
case '7bit':
default:
return $input;
}
}
-
/**
* Split RFC822 header string into an associative array
- * @access private
*/
public static function parse_headers($headers)
{
$a_headers = array();
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
$lines = explode("\n", $headers);
$c = count($lines);
for ($i=0; $i<$c; $i++) {
if ($p = strpos($lines[$i], ': ')) {
$field = strtolower(substr($lines[$i], 0, $p));
$value = trim(substr($lines[$i], $p+1));
if (!empty($value))
$a_headers[$field] = $value;
}
}
return $a_headers;
}
-
/**
* @access private
*/
private static function parse_address_list($str, $decode = true, $fallback = null)
{
// remove any newlines and carriage returns before
$str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
// extract list items, remove comments
$str = self::explode_header_string(',;', $str, true);
$result = array();
// simplified regexp, supporting quoted local part
$email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
foreach ($str as $key => $val) {
$name = '';
$address = '';
$val = trim($val);
if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
$address = $m[2];
$name = trim($m[1]);
}
else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
$address = $m[1];
$name = '';
}
// special case (#1489092)
else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
$address = 'MAILER-DAEMON';
$name = substr($val, 0, -strlen($m[1]));
}
else if (preg_match('/('.$email_rx.')/', $val, $m)) {
$name = $m[1];
}
else {
$name = $val;
}
// dequote and/or decode name
if ($name) {
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
$name = stripslashes($name);
}
if ($decode) {
$name = self::decode_header($name, $fallback);
// some clients encode addressee name with quotes around it
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
}
}
}
if (!$address && $name) {
$address = $name;
$name = '';
}
if ($address) {
$address = self::fix_email($address);
$result[$key] = array('name' => $name, 'address' => $address);
}
}
return $result;
}
-
/**
* Explodes header (e.g. address-list) string into array of strings
* using specified separator characters with proper handling
* of quoted-strings and comments (RFC2822)
*
* @param string $separator String containing separator characters
* @param string $str Header string
* @param bool $remove_comments Enable to remove comments
*
* @return array Header items
*/
public static function explode_header_string($separator, $str, $remove_comments = false)
{
$length = strlen($str);
$result = array();
$quoted = false;
$comment = 0;
$out = '';
for ($i=0; $i<$length; $i++) {
// we're inside a quoted string
if ($quoted) {
if ($str[$i] == '"') {
$quoted = false;
}
else if ($str[$i] == "\\") {
if ($comment <= 0) {
$out .= "\\";
}
$i++;
}
}
// we are inside a comment string
else if ($comment > 0) {
if ($str[$i] == ')') {
$comment--;
}
else if ($str[$i] == '(') {
$comment++;
}
else if ($str[$i] == "\\") {
$i++;
}
continue;
}
// separator, add to result array
else if (strpos($separator, $str[$i]) !== false) {
if ($out) {
$result[] = $out;
}
$out = '';
continue;
}
// start of quoted string
else if ($str[$i] == '"') {
$quoted = true;
}
// start of comment
else if ($remove_comments && $str[$i] == '(') {
$comment++;
}
if ($comment <= 0) {
$out .= $str[$i];
}
}
if ($out && $comment <= 0) {
$result[] = $out;
}
return $result;
}
-
/**
* Interpret a format=flowed message body according to RFC 2646
*
- * @param string $text Raw body formatted as flowed text
+ * @param string $text Raw body formatted as flowed text
+ * @param string $mark Mark each flowed line with specified character
*
* @return string Interpreted text with unwrapped lines and stuffed space removed
*/
- public static function unfold_flowed($text)
+ public static function unfold_flowed($text, $mark = null)
{
- $text = preg_split('/\r?\n/', $text);
- $last = -1;
+ $text = preg_split('/\r?\n/', $text);
+ $last = -1;
$q_level = 0;
+ $marks = array();
foreach ($text as $idx => $line) {
if (preg_match('/^(>+)/', $line, $m)) {
// remove quote chars
$q = strlen($m[1]);
$line = preg_replace('/^>+/', '', $line);
// remove (optional) space-staffing
$line = preg_replace('/^ /', '', $line);
// The same paragraph (We join current line with the previous one) when:
// - the same level of quoting
// - previous line was flowed
// - previous line contains more than only one single space (and quote char(s))
if ($q == $q_level
&& isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' '
&& !preg_match('/^>+ {0,1}$/', $text[$last])
) {
$text[$last] .= $line;
unset($text[$idx]);
+
+ if ($mark) {
+ $marks[$last] = true;
+ }
}
else {
$last = $idx;
}
}
else {
$q = 0;
if ($line == '-- ') {
$last = $idx;
}
else {
// remove space-stuffing
- $line = preg_replace('/^\s/', '', $line);
+ $line = preg_replace('/^ /', '', $line);
if (isset($text[$last]) && $line
&& $text[$last] != '-- '
&& $text[$last][strlen($text[$last])-1] == ' '
) {
$text[$last] .= $line;
unset($text[$idx]);
+
+ if ($mark) {
+ $marks[$last] = true;
+ }
}
else {
$text[$idx] = $line;
$last = $idx;
}
}
}
$q_level = $q;
}
+ if (!empty($marks)) {
+ foreach (array_keys($marks) as $mk) {
+ $text[$mk] = $mark . $text[$mk];
+ }
+ }
+
return implode("\r\n", $text);
}
-
/**
* Wrap the given text to comply with RFC 2646
*
- * @param string $text Text to wrap
- * @param int $length Length
+ * @param string $text Text to wrap
+ * @param int $length Length
* @param string $charset Character encoding of $text
*
* @return string Wrapped text
*/
public static function format_flowed($text, $length = 72, $charset=null)
{
$text = preg_split('/\r?\n/', $text);
foreach ($text as $idx => $line) {
if ($line != '-- ') {
if (preg_match('/^(>+)/', $line, $m)) {
// remove quote chars
$level = strlen($m[1]);
$line = preg_replace('/^>+/', '', $line);
// remove (optional) space-staffing and spaces before the line end
$line = preg_replace('/(^ | +$)/', '', $line);
$prefix = str_repeat('>', $level) . ' ';
$line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
}
else if ($line) {
$line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
// space-stuffing
$line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
}
$text[$idx] = $line;
}
}
return implode("\r\n", $text);
}
-
/**
* Improved wordwrap function with multibyte support.
* The code is based on Zend_Text_MultiByte::wordWrap().
*
* @param string $string Text to wrap
* @param int $width Line width
* @param string $break Line separator
* @param bool $cut Enable to cut word
* @param string $charset Charset of $string
* @param bool $wrap_quoted When enabled quoted lines will not be wrapped
*
* @return string Text
*/
public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true)
{
// Note: Never try to use iconv instead of mbstring functions here
// Iconv's substr/strlen are 100x slower (#1489113)
if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) {
mb_internal_encoding($charset);
}
// Convert \r\n to \n, this is our line-separator
$string = str_replace("\r\n", "\n", $string);
$separator = "\n"; // must be 1 character length
$result = array();
while (($stringLength = mb_strlen($string)) > 0) {
$breakPos = mb_strpos($string, $separator, 0);
// quoted line (do not wrap)
if ($wrap_quoted && $string[0] == '>') {
if ($breakPos === $stringLength - 1 || $breakPos === false) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
// next line found and current line is shorter than the limit
else if ($breakPos !== false && $breakPos < $width) {
if ($breakPos === $stringLength - 1) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
$subString = mb_substr($string, 0, $width);
// last line
if ($breakPos === false && $subString === $string) {
$cutLength = null;
}
else {
$nextChar = mb_substr($string, $width, 1);
if ($nextChar === ' ' || $nextChar === $separator) {
$afterNextChar = mb_substr($string, $width + 1, 1);
// Note: mb_substr() does never return False
if ($afterNextChar === false || $afterNextChar === '') {
$subString .= $nextChar;
}
$cutLength = mb_strlen($subString) + 1;
}
else {
$spacePos = mb_strrpos($subString, ' ', 0);
if ($spacePos !== false) {
$subString = mb_substr($subString, 0, $spacePos);
$cutLength = $spacePos + 1;
}
else if ($cut === false) {
$spacePos = mb_strpos($string, ' ', 0);
if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
$subString = mb_substr($string, 0, $spacePos);
$cutLength = $spacePos + 1;
}
else if ($breakPos === false) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
$cutLength = $width;
}
}
}
}
$result[] = $subString;
if ($cutLength !== null) {
$string = mb_substr($string, $cutLength, ($stringLength - $cutLength));
}
else {
break;
}
}
if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) {
mb_internal_encoding(RCUBE_CHARSET);
}
return implode($break, $result);
}
-
/**
* A method to guess the mime_type of an attachment.
*
* @param string $path Path to the file or file contents
* @param string $name File name (with suffix)
* @param string $failover Mime type supplied for failover
* @param boolean $is_stream Set to True if $path contains file contents
* @param boolean $skip_suffix Set to True if the config/mimetypes.php mappig should be ignored
*
* @return string
* @author Till Klampaeckel <till@php.net>
* @see http://de2.php.net/manual/en/ref.fileinfo.php
* @see http://de2.php.net/mime_content_type
*/
public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
{
static $mime_ext = array();
- $mime_type = null;
- $config = rcube::get_instance()->config;
+ $mime_type = null;
+ $config = rcube::get_instance()->config;
$mime_magic = $config->get('mime_magic');
if (!$skip_suffix && empty($mime_ext)) {
foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
$mime_ext = array_merge($mime_ext, (array) @include($fpath));
}
}
// use file name suffix with hard-coded mime-type map
if (!$skip_suffix && is_array($mime_ext) && $name) {
if ($suffix = substr($name, strrpos($name, '.')+1)) {
$mime_type = $mime_ext[strtolower($suffix)];
}
}
// try fileinfo extension if available
if (!$mime_type && function_exists('finfo_open')) {
// null as a 2nd argument should be the same as no argument
// this however is not true on all systems/versions
if ($mime_magic) {
$finfo = finfo_open(FILEINFO_MIME, $mime_magic);
}
else {
$finfo = finfo_open(FILEINFO_MIME);
}
if ($finfo) {
if ($is_stream)
$mime_type = finfo_buffer($finfo, $path);
else
$mime_type = finfo_file($finfo, $path);
finfo_close($finfo);
}
}
// try PHP's mime_content_type
if (!$mime_type && !$is_stream && function_exists('mime_content_type')) {
$mime_type = @mime_content_type($path);
}
// fall back to user-submitted string
if (!$mime_type) {
$mime_type = $failover;
}
else {
// Sometimes (PHP-5.3?) content-type contains charset definition,
// Remove it (#1487122) also "charset=binary" is useless
$mime_type = array_shift(preg_split('/[; ]/', $mime_type));
}
return $mime_type;
}
-
/**
* Get mimetype => file extension mapping
*
- * @param string Mime-Type to get extensions for
- * @return array List of extensions matching the given mimetype or a hash array with ext -> mimetype mappings if $mimetype is not given
+ * @param string Mime-Type to get extensions for
+ *
+ * @return array List of extensions matching the given mimetype or a hash array
+ * with ext -> mimetype mappings if $mimetype is not given
*/
public static function get_mime_extensions($mimetype = null)
{
static $mime_types, $mime_extensions;
// return cached data
if (is_array($mime_types)) {
return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
}
// load mapping file
$file_paths = array();
if ($mime_types = rcube::get_instance()->config->get('mime_types')) {
$file_paths[] = $mime_types;
}
// try common locations
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
$file_paths[] = 'C:/xampp/apache/conf/mime.types.';
}
else {
$file_paths[] = '/etc/mime.types';
$file_paths[] = '/etc/httpd/mime.types';
$file_paths[] = '/etc/httpd2/mime.types';
$file_paths[] = '/etc/apache/mime.types';
$file_paths[] = '/etc/apache2/mime.types';
+ $file_paths[] = '/etc/nginx/mime.types';
$file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
$file_paths[] = '/usr/local/etc/apache/conf/mime.types';
}
foreach ($file_paths as $fp) {
if (@is_readable($fp)) {
$lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
break;
}
}
$mime_types = $mime_extensions = array();
$regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
foreach((array)$lines as $line) {
// skip comments or mime types w/o any extensions
if ($line[0] == '#' || !preg_match($regex, $line, $matches))
continue;
$mime = $matches[1];
foreach (explode(' ', $matches[2]) as $ext) {
$ext = trim($ext);
$mime_types[$mime][] = $ext;
$mime_extensions[$ext] = $mime;
}
}
// fallback to some well-known types most important for daily emails
if (empty($mime_types)) {
foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
$mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
}
foreach ($mime_extensions as $ext => $mime) {
$mime_types[$mime][] = $ext;
}
}
// Add some known aliases that aren't included by some mime.types (#1488891)
// the order is important here so standard extensions have higher prio
$aliases = array(
'image/gif' => array('gif'),
'image/png' => array('png'),
'image/x-png' => array('png'),
'image/jpeg' => array('jpg', 'jpeg', 'jpe'),
'image/jpg' => array('jpg', 'jpeg', 'jpe'),
'image/pjpeg' => array('jpg', 'jpeg', 'jpe'),
'image/tiff' => array('tif'),
'message/rfc822' => array('eml'),
'text/x-mail' => array('eml'),
);
foreach ($aliases as $mime => $exts) {
$mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
foreach ($exts as $ext) {
if (!isset($mime_extensions[$ext])) {
$mime_extensions[$ext] = $mime;
}
}
}
return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
}
-
/**
* Detect image type of the given binary data by checking magic numbers.
*
* @param string $data Binary file content
*
* @return string Detected mime-type or jpeg as fallback
*/
public static function image_content_type($data)
{
$type = 'jpeg';
if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
// else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
return 'image/' . $type;
}
/**
* Try to fix invalid email addresses
*/
public static function fix_email($email)
{
$parts = rcube_utils::explode_quoted_string('@', $email);
foreach ($parts as $idx => $part) {
// remove redundant quoting (#1490040)
if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
$parts[$idx] = $m[1];
}
}
return implode('@', $parts);
}
}
diff --git a/lib/ext/Roundcube/rcube_output.php b/lib/ext/Roundcube/rcube_output.php
index 7ccf9a0..55a38b2 100644
--- a/lib/ext/Roundcube/rcube_output.php
+++ b/lib/ext/Roundcube/rcube_output.php
@@ -1,269 +1,275 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2012 The Roundcube Dev Team |
+ | Copyright (C) 2005-2014 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: |
| Abstract class for output generation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Framework
* @subpackage View
*/
abstract class rcube_output
{
public $browser;
protected $app;
protected $config;
protected $charset = RCUBE_CHARSET;
protected $env = array();
+ protected $skins = array();
/**
* Object constructor
*/
public function __construct()
{
$this->app = rcube::get_instance();
$this->config = $this->app->config;
$this->browser = new rcube_browser();
}
-
/**
* Magic getter
*/
public function __get($var)
{
- // allow read-only access to $env
- if ($var == 'env')
- return $this->env;
+ // allow read-only access to some members
+ switch ($var) {
+ case 'env': return $this->env;
+ case 'skins': return $this->skins;
+ case 'charset': return $this->charset;
+ }
return null;
}
-
/**
* Setter for output charset.
* To be specified in a meta tag and sent as http-header
*
* @param string $charset Charset name
*/
public function set_charset($charset)
{
$this->charset = $charset;
}
-
/**
* Getter for output charset
*
* @return string Output charset name
*/
public function get_charset()
{
return $this->charset;
}
-
/**
* Set environment variable
*
* @param string $name Property name
* @param mixed $value Property value
*/
public function set_env($name, $value)
{
$this->env[$name] = $value;
}
-
/**
* Environment variable getter.
*
* @param string $name Property name
*
* @return mixed Property value
*/
public function get_env($name)
{
return $this->env[$name];
}
-
/**
* Delete all stored env variables and commands
*/
public function reset()
{
$this->env = array();
}
-
/**
* 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 displaying time in seconds
*/
abstract function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0);
-
/**
* 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
*/
abstract function redirect($p = array(), $delay = 1);
-
/**
* Send output to the client.
*/
abstract function send();
-
/**
* Send HTTP headers to prevent caching a page
*/
public function nocacheing_headers()
{
if (headers_sent()) {
return;
}
header("Expires: ".gmdate("D, d M Y H:i:s")." GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
- // Request browser to disable DNS prefetching (CVE-2010-0464)
- header("X-DNS-Prefetch-Control: off");
-
// We need to set the following headers to make downloads work using IE in HTTPS mode.
if ($this->browser->ie && rcube_utils::https_check()) {
header('Pragma: private');
header("Cache-Control: private, must-revalidate");
}
else {
header("Cache-Control: private, no-cache, no-store, must-revalidate, post-check=0, pre-check=0");
header("Pragma: no-cache");
}
}
/**
* Send header with expire date 30 days in future
*
* @param int Expiration time in seconds
*/
public function future_expire_header($offset = 2600000)
{
- if (headers_sent())
+ if (headers_sent()) {
return;
+ }
header("Expires: " . gmdate("D, d M Y H:i:s", time()+$offset) . " GMT");
header("Cache-Control: max-age=$offset");
header("Pragma: ");
}
+ /**
+ * Send browser compatibility/security/etc. headers
+ */
+ public function common_headers()
+ {
+ if (headers_sent()) {
+ return;
+ }
+
+ // Unlock IE compatibility mode
+ if ($this->browser->ie) {
+ header('X-UA-Compatible: IE=edge');
+ }
+
+ // Request browser to disable DNS prefetching (CVE-2010-0464)
+ header("X-DNS-Prefetch-Control: off");
+ }
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
// STUB: to be overloaded by specific output classes
fputs(STDERR, "Error $code: $message\n");
exit(-1);
}
-
/**
* 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 = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
$value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
}
$out = $input->show($value);
return $out;
}
-
/**
* Convert a variable into a javascript object notation
*
* @param mixed Input value
*
* @return string Serialized JSON string
*/
public static function json_serialize($input)
{
$input = rcube_charset::clean($input);
// sometimes even using rcube_charset::clean() the input contains invalid UTF-8 sequences
// that's why we have @ here
return @json_encode($input);
}
-
}
diff --git a/lib/ext/Roundcube/rcube_plugin.php b/lib/ext/Roundcube/rcube_plugin.php
index f0af953..e240448 100644
--- a/lib/ext/Roundcube/rcube_plugin.php
+++ b/lib/ext/Roundcube/rcube_plugin.php
@@ -1,421 +1,451 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | Copyright (C) 2008-2014, 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 Framework
* @subpackage 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;
/**
* A list of config option names that can be modified
* by the user via user interface (with save-prefs command)
*
* @var array
*/
public $allowed_prefs;
protected $home;
protected $urlbase;
private $mytask;
+ private $loaded_config = array();
/**
* 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();
/**
* Provide information about this
*
- * @return array Meta information about a plugin or false if not implemented
+ * @return array Meta information about a plugin or false if not implemented:
+ * As hash array with the following keys:
+ * name: The plugin name
+ * vendor: Name of the plugin developer
+ * version: Plugin version name
+ * license: License name (short form according to http://spdx.org/licenses/)
+ * uri: The URL to the plugin homepage or source repository
+ * src_uri: Direct download URL to the source code of this plugin
+ * require: List of plugins required for this one (as array of plugin names)
*/
public static function info()
{
return false;
}
/**
* 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, true);
}
+ /**
+ * Attempt to load the given plugin which is optional for the current plugin
+ *
+ * @param string Plugin name
+ * @return boolean True on success, false on failure
+ */
+ public function include_plugin($plugin_name)
+ {
+ return $this->api->load_plugin($plugin_name, true, false);
+ }
+
/**
* 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')
{
+ if (in_array($fname, $this->loaded_config)) {
+ return true;
+ }
+
+ $this->loaded_config[] = $fname;
+
$fpath = $this->home.'/'.$fname;
$rcube = rcube::get_instance();
if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) {
rcube::raise_error(array(
'code' => 527, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load config from $fpath"), true, false);
return false;
}
else if (!$is_local) {
// Search plugin_name.inc.php file in any configured path
return $rcube->config->load_from_file($this->ID . '.inc.php');
}
return true;
}
/**
* 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;
}
$rcube = rcube::get_instance();
$rcube->load_language($lang, $add);
// add labels to client
if ($add2client && method_exists($rcube->output, 'add_label')) {
if (is_array($add2client)) {
$js_labels = array_map(array($this, 'label_map_callback'), $add2client);
}
else {
$js_labels = array_keys($add);
}
$rcube->output->add_label($js_labels);
}
}
}
/**
* Wrapper for add_label() adding the plugin ID as domain
*/
public function add_label()
{
$rcube = rcube::get_instance();
if (method_exists($rcube->output, 'add_label')) {
$args = func_get_args();
if (count($args) == 1 && is_array($args[0])) {
$args = $args[0];
}
$args = array_map(array($this, 'label_map_callback'), $args);
$rcube->output->add_label($args);
}
}
/**
* Wrapper for rcube::gettext() adding the plugin ID as domain
*
* @param string $p Message identifier
*
* @return string Localized text
* @see rcube::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()
{
$rcube = rcube::get_instance();
- foreach (array($rcube->config->get('skin'), 'larry') as $skin) {
+ $skins = array_keys((array)$rcube->output->skins);
+ if (empty($skins)) {
+ $skins = (array) $rcube->config->get('skin');
+ }
+ foreach ($skins 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)
{
if (strpos($key, $this->ID.'.') === 0) {
return $key;
}
return $this->ID.'.'.$key;
}
}
diff --git a/lib/ext/Roundcube/rcube_plugin_api.php b/lib/ext/Roundcube/rcube_plugin_api.php
index 617e921..92a5636 100644
--- a/lib/ext/Roundcube/rcube_plugin_api.php
+++ b/lib/ext/Roundcube/rcube_plugin_api.php
@@ -1,628 +1,663 @@
<?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: |
| Plugins repository |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// location where plugins are loade from
if (!defined('RCUBE_PLUGINS_DIR')) {
define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/');
}
/**
* The plugin loader and global API
*
* @package Framework
* @subpackage PluginAPI
*/
class rcube_plugin_api
{
static protected $instance;
public $dir;
public $url = 'plugins/';
public $task = '';
public $output;
public $handlers = array();
public $allowed_prefs = array();
public $allowed_session_prefs = array();
+ public $active_plugins = array();
protected $plugins = array();
protected $tasks = array();
protected $actions = array();
protected $actionmap = array();
protected $objectsmap = array();
protected $template_contents = array();
- protected $active_hook = false;
+ protected $exec_stack = array();
// Deprecated names of hooks, will be removed after 0.5-stable release
protected $deprecated_hooks = array(
'create_user' => 'user_create',
'kill_session' => 'session_destroy',
'upload_attachment' => 'attachment_upload',
'save_attachment' => 'attachment_save',
'get_attachment' => 'attachment_get',
'cleanup_attachments' => 'attachments_cleanup',
'display_attachment' => 'attachment_display',
'remove_attachment' => 'attachment_delete',
'outgoing_message_headers' => 'message_outgoing_headers',
'outgoing_message_body' => 'message_outgoing_body',
'address_sources' => 'addressbooks_list',
'get_address_book' => 'addressbook_get',
'create_contact' => 'contact_create',
'save_contact' => 'contact_update',
'contact_save' => 'contact_update',
'delete_contact' => 'contact_delete',
'manage_folders' => 'folders_list',
'list_mailboxes' => 'mailboxes_list',
'save_preferences' => 'preferences_save',
'user_preferences' => 'preferences_list',
'list_prefs_sections' => 'preferences_sections_list',
'list_identities' => 'identities_list',
'create_identity' => 'identity_create',
'delete_identity' => 'identity_delete',
'save_identity' => 'identity_update',
'identity_save' => 'identity_update',
// to be removed after 0.8
'imap_init' => 'storage_init',
'mailboxes_list' => 'storage_folders',
'imap_connect' => 'storage_connect',
);
/**
* This implements the 'singleton' design pattern
*
* @return rcube_plugin_api The one and only instance if this class
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new rcube_plugin_api();
}
return self::$instance;
}
/**
* Private constructor
*/
protected function __construct()
{
$this->dir = slashify(RCUBE_PLUGINS_DIR);
}
/**
* Initialize plugin engine
*
* This has to be done after rcmail::load_gui() or rcmail::json_init()
* was called because plugins need to have access to rcmail->output
*
* @param object rcube Instance of the rcube base class
* @param string Current application task (used for conditional plugin loading)
*/
public function init($app, $task = '')
{
$this->task = $task;
$this->output = $app->output;
// register an internal hook
$this->register_hook('template_container', array($this, 'template_container_hook'));
// maybe also register a shudown function which triggers
// shutdown functions of all plugin objects
}
/**
* Load and init all enabled plugins
*
* This has to be done after rcmail::load_gui() or rcmail::json_init()
* was called because plugins need to have access to rcmail->output
*
* @param array List of configured plugins to load
* @param array List of plugins required by the application
*/
public function load_plugins($plugins_enabled, $required_plugins = array())
{
foreach ($plugins_enabled as $plugin_name) {
$this->load_plugin($plugin_name);
}
// check existance of all required core plugins
foreach ($required_plugins as $plugin_name) {
$loaded = false;
foreach ($this->plugins as $plugin) {
if ($plugin instanceof $plugin_name) {
$loaded = true;
break;
}
}
// load required core plugin if no derivate was found
if (!$loaded) {
$loaded = $this->load_plugin($plugin_name);
}
// trigger fatal error if still not loaded
if (!$loaded) {
rcube::raise_error(array(
'code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Requried plugin $plugin_name was not loaded"), true, true);
}
}
}
/**
* Load the specified plugin
*
* @param string Plugin name
* @param boolean Force loading of the plugin even if it doesn't match the filter
+ * @param boolean Require loading of the plugin, error if it doesn't exist
*
* @return boolean True on success, false if not loaded or failure
*/
- public function load_plugin($plugin_name, $force = false)
+ public function load_plugin($plugin_name, $force = false, $require = true)
{
static $plugins_dir;
if (!$plugins_dir) {
$dir = dir($this->dir);
$plugins_dir = unslashify($dir->path);
}
// plugin already loaded
if ($this->plugins[$plugin_name]) {
return true;
}
- $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name
- . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+ $fn = "$plugins_dir/$plugin_name/$plugin_name.php";
- if (file_exists($fn)) {
+ if (is_readable($fn)) {
if (!class_exists($plugin_name, false)) {
include $fn;
}
// instantiate class if exists
if (class_exists($plugin_name, false)) {
$plugin = new $plugin_name($this);
+ $this->active_plugins[] = $plugin_name;
+
// check inheritance...
if (is_subclass_of($plugin, 'rcube_plugin')) {
// ... task, request type and framed mode
if (($force || !$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
&& (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html'))
&& (!$plugin->noframe || empty($_REQUEST['_framed']))
) {
$plugin->init();
$this->plugins[$plugin_name] = $plugin;
}
if (!empty($plugin->allowed_prefs)) {
$this->allowed_prefs = array_merge($this->allowed_prefs, $plugin->allowed_prefs);
}
return true;
}
}
else {
rcube::raise_error(array('code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No plugin class $plugin_name found in $fn"),
true, false);
}
}
- else {
+ elseif ($require) {
rcube::raise_error(array('code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load plugin file $fn"), true, false);
}
return false;
}
/**
* Get information about a specific plugin.
- * This is either provided by a plugin's info() method or extracted from a package.xml or a composer.json file
+ * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
*
* @param string Plugin name
* @return array Meta information about a plugin or False if plugin was not found
*/
public function get_info($plugin_name)
{
static $composer_lock, $license_uris = array(
'Apache' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
'Apache-2' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
'Apache-1' => 'http://www.apache.org/licenses/LICENSE-1.0',
'Apache-1.1' => 'http://www.apache.org/licenses/LICENSE-1.1',
'GPL' => 'http://www.gnu.org/licenses/gpl.html',
'GPLv2' => 'http://www.gnu.org/licenses/gpl-2.0.html',
'GPL-2.0' => 'http://www.gnu.org/licenses/gpl-2.0.html',
'GPLv3' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html',
'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html',
+ 'AGPLv3' => 'http://www.gnu.org/licenses/agpl.html',
+ 'AGPLv3+' => 'http://www.gnu.org/licenses/agpl.html',
+ 'AGPL-3.0' => 'http://www.gnu.org/licenses/agpl.html',
'LGPL' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPLv2' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
'LGPLv2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
'LGPLv3' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPL-2.0' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
'LGPL-2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
'LGPL-3.0' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPL-3.0+' => 'http://www.gnu.org/licenses/lgpl.html',
'BSD' => 'http://opensource.org/licenses/bsd-license.html',
'BSD-2-Clause' => 'http://opensource.org/licenses/BSD-2-Clause',
'BSD-3-Clause' => 'http://opensource.org/licenses/BSD-3-Clause',
'FreeBSD' => 'http://opensource.org/licenses/BSD-2-Clause',
'MIT' => 'http://www.opensource.org/licenses/mit-license.php',
'PHP' => 'http://opensource.org/licenses/PHP-3.0',
'PHP-3' => 'http://www.php.net/license/3_01.txt',
'PHP-3.0' => 'http://www.php.net/license/3_0.txt',
'PHP-3.01' => 'http://www.php.net/license/3_01.txt',
);
$dir = dir($this->dir);
- $fn = unslashify($dir->path) . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+ $fn = unslashify($dir->path) . "/$plugin_name/$plugin_name.php";
$info = false;
- if (!class_exists($plugin_name))
- include($fn);
+ if (!class_exists($plugin_name, false)) {
+ if (is_readable($fn))
+ include($fn);
+ else
+ return false;
+ }
if (class_exists($plugin_name))
- $info = call_user_func(array($plugin_name, 'info'));
+ $info = $plugin_name::info();
// fall back to composer.json file
if (!$info) {
$composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
- if (file_exists($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
+ if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
list($info['vendor'], $info['name']) = explode('/', $json['name']);
$info['version'] = $json['version'];
$info['license'] = $json['license'];
- if ($license_uri = $license_uris[$info['license']])
- $info['license_uri'] = $license_uri;
+ $info['uri'] = $json['homepage'];
+ $info['require'] = array_filter(array_keys((array)$json['require']), function($pname) {
+ if (strpos($pname, '/') == false)
+ return false;
+ list($vendor, $name) = explode('/', $pname);
+ return !($name == 'plugin-installer' || $vendor == 'pear-pear');
+ });
}
// read local composer.lock file (once)
if (!isset($composer_lock)) {
$composer_lock = @json_decode(@file_get_contents(INSTALL_PATH . "/composer.lock"), true);
if ($composer_lock['packages']) {
foreach ($composer_lock['packages'] as $i => $package) {
$composer_lock['installed'][$package['name']] = $package;
}
}
}
// load additional information from local composer.lock file
if ($lock = $composer_lock['installed'][$json['name']]) {
$info['version'] = $lock['version'];
$info['uri'] = $lock['homepage'] ? $lock['homepage'] : $lock['source']['uri'];
$info['src_uri'] = $lock['dist']['uri'] ? $lock['dist']['uri'] : $lock['source']['uri'];
}
}
// fall back to package.xml file
if (!$info) {
$package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
- if (file_exists($package) && ($file = file_get_contents($package))) {
+ if (is_readable($package) && ($file = file_get_contents($package))) {
$doc = new DOMDocument();
$doc->loadXML($file);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('rc', "http://pear.php.net/dtd/package-2.0");
// XPaths of plugin metadata elements
$metadata = array(
'name' => 'string(//rc:package/rc:name)',
'version' => 'string(//rc:package/rc:version/rc:release)',
'license' => 'string(//rc:package/rc:license)',
'license_uri' => 'string(//rc:package/rc:license/@uri)',
'src_uri' => 'string(//rc:package/rc:srcuri)',
'uri' => 'string(//rc:package/rc:uri)',
);
foreach ($metadata as $key => $path) {
$info[$key] = $xpath->evaluate($path);
}
// dependent required plugins (can be used, but not included in config)
$deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name');
for ($i = 0; $i < $deps->length; $i++) {
$dn = $deps->item($i)->nodeValue;
- $info['requires'][] = $dn;
+ $info['require'][] = $dn;
}
}
}
+ // At least provide the name
+ if (!$info && class_exists($plugin_name)) {
+ $info = array('name' => $plugin_name, 'version' => '--');
+ }
+ else if ($info['license'] && empty($info['license_uri']) && ($license_uri = $license_uris[$info['license']])) {
+ $info['license_uri'] = $license_uri;
+ }
+
return $info;
}
/**
* Allows a plugin object to register a callback for a certain hook
*
* @param string $hook Hook name
* @param mixed $callback String with global function name or array($obj, 'methodname')
*/
public function register_hook($hook, $callback)
{
if (is_callable($callback)) {
if (isset($this->deprecated_hooks[$hook])) {
rcube::raise_error(array('code' => 522, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Deprecated hook name. "
. $hook . ' -> ' . $this->deprecated_hooks[$hook]), true, false);
$hook = $this->deprecated_hooks[$hook];
}
$this->handlers[$hook][] = $callback;
}
else {
rcube::raise_error(array('code' => 521, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid callback function for $hook"), true, false);
}
}
/**
* Allow a plugin object to unregister a callback.
*
* @param string $hook Hook name
* @param mixed $callback String with global function name or array($obj, 'methodname')
*/
public function unregister_hook($hook, $callback)
{
$callback_id = array_search($callback, $this->handlers[$hook]);
if ($callback_id !== false) {
unset($this->handlers[$hook][$callback_id]);
}
}
/**
* Triggers a plugin hook.
* This is called from the application and executes all registered handlers
*
* @param string $hook Hook name
* @param array $args Named arguments (key->value pairs)
*
* @return array The (probably) altered hook arguments
*/
public function exec_hook($hook, $args = array())
{
if (!is_array($args)) {
$args = array('arg' => $args);
}
+ // TODO: avoid recusion by checking in_array($hook, $this->exec_stack) ?
+
$args += array('abort' => false);
- $this->active_hook = $hook;
+ array_push($this->exec_stack, $hook);
foreach ((array)$this->handlers[$hook] as $callback) {
$ret = call_user_func($callback, $args);
if ($ret && is_array($ret)) {
$args = $ret + $args;
}
if ($args['break']) {
break;
}
}
- $this->active_hook = false;
+ array_pop($this->exec_stack);
return $args;
}
/**
* Let a plugin register a handler for a specific request
*
* @param string $action Action name (_task=mail&_action=plugin.foo)
* @param string $owner Plugin name that registers this action
* @param mixed $callback Callback: string with global function name or array($obj, 'methodname')
* @param string $task Task name registered by this plugin
*/
public function register_action($action, $owner, $callback, $task = null)
{
// check action name
if ($task)
$action = $task.'.'.$action;
else if (strpos($action, 'plugin.') !== 0)
$action = 'plugin.'.$action;
// can register action only if it's not taken or registered by myself
if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) {
$this->actions[$action] = $callback;
$this->actionmap[$action] = $owner;
}
else {
rcube::raise_error(array('code' => 523, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register action $action;"
." already taken by another plugin"), true, false);
}
}
/**
* This method handles requests like _task=mail&_action=plugin.foo
* It executes the callback function that was registered with the given action.
*
* @param string $action Action name
*/
public function exec_action($action)
{
if (isset($this->actions[$action])) {
call_user_func($this->actions[$action]);
}
else if (rcube::get_instance()->action != 'refresh') {
rcube::raise_error(array('code' => 524, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No handler found for action $action"), true, true);
}
}
/**
* Register a handler function for template objects
*
* @param string $name Object name
* @param string $owner Plugin name that registers this action
* @param mixed $callback Callback: string with global function name or array($obj, 'methodname')
*/
public function register_handler($name, $owner, $callback)
{
// check name
if (strpos($name, 'plugin.') !== 0) {
$name = 'plugin.' . $name;
}
// can register handler only if it's not taken or registered by myself
if (is_object($this->output)
&& (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)
) {
$this->output->add_handler($name, $callback);
$this->objectsmap[$name] = $owner;
}
else {
rcube::raise_error(array('code' => 525, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register template handler $name;"
." already taken by another plugin or no output object available"), true, false);
}
}
/**
* Register this plugin to be responsible for a specific task
*
* @param string $task Task name (only characters [a-z0-9_-] are allowed)
* @param string $owner Plugin name that registers this action
*/
public function register_task($task, $owner)
{
// tasks are irrelevant in framework mode
if (!class_exists('rcmail', false)) {
return true;
}
if ($task != asciiwords($task, true)) {
rcube::raise_error(array('code' => 526, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid task name: $task."
." Only characters [a-z0-9_.-] are allowed"), true, false);
}
else if (in_array($task, rcmail::$main_tasks)) {
rcube::raise_error(array('code' => 526, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register taks $task;"
." already taken by another plugin or the application itself"), true, false);
}
else {
$this->tasks[$task] = $owner;
rcmail::$main_tasks[] = $task;
return true;
}
return false;
}
/**
* Checks whether the given task is registered by a plugin
*
* @param string $task Task name
*
* @return boolean True if registered, otherwise false
*/
public function is_plugin_task($task)
{
return $this->tasks[$task] ? true : false;
}
/**
* Check if a plugin hook is currently processing.
* Mainly used to prevent loops and recursion.
*
* @param string $hook Hook to check (optional)
*
* @return boolean True if any/the given hook is currently processed, otherwise false
*/
public function is_processing($hook = null)
{
- return $this->active_hook && (!$hook || $this->active_hook == $hook);
+ return count($this->exec_stack) > 0 && (!$hook || in_array($hook, $this->exec_stack));
}
/**
* Include a plugin script file in the current HTML page
*
* @param string $fn Path to script
*/
public function include_script($fn)
{
if (is_object($this->output) && $this->output->type == 'html') {
$src = $this->resource_url($fn);
$this->output->add_header(html::tag('script',
array('type' => "text/javascript", 'src' => $src)));
}
}
/**
* Include a plugin stylesheet in the current HTML page
*
* @param string $fn Path to stylesheet
*/
public function include_stylesheet($fn)
{
if (is_object($this->output) && $this->output->type == 'html') {
$src = $this->resource_url($fn);
$this->output->include_css($src);
}
}
/**
* Save the given HTML content to be added to a template container
*
* @param string $html HTML content
* @param string $container Template container identifier
*/
public function add_content($html, $container)
{
$this->template_contents[$container] .= $html . "\n";
}
/**
* Returns list of loaded plugins names
*
* @return array List of plugin names
*/
public function loaded_plugins()
{
return array_keys($this->plugins);
}
+ /**
+ * Returns loaded plugin
+ *
+ * @return rcube_plugin Plugin instance
+ */
+ public function get_plugin($name)
+ {
+ return $this->plugins[$name];
+ }
+
/**
* Callback for template_container hooks
*
* @param array $attrib
* @return array
*/
protected function template_container_hook($attrib)
{
$container = $attrib['name'];
return array('content' => $attrib['content'] . $this->template_contents[$container]);
}
/**
* Make the given file name link into the plugins directory
*
* @param string $fn Filename
* @return string
*/
protected function resource_url($fn)
{
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
return $this->url . $fn;
else
return $fn;
}
}
diff --git a/lib/ext/Roundcube/rcube_result_index.php b/lib/ext/Roundcube/rcube_result_index.php
index 058f25c..ffc1ad7 100644
--- a/lib/ext/Roundcube/rcube_result_index.php
+++ b/lib/ext/Roundcube/rcube_result_index.php
@@ -1,434 +1,436 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-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: |
| SORT/SEARCH/ESEARCH response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for accessing IMAP's SORT/SEARCH/ESEARCH result
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_index
{
+ public $incomplete = false;
+
protected $raw_data;
protected $mailbox;
protected $meta = array();
protected $params = array();
protected $order = 'ASC';
const SEPARATOR_ELEMENT = ' ';
/**
* Object constructor.
*/
public function __construct($mailbox = null, $data = null)
{
$this->mailbox = $mailbox;
$this->init($data);
}
/**
* Initializes object with SORT command response
*
* @param string $data IMAP response string
*/
public function init($data = null)
{
$this->meta = array();
$data = explode('*', (string)$data);
// ...skip unilateral untagged server responses
for ($i=0, $len=count($data); $i<$len; $i++) {
$data_item = &$data[$i];
if (preg_match('/^ SORT/i', $data_item)) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data_item = substr($data_item, 5);
break;
}
else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data_item = substr($data_item, strlen($m[0]));
if (strtoupper($m[1]) == 'ESEARCH') {
$data_item = trim($data_item);
// remove MODSEQ response
if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) {
$data_item = substr($data_item, 0, -strlen($m[0]));
$this->params['MODSEQ'] = $m[1];
}
// remove TAG response part
if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) {
$data_item = substr($data_item, strlen($m[0]));
}
// remove UID
$data_item = preg_replace('/^UID\s*/i', '', $data_item);
// ESEARCH parameters
while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) {
$param = strtoupper($m[1]);
$value = $m[2];
$this->params[$param] = $value;
$data_item = substr($data_item, strlen($m[0]));
if (in_array($param, array('COUNT', 'MIN', 'MAX'))) {
$this->meta[strtolower($param)] = (int) $value;
}
}
// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ?
// @TODO: work with compressed result?!
if (isset($this->params['ALL'])) {
$data_item = implode(self::SEPARATOR_ELEMENT,
rcube_imap_generic::uncompressMessageSet($this->params['ALL']));
}
}
break;
}
unset($data[$i]);
}
$data = array_filter($data);
if (empty($data)) {
return;
}
$data = array_shift($data);
$data = trim($data);
$data = preg_replace('/[\r\n]/', '', $data);
$data = preg_replace('/\s+/', ' ', $data);
$this->raw_data = $data;
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return $this->raw_data === null ? true : false;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->raw_data) ? true : false;
}
/**
* Returns number of elements in the result
*
* @return int Number of elements
*/
public function count()
{
if ($this->meta['count'] !== null)
return $this->meta['count'];
if (empty($this->raw_data)) {
$this->meta['count'] = 0;
$this->meta['length'] = 0;
}
else {
$this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
}
return $this->meta['count'];
}
/**
* Returns number of elements in the result.
* Alias for count() for compatibility with rcube_result_thread
*
* @return int Number of elements
*/
public function count_messages()
{
return $this->count();
}
/**
* Returns maximal message identifier in the result
*
* @return int Maximal message identifier
*/
public function max()
{
if (!isset($this->meta['max'])) {
$this->meta['max'] = (int) @max($this->get());
}
return $this->meta['max'];
}
/**
* Returns minimal message identifier in the result
*
* @return int Minimal message identifier
*/
public function min()
{
if (!isset($this->meta['min'])) {
$this->meta['min'] = (int) @min($this->get());
}
return $this->meta['min'];
}
/**
* Slices data set.
*
* @param $offset Offset (as for PHP's array_slice())
* @param $length Number of elements (as for PHP's array_slice())
*
*/
public function slice($offset, $length)
{
$data = $this->get();
$data = array_slice($data, $offset, $length);
$this->meta = array();
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Filters data set. Removes elements not listed in $ids list.
*
* @param array $ids List of IDs to remove.
*/
public function filter($ids = array())
{
$data = $this->get();
$data = array_intersect($data, $ids);
$this->meta = array();
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
if (empty($this->raw_data)) {
return;
}
// @TODO: maybe do this in chunks
$data = $this->get();
$data = array_reverse($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
$this->meta['pos'] = array();
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
*
* @return mixed False if message ID doesn't exist, True if exists or
* index of the element if $get_index=true
*/
public function exists($msgid, $get_index = false)
{
if (empty($this->raw_data)) {
return false;
}
$msgid = (int) $msgid;
$begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
$end = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
$get_index ? PREG_OFFSET_CAPTURE : null)
) {
if ($get_index) {
$idx = 0;
if ($m[0][1]) {
$idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
}
// cache position of this element, so we can use it in get_element()
$this->meta['pos'][$idx] = (int)$m[0][1];
return $idx;
}
return true;
}
return false;
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get()
{
if (empty($this->raw_data)) {
return array();
}
return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get_compressed()
{
if (empty($this->raw_data)) {
return '';
}
return rcube_imap_generic::compressMessageSet($this->get());
}
/**
* Return result element at specified index
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int Element value
*/
public function get_element($index)
{
$count = $this->count();
if (!$count) {
return null;
}
// first element
if ($index === 0 || $index === '0' || $index === 'FIRST') {
$pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
if ($pos === false)
$result = (int) $this->raw_data;
else
$result = (int) substr($this->raw_data, 0, $pos);
return $result;
}
// last element
if ($index === 'LAST' || $index == $count-1) {
$pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
if ($pos === false)
$result = (int) $this->raw_data;
else
$result = (int) substr($this->raw_data, $pos);
return $result;
}
// do we know the position of the element or the neighbour of it?
if (!empty($this->meta['pos'])) {
if (isset($this->meta['pos'][$index]))
$pos = $this->meta['pos'][$index];
else if (isset($this->meta['pos'][$index-1]))
$pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
$this->meta['pos'][$index-1] + 1);
else if (isset($this->meta['pos'][$index+1]))
$pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
$this->meta['pos'][$index+1] - $this->length() - 1);
if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
return (int) $m[1];
}
}
// Finally use less effective method
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
return $data[$index];
}
/**
* Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
* or internal data e.g. MAILBOX, ORDER
*
* @param string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param=null)
{
$params = $this->params;
$params['MAILBOX'] = $this->mailbox;
$params['ORDER'] = $this->order;
if ($param !== null) {
return $params[$param];
}
return $params;
}
/**
* Returns length of internal data representation
*
* @return int Data length
*/
protected function length()
{
if (!isset($this->meta['length'])) {
$this->meta['length'] = strlen($this->raw_data);
}
return $this->meta['length'];
}
}
diff --git a/lib/ext/Roundcube/rcube_result_multifolder.php b/lib/ext/Roundcube/rcube_result_multifolder.php
new file mode 100644
index 0000000..786ee85
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_result_multifolder.php
@@ -0,0 +1,337 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2005-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: |
+ | SORT/SEARCH/ESEARCH response handler |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class holding a set of rcube_result_index instances that together form a
+ * result set of a multi-folder search
+ *
+ * @package Framework
+ * @subpackage Storage
+ */
+class rcube_result_multifolder
+{
+ public $multi = true;
+ public $sets = array();
+ public $incomplete = false;
+ public $folder;
+
+ protected $meta = array();
+ protected $index = array();
+ protected $folders = array();
+ protected $order = 'ASC';
+ protected $sorting;
+
+
+ /**
+ * Object constructor.
+ */
+ public function __construct($folders = array())
+ {
+ $this->folders = $folders;
+ $this->meta = array('count' => 0);
+ }
+
+
+ /**
+ * Initializes object with SORT command response
+ *
+ * @param string $data IMAP response string
+ */
+ public function add($result)
+ {
+ $this->sets[] = $result;
+
+ if ($result->count()) {
+ $this->append_result($result);
+ }
+ else if ($result->incomplete) {
+ $this->incomplete = true;
+ }
+ }
+
+ /**
+ * Append message UIDs from the given result to our index
+ */
+ protected function append_result($result)
+ {
+ $this->meta['count'] += $result->count();
+
+ // append UIDs to global index
+ $folder = $result->get_parameters('MAILBOX');
+ $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get());
+
+ $this->index = array_merge($this->index, $index);
+ }
+
+ /**
+ * Store a global index of (sorted) message UIDs
+ */
+ public function set_message_index($headers, $sort_field, $sort_order)
+ {
+ $this->index = array();
+ foreach ($headers as $header) {
+ $this->index[] = $header->uid . '-' . $header->folder;
+ }
+
+ $this->sorting = $sort_field;
+ $this->order = $sort_order;
+ }
+
+ /**
+ * Checks the result from IMAP command
+ *
+ * @return bool True if the result is an error, False otherwise
+ */
+ public function is_error()
+ {
+ return false;
+ }
+
+
+ /**
+ * Checks if the result is empty
+ *
+ * @return bool True if the result is empty, False otherwise
+ */
+ public function is_empty()
+ {
+ return empty($this->sets) || $this->meta['count'] == 0;
+ }
+
+
+ /**
+ * Returns number of elements in the result
+ *
+ * @return int Number of elements
+ */
+ public function count()
+ {
+ return $this->meta['count'];
+ }
+
+
+ /**
+ * Returns number of elements in the result.
+ * Alias for count() for compatibility with rcube_result_thread
+ *
+ * @return int Number of elements
+ */
+ public function count_messages()
+ {
+ return $this->count();
+ }
+
+
+ /**
+ * Reverts order of elements in the result
+ */
+ public function revert()
+ {
+ $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
+ $this->index = array();
+
+ // revert order in all sub-sets
+ foreach ($this->sets as $set) {
+ if ($this->order != $set->get_parameters('ORDER')) {
+ $set->revert();
+ }
+
+ $folder = $set->get_parameters('MAILBOX');
+ $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $set->get());
+
+ $this->index = array_merge($this->index, $index);
+ }
+ }
+
+
+ /**
+ * Check if the given message ID exists in the object
+ *
+ * @param int $msgid Message ID
+ * @param bool $get_index When enabled element's index will be returned.
+ * Elements are indexed starting with 0
+ * @return mixed False if message ID doesn't exist, True if exists or
+ * index of the element if $get_index=true
+ */
+ public function exists($msgid, $get_index = false)
+ {
+ if (!empty($this->folder)) {
+ $msgid .= '-' . $this->folder;
+ }
+
+ return array_search($msgid, $this->index);
+ }
+
+
+ /**
+ * Filters data set. Removes elements listed in $ids list.
+ *
+ * @param array $ids List of IDs to remove.
+ * @param string $folder IMAP folder
+ */
+ public function filter($ids = array(), $folder = null)
+ {
+ $this->meta['count'] = 0;
+ foreach ($this->sets as $set) {
+ if ($set->get_parameters('MAILBOX') == $folder) {
+ $set->filter($ids);
+ }
+
+ $this->meta['count'] += $set->count();
+ }
+ }
+
+ /**
+ * Slices data set.
+ *
+ * @param $offset Offset (as for PHP's array_slice())
+ * @param $length Number of elements (as for PHP's array_slice())
+ *
+ */
+ public function slice($offset, $length)
+ {
+ $data = array_slice($this->get(), $offset, $length);
+
+ $this->index = $data;
+ $this->meta['count'] = count($data);
+ }
+
+ /**
+ * Filters data set. Removes elements not listed in $ids list.
+ *
+ * @param array $ids List of IDs to keep.
+ */
+ public function intersect($ids = array())
+ {
+ // not implemented
+ }
+
+ /**
+ * Return all messages in the result.
+ *
+ * @return array List of message IDs
+ */
+ public function get()
+ {
+ return $this->index;
+ }
+
+
+ /**
+ * Return all messages in the result.
+ *
+ * @return array List of message IDs
+ */
+ public function get_compressed()
+ {
+ return '';
+ }
+
+
+ /**
+ * Return result element at specified index
+ *
+ * @param int|string $index Element's index or "FIRST" or "LAST"
+ *
+ * @return int Element value
+ */
+ public function get_element($idx)
+ {
+ switch ($idx) {
+ case 'FIRST': return $this->index[0];
+ case 'LAST': return end($this->index);
+ default: return $this->index[$idx];
+ }
+ }
+
+
+ /**
+ * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
+ * or internal data e.g. MAILBOX, ORDER
+ *
+ * @param string $param Parameter name
+ *
+ * @return array|string Response parameters or parameter value
+ */
+ public function get_parameters($param=null)
+ {
+ $params = array(
+ 'SORT' => $this->sorting,
+ 'ORDER' => $this->order,
+ 'MAILBOX' => $this->folders,
+ );
+
+ if ($param !== null) {
+ return $params[$param];
+ }
+
+ return $params;
+ }
+
+ /**
+ * Returns the stored result object for a particular folder
+ *
+ * @param string $folder Folder name
+ * @return false|obejct rcube_result_* instance of false if none found
+ */
+ public function get_set($folder)
+ {
+ foreach ($this->sets as $set) {
+ if ($set->get_parameters('MAILBOX') == $folder) {
+ return $set;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns length of internal data representation
+ *
+ * @return int Data length
+ */
+ protected function length()
+ {
+ return $this->count();
+ }
+
+
+ /* Serialize magic methods */
+
+ public function __sleep()
+ {
+ return array('sets','folders','sorting','order');
+ }
+
+ public function __wakeup()
+ {
+ // restore index from saved result sets
+ $this->meta = array('count' => 0);
+
+ foreach ($this->sets as $result) {
+ if ($result->count()) {
+ $this->append_result($result);
+ }
+ else if ($result->incomplete) {
+ $this->incomplete = true;
+ }
+ }
+ }
+
+}
diff --git a/lib/ext/Roundcube/rcube_result_thread.php b/lib/ext/Roundcube/rcube_result_thread.php
index ceaaf59..1687616 100644
--- a/lib/ext/Roundcube/rcube_result_thread.php
+++ b/lib/ext/Roundcube/rcube_result_thread.php
@@ -1,679 +1,681 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-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: |
| THREAD response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for accessing IMAP's THREAD result
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_thread
{
+ public $incomplete = false;
+
protected $raw_data;
protected $mailbox;
protected $meta = array();
protected $order = 'ASC';
const SEPARATOR_ELEMENT = ' ';
const SEPARATOR_ITEM = '~';
const SEPARATOR_LEVEL = ':';
/**
* Object constructor.
*/
public function __construct($mailbox = null, $data = null)
{
$this->mailbox = $mailbox;
$this->init($data);
}
/**
* Initializes object with IMAP command response
*
* @param string $data IMAP response string
*/
public function init($data = null)
{
$this->meta = array();
$data = explode('*', (string)$data);
// ...skip unilateral untagged server responses
for ($i=0, $len=count($data); $i<$len; $i++) {
if (preg_match('/^ THREAD/i', $data[$i])) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data[$i] = substr($data[$i], 7);
break;
}
unset($data[$i]);
}
if (empty($data)) {
return;
}
$data = array_shift($data);
$data = trim($data);
$data = preg_replace('/[\r\n]/', '', $data);
$data = preg_replace('/\s+/', ' ', $data);
$this->raw_data = $this->parse_thread($data);
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return $this->raw_data === null ? true : false;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->raw_data) ? true : false;
}
/**
* Returns number of elements (threads) in the result
*
* @return int Number of elements
*/
public function count()
{
if ($this->meta['count'] !== null)
return $this->meta['count'];
if (empty($this->raw_data)) {
$this->meta['count'] = 0;
}
else {
$this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
}
if (!$this->meta['count'])
$this->meta['messages'] = 0;
return $this->meta['count'];
}
/**
* Returns number of all messages in the result
*
* @return int Number of elements
*/
public function count_messages()
{
if ($this->meta['messages'] !== null)
return $this->meta['messages'];
if (empty($this->raw_data)) {
$this->meta['messages'] = 0;
}
else {
$this->meta['messages'] = 1
+ substr_count($this->raw_data, self::SEPARATOR_ELEMENT)
+ substr_count($this->raw_data, self::SEPARATOR_ITEM);
}
if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1)
$this->meta['count'] = $this->meta['messages'];
return $this->meta['messages'];
}
/**
* Returns maximum message identifier in the result
*
* @return int Maximum message identifier
*/
public function max()
{
if (!isset($this->meta['max'])) {
$this->meta['max'] = (int) @max($this->get());
}
return $this->meta['max'];
}
/**
* Returns minimum message identifier in the result
*
* @return int Minimum message identifier
*/
public function min()
{
if (!isset($this->meta['min'])) {
$this->meta['min'] = (int) @min($this->get());
}
return $this->meta['min'];
}
/**
* Slices data set.
*
* @param $offset Offset (as for PHP's array_slice())
* @param $length Number of elements (as for PHP's array_slice())
*/
public function slice($offset, $length)
{
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
$data = array_slice($data, $offset, $length);
$this->meta = array();
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Filters data set. Removes threads not listed in $roots list.
*
* @param array $roots List of IDs of thread roots.
*/
public function filter($roots)
{
$datalen = strlen($this->raw_data);
$roots = array_flip($roots);
$result = '';
$start = 0;
$this->meta = array();
$this->meta['count'] = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
// extract root message ID
if ($npos = strpos($elem, self::SEPARATOR_ITEM)) {
$root = (int) substr($elem, 0, $npos);
}
else {
$root = $elem;
}
if (isset($roots[$root])) {
$this->meta['count']++;
$result .= self::SEPARATOR_ELEMENT . $elem;
}
}
$this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT);
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
if (empty($this->raw_data)) {
return;
}
$this->meta['pos'] = array();
$datalen = strlen($this->raw_data);
$result = '';
$start = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$result = $elem . self::SEPARATOR_ELEMENT . $result;
}
$this->raw_data = rtrim($result, self::SEPARATOR_ELEMENT);
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
*
* @return boolean True on success, False if message ID doesn't exist
*/
public function exists($msgid, $get_index = false)
{
$msgid = (int) $msgid;
$begin = implode('|', array(
'^',
preg_quote(self::SEPARATOR_ELEMENT, '/'),
preg_quote(self::SEPARATOR_LEVEL, '/'),
));
$end = implode('|', array(
'$',
preg_quote(self::SEPARATOR_ELEMENT, '/'),
preg_quote(self::SEPARATOR_ITEM, '/'),
));
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
$get_index ? PREG_OFFSET_CAPTURE : null)
) {
if ($get_index) {
$idx = 0;
if ($m[0][1]) {
$idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1)
+ substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1);
}
// cache position of this element, so we can use it in get_element()
$this->meta['pos'][$idx] = (int)$m[0][1];
return $idx;
}
return true;
}
return false;
}
/**
* Return IDs of all messages in the result. Threaded data will be flattened.
*
* @return array List of message identifiers
*/
public function get()
{
if (empty($this->raw_data)) {
return array();
}
$regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/')
. '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/')
.')/';
return preg_split($regexp, $this->raw_data);
}
/**
* Return all messages in the result.
*
* @return array List of message identifiers
*/
public function get_compressed()
{
if (empty($this->raw_data)) {
return '';
}
return rcube_imap_generic::compressMessageSet($this->get());
}
/**
* Return result element at specified index (all messages, not roots)
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int Element value
*/
public function get_element($index)
{
$count = $this->count();
if (!$count) {
return null;
}
// first element
if ($index === 0 || $index === '0' || $index === 'FIRST') {
preg_match('/^([0-9]+)/', $this->raw_data, $m);
$result = (int) $m[1];
return $result;
}
// last element
if ($index === 'LAST' || $index == $count-1) {
preg_match('/([0-9]+)$/', $this->raw_data, $m);
$result = (int) $m[1];
return $result;
}
// do we know the position of the element or the neighbour of it?
if (!empty($this->meta['pos'])) {
$element = preg_quote(self::SEPARATOR_ELEMENT, '/');
$item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?';
$regexp = '(' . $element . '|' . $item . ')';
if (isset($this->meta['pos'][$index])) {
if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index]))
$result = $m[1];
}
else if (isset($this->meta['pos'][$index-1])) {
// get chunk of data after previous element
$data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50);
$data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position
$data = preg_replace("/^$regexp/", '', $data); // remove separator
if (preg_match('/^([0-9]+)/', $data, $m))
$result = $m[1];
}
else if (isset($this->meta['pos'][$index+1])) {
// get chunk of data before next element
$pos = max(0, $this->meta['pos'][$index+1] - 50);
$len = min(50, $this->meta['pos'][$index+1]);
$data = substr($this->raw_data, $pos, $len);
$data = preg_replace("/$regexp\$/", '', $data); // remove separator
if (preg_match('/([0-9]+)$/', $data, $m))
$result = $m[1];
}
if (isset($result)) {
return (int) $result;
}
}
// Finally use less effective method
$data = $this->get();
return $data[$index];
}
/**
* Returns response parameters e.g. MAILBOX, ORDER
*
* @param string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param=null)
{
$params = $this->params;
$params['MAILBOX'] = $this->mailbox;
$params['ORDER'] = $this->order;
if ($param !== null) {
return $params[$param];
}
return $params;
}
/**
* THREAD=REFS sorting implementation (based on provided index)
*
* @param rcube_result_index $index Sorted message identifiers
*/
public function sort($index)
{
$this->sort_order = $index->get_parameters('ORDER');
if (empty($this->raw_data)) {
return;
}
// when sorting search result it's good to make the index smaller
if ($index->count() != $this->count_messages()) {
$index->filter($this->get());
}
$result = array_fill_keys($index->get(), null);
$datalen = strlen($this->raw_data);
$start = 0;
// Here we're parsing raw_data twice, we want only one big array
// in memory at a time
// Assign roots
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$items = explode(self::SEPARATOR_ITEM, $elem);
$root = (int) array_shift($items);
if ($root) {
$result[$root] = $root;
foreach ($items as $item) {
list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item);
$result[$id] = $root;
}
}
}
// get only unique roots
$result = array_filter($result); // make sure there are no nulls
$result = array_unique($result);
// Re-sort raw data
$result = array_fill_keys($result, null);
$start = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$npos = strpos($elem, self::SEPARATOR_ITEM);
$root = (int) ($npos ? substr($elem, 0, $npos) : $elem);
$result[$root] = $elem;
}
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $result);
}
/**
* Returns data as tree
*
* @return array Data tree
*/
public function get_tree()
{
$datalen = strlen($this->raw_data);
$result = array();
$start = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$items = explode(self::SEPARATOR_ITEM, $elem);
$result[array_shift($items)] = $this->build_thread($items);
$start = $pos + 1;
}
return $result;
}
/**
* Returns thread depth and children data
*
* @return array Thread data
*/
public function get_thread_data()
{
$data = $this->get_tree();
$depth = array();
$children = array();
$this->build_thread_data($data, $depth, $children);
return array($depth, $children);
}
/**
* Creates 'depth' and 'children' arrays from stored thread 'tree' data.
*/
protected function build_thread_data($data, &$depth, &$children, $level = 0)
{
foreach ((array)$data as $key => $val) {
$empty = empty($val) || !is_array($val);
$children[$key] = !$empty;
$depth[$key] = $level;
if (!$empty) {
$this->build_thread_data($val, $depth, $children, $level + 1);
}
}
}
/**
* Converts part of the raw thread into an array
*/
protected function build_thread($items, $level = 1, &$pos = 0)
{
$result = array();
for ($len=count($items); $pos < $len; $pos++) {
list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]);
if ($level == $lv) {
$pos++;
$result[$id] = $this->build_thread($items, $level+1, $pos);
}
else {
$pos--;
break;
}
}
return $result;
}
/**
* IMAP THREAD response parser
*/
protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0)
{
// Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
// 7 times instead :-) See comments on http://uk2.php.net/references and this article:
// http://derickrethans.nl/files/phparch-php-variables-article.pdf
$node = '';
if (!$end) {
$end = strlen($str);
}
// Let's try to store data in max. compacted stracture as a string,
// arrays handling is much more expensive
// For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))
// -- 2
// -- 3
// \-- 6
// |-- 4
// | \-- 23
// |
// \-- 44
// \-- 7
// \-- 96
//
// The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96
if ($str[$begin] != '(') {
// find next bracket
$stop = $begin + strcspn($str, '()', $begin, $end - $begin);
$messages = explode(' ', trim(substr($str, $begin, $stop - $begin)));
if (empty($messages)) {
return $node;
}
foreach ($messages as $msg) {
if ($msg) {
$node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
$this->meta['messages']++;
$depth++;
}
}
if ($stop < $end) {
$node .= $this->parse_thread($str, $stop, $end, $depth);
}
}
else {
$off = $begin;
while ($off < $end) {
$start = $off;
$off++;
$n = 1;
while ($n > 0) {
$p = strpos($str, ')', $off);
if ($p === false) {
// error, wrong structure, mismatched brackets in IMAP THREAD response
// @TODO: write error to the log or maybe set $this->raw_data = null;
return $node;
}
$p1 = strpos($str, '(', $off);
if ($p1 !== false && $p1 < $p) {
$off = $p1 + 1;
$n++;
}
else {
$off = $p + 1;
$n--;
}
}
$thread = $this->parse_thread($str, $start + 1, $off - 1, $depth);
if ($thread) {
if (!$depth) {
if ($node) {
$node .= self::SEPARATOR_ELEMENT;
}
}
$node .= $thread;
}
}
}
return $node;
}
}
diff --git a/lib/ext/Roundcube/rcube_session.php b/lib/ext/Roundcube/rcube_session.php
index caca262..8306a06 100644
--- a/lib/ext/Roundcube/rcube_session.php
+++ b/lib/ext/Roundcube/rcube_session.php
@@ -1,797 +1,807 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | Copyright (C) 2005-2014, 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: |
| Provide database supported session management |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide database supported session storage
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_session
{
private $db;
private $ip;
private $start;
private $changed;
private $time_diff = 0;
private $reloaded = false;
private $appends = array();
private $unsets = array();
private $gc_handlers = array();
private $cookiename = 'roundcube_sessauth';
private $vars;
private $key;
private $now;
private $secret = '';
private $ip_check = false;
private $logging = false;
private $storage;
private $memcache;
+ /**
+ * Blocks session data from being written to database.
+ * Can be used if write-race conditions are to be expected
+ * @var boolean
+ */
+ public $nowrite = false;
+
/**
* Default constructor
*/
public function __construct($db, $config)
{
$this->db = $db;
$this->start = microtime(true);
$this->ip = rcube_utils::remote_addr();
$this->logging = $config->get('log_session', false);
$lifetime = $config->get('session_lifetime', 1) * 60;
$this->set_lifetime($lifetime);
// use memcache backend
$this->storage = $config->get('session_storage', 'db');
if ($this->storage == 'memcache') {
$this->memcache = rcube::get_instance()->get_memcache();
// set custom functions for PHP session management if memcache is available
if ($this->memcache) {
ini_set('session.serialize_handler', 'php');
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
array($this, 'mc_read'),
array($this, 'mc_write'),
array($this, 'mc_destroy'),
array($this, 'gc'));
}
else {
rcube::raise_error(array('code' => 604, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Failed to connect to memcached. Please check configuration"),
true, true);
}
}
else if ($this->storage != 'php') {
ini_set('session.serialize_handler', 'php');
// set custom functions for PHP session management
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
array($this, 'db_read'),
array($this, 'db_write'),
array($this, 'db_destroy'),
array($this, 'gc'));
+
+ $this->table_name = $this->db->table_name('session', true);
}
}
/**
* Wrapper for session_start()
*/
public function start()
{
session_start();
// copy some session properties to object vars
if ($this->storage == 'php') {
$this->key = session_id();
$this->ip = $_SESSION['__IP'];
$this->changed = $_SESSION['__MTIME'];
}
}
public function open($save_path, $session_name)
{
return true;
}
public function close()
{
return true;
}
/**
* Delete session data for the given key
*
* @param string Session ID
*/
public function destroy($key)
{
return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key);
}
/**
* Wrapper for session_write_close()
*/
public function write_close()
{
if ($this->storage == 'php') {
$_SESSION['__IP'] = $this->ip;
$_SESSION['__MTIME'] = time();
}
session_write_close();
// write_close() is called on script shutdown, see rcube::shutdown()
// execute cleanup functionality if enabled by session gc handler
// we do this after closing the session for better performance
$this->gc_shutdown();
}
/**
* Read session data from database
*
* @param string Session ID
*
* @return string Session vars
*/
public function db_read($key)
{
$sql_result = $this->db->query(
- "SELECT vars, ip, changed, " . $this->db->now() . " AS ts"
- . " FROM " . $this->db->table_name('session')
- . " WHERE sess_id = ?", $key);
+ "SELECT `vars`, `ip`, `changed`, " . $this->db->now() . " AS ts"
+ . " FROM {$this->table_name} WHERE `sess_id` = ?", $key);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$this->time_diff = time() - strtotime($sql_arr['ts']);
$this->changed = strtotime($sql_arr['changed']);
$this->ip = $sql_arr['ip'];
$this->vars = base64_decode($sql_arr['vars']);
$this->key = $key;
return !empty($this->vars) ? (string) $this->vars : '';
}
return null;
}
/**
* Save session data.
* handler for session_read()
*
* @param string Session ID
* @param string Serialized session vars
*
* @return boolean True on success
*/
public function db_write($key, $vars)
{
- $now = $this->db->now();
- $table = $this->db->table_name('session');
- $ts = microtime(true);
+ $now = $this->db->now();
+ $ts = microtime(true);
+
+ if ($this->nowrite)
+ return true;
// no session row in DB (db_read() returns false)
if (!$this->key) {
$oldvars = null;
}
// use internal data from read() for fast requests (up to 0.5 sec.)
else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
$oldvars = $this->vars;
}
else { // else read data again from DB
$oldvars = $this->db_read($key);
}
if ($oldvars !== null) {
$newvars = $this->_fixvars($vars, $oldvars);
if ($newvars !== $oldvars) {
- $this->db->query("UPDATE $table "
- . "SET changed = $now, vars = ? WHERE sess_id = ?",
+ $this->db->query("UPDATE {$this->table_name} "
+ . "SET `changed` = $now, `vars` = ? WHERE `sess_id` = ?",
base64_encode($newvars), $key);
}
else if ($ts - $this->changed + $this->time_diff > $this->lifetime / 2) {
- $this->db->query("UPDATE $table SET changed = $now"
- . " WHERE sess_id = ?", $key);
+ $this->db->query("UPDATE {$this->table_name} SET `changed` = $now"
+ . " WHERE `sess_id` = ?", $key);
}
}
else {
- $this->db->query("INSERT INTO $table (sess_id, vars, ip, created, changed)"
+ $this->db->query("INSERT INTO {$this->table_name}"
+ . " (`sess_id`, `vars`, `ip`, `created`, `changed`)"
. " VALUES (?, ?, ?, $now, $now)",
$key, base64_encode($vars), (string)$this->ip);
}
return true;
}
/**
* Merge vars with old vars and apply unsets
*/
private function _fixvars($vars, $oldvars)
{
if ($oldvars !== null) {
$a_oldvars = $this->unserialize($oldvars);
if (is_array($a_oldvars)) {
// remove unset keys on oldvars
foreach ((array)$this->unsets as $var) {
if (isset($a_oldvars[$var])) {
unset($a_oldvars[$var]);
}
else {
$path = explode('.', $var);
$k = array_pop($path);
$node = &$this->get_node($path, $a_oldvars);
unset($node[$k]);
}
}
$newvars = $this->serialize(array_merge(
(array)$a_oldvars, (array)$this->unserialize($vars)));
}
else {
$newvars = $vars;
}
}
$this->unsets = array();
return $newvars;
}
/**
* Handler for session_destroy()
*
* @param string Session ID
*
* @return boolean True on success
*/
public function db_destroy($key)
{
if ($key) {
- $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?",
- $this->db->table_name('session')), $key);
+ $this->db->query("DELETE FROM {$this->table_name} WHERE `sess_id` = ?", $key);
}
return true;
}
/**
* Read session data from memcache
*
* @param string Session ID
* @return string Session vars
*/
public function mc_read($key)
{
if ($value = $this->memcache->get($key)) {
$arr = unserialize($value);
$this->changed = $arr['changed'];
$this->ip = $arr['ip'];
$this->vars = $arr['vars'];
$this->key = $key;
return !empty($this->vars) ? (string) $this->vars : '';
}
return null;
}
/**
* Save session data.
* handler for session_read()
*
* @param string Session ID
* @param string Serialized session vars
*
* @return boolean True on success
*/
public function mc_write($key, $vars)
{
$ts = microtime(true);
// no session data in cache (mc_read() returns false)
if (!$this->key)
$oldvars = null;
// use internal data for fast requests (up to 0.5 sec.)
else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
$oldvars = $this->vars;
else // else read data again
$oldvars = $this->mc_read($key);
$newvars = $oldvars !== null ? $this->_fixvars($vars, $oldvars) : $vars;
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)),
MEMCACHE_COMPRESSED, $this->lifetime + 60);
}
return true;
}
/**
* Handler for session_destroy() with memcache backend
*
* @param string Session ID
*
* @return boolean True on success
*/
public function mc_destroy($key)
{
if ($key) {
// #1488592: use 2nd argument
$this->memcache->delete($key, 0);
}
return true;
}
/**
* Execute registered garbage collector routines
*/
public function gc($maxlifetime)
{
// move gc execution to the script shutdown function
// see rcube::shutdown() and rcube_session::write_close()
return $this->gc_enabled = $maxlifetime;
}
/**
* Register additional garbage collector functions
*
* @param mixed Callback function
*/
public function register_gc_handler($func)
{
foreach ($this->gc_handlers as $handler) {
if ($handler == $func) {
return;
}
}
$this->gc_handlers[] = $func;
}
/**
* Garbage collector handler to run on script shutdown
*/
protected function gc_shutdown()
{
if ($this->gc_enabled) {
// just delete all expired sessions
if ($this->storage == 'db') {
- $this->db->query("DELETE FROM " . $this->db->table_name('session')
- . " WHERE changed < " . $this->db->now(-$this->gc_enabled));
+ $this->db->query("DELETE FROM {$this->table_name}"
+ . " WHERE `changed` < " . $this->db->now(-$this->gc_enabled));
}
foreach ($this->gc_handlers as $fct) {
call_user_func($fct);
}
}
}
/**
* Generate and set new session id
*
* @param boolean $destroy If enabled the current session will be destroyed
*/
public function regenerate_id($destroy=true)
{
session_regenerate_id($destroy);
$this->vars = null;
$this->key = session_id();
return true;
}
/**
* Append the given value to the certain node in the session data array
*
* @param string Path denoting the session variable where to append the value
* @param string Key name under which to append the new value (use null for appending to an indexed list)
* @param mixed Value to append to the session data array
*/
public function append($path, $key, $value)
{
// re-read session data from DB because it might be outdated
if (!$this->reloaded && microtime(true) - $this->start > 0.5) {
$this->reload();
$this->reloaded = true;
$this->start = microtime(true);
}
$node = &$this->get_node(explode('.', $path), $_SESSION);
if ($key !== null) {
$node[$key] = $value;
$path .= '.' . $key;
}
else {
$node[] = $value;
}
$this->appends[] = $path;
// when overwriting a previously unset variable
if ($this->unsets[$path])
unset($this->unsets[$path]);
}
/**
* Unset a session variable
*
* @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5)
* @return boolean True on success
*/
public function remove($var=null)
{
if (empty($var)) {
return $this->destroy(session_id());
}
$this->unsets[] = $var;
if (isset($_SESSION[$var])) {
unset($_SESSION[$var]);
}
else {
$path = explode('.', $var);
$key = array_pop($path);
$node = &$this->get_node($path, $_SESSION);
unset($node[$key]);
}
return true;
}
/**
* Kill this session
*/
public function kill()
{
$this->vars = null;
$this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
$this->destroy(session_id());
rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
}
/**
* Re-read session data from storage backend
*/
public function reload()
{
// collect updated data from previous appends
$merge_data = array();
foreach ((array)$this->appends as $var) {
$path = explode('.', $var);
$value = $this->get_node($path, $_SESSION);
$k = array_pop($path);
$node = &$this->get_node($path, $merge_data);
$node[$k] = $value;
}
if ($this->key && $this->memcache)
$data = $this->mc_read($this->key);
else if ($this->key)
$data = $this->db_read($this->key);
if ($data) {
session_decode($data);
// apply appends and unsets to reloaded data
$_SESSION = array_merge_recursive($_SESSION, $merge_data);
foreach ((array)$this->unsets as $var) {
if (isset($_SESSION[$var])) {
unset($_SESSION[$var]);
}
else {
$path = explode('.', $var);
$k = array_pop($path);
$node = &$this->get_node($path, $_SESSION);
unset($node[$k]);
}
}
}
}
/**
* Returns a reference to the node in data array referenced by the given path.
* e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments']
*/
private function &get_node($path, &$data_arr)
{
$node = &$data_arr;
if (!empty($path)) {
foreach ((array)$path as $key) {
if (!isset($node[$key]))
$node[$key] = array();
$node = &$node[$key];
}
}
return $node;
}
/**
* Serialize session data
*/
private function serialize($vars)
{
$data = '';
if (is_array($vars)) {
foreach ($vars as $var=>$value)
$data .= $var.'|'.serialize($value);
}
else {
$data = 'b:0;';
}
return $data;
}
/**
* Unserialize session data
* http://www.php.net/manual/en/function.session-decode.php#56106
*/
private function unserialize($str)
{
$str = (string)$str;
$endptr = strlen($str);
$p = 0;
$serialized = '';
$items = 0;
$level = 0;
while ($p < $endptr) {
$q = $p;
while ($str[$q] != '|')
if (++$q >= $endptr)
break 2;
if ($str[$p] == '!') {
$p++;
$has_value = false;
}
else {
$has_value = true;
}
$name = substr($str, $p, $q - $p);
$q++;
$serialized .= 's:' . strlen($name) . ':"' . $name . '";';
if ($has_value) {
for (;;) {
$p = $q;
switch (strtolower($str[$q])) {
case 'n': // null
case 'b': // boolean
case 'i': // integer
case 'd': // decimal
do $q++;
while ( ($q < $endptr) && ($str[$q] != ';') );
$q++;
$serialized .= substr($str, $p, $q - $p);
if ($level == 0)
break 2;
break;
case 'r': // reference
$q+= 2;
for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++)
$id .= $str[$q];
$q++;
// increment pointer because of outer array
$serialized .= 'R:' . ($id + 1) . ';';
if ($level == 0)
break 2;
break;
case 's': // string
$q+=2;
for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++)
$length .= $str[$q];
$q+=2;
$q+= (int)$length + 2;
$serialized .= substr($str, $p, $q - $p);
if ($level == 0)
break 2;
break;
case 'a': // array
case 'o': // object
do $q++;
while ($q < $endptr && $str[$q] != '{');
$q++;
$level++;
$serialized .= substr($str, $p, $q - $p);
break;
case '}': // end of array|object
$q++;
$serialized .= substr($str, $p, $q - $p);
if (--$level == 0)
break 2;
break;
default:
return false;
}
}
}
else {
$serialized .= 'N;';
$q += 2;
}
$items++;
$p = $q;
}
return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
}
/**
* Setter for session lifetime
*/
public function set_lifetime($lifetime)
{
$this->lifetime = max(120, $lifetime);
// valid time range is now - 1/2 lifetime to now + 1/2 lifetime
$now = time();
$this->now = $now - ($now % ($this->lifetime / 2));
}
/**
* Getter for remote IP saved with this session
*/
public function get_ip()
{
return $this->ip;
}
/**
* Setter for cookie encryption secret
*/
function set_secret($secret)
{
$this->secret = $secret;
}
/**
* Enable/disable IP check
*/
function set_ip_check($check)
{
$this->ip_check = $check;
}
/**
* Setter for the cookie name used for session cookie
*/
function set_cookiename($cookiename)
{
if ($cookiename) {
$this->cookiename = $cookiename;
}
}
/**
* Check session authentication cookie
*
* @return boolean True if valid, False if not
*/
function check_auth()
{
$this->cookie = $_COOKIE[$this->cookiename];
$result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
if (!$result) {
$this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
}
if ($result && $this->_mkcookie($this->now) != $this->cookie) {
$this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
$result = false;
// Check if using id from a previous time slot
for ($i = 1; $i <= 2; $i++) {
$prev = $this->now - ($this->lifetime / 2) * $i;
if ($this->_mkcookie($prev) == $this->cookie) {
$this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
$this->set_auth_cookie();
$result = true;
}
}
}
if (!$result) {
$this->log("Session authentication failed for " . $this->key
. "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
}
return $result;
}
/**
* Set session authentication cookie
*/
function set_auth_cookie()
{
$this->cookie = $this->_mkcookie($this->now);
rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
$_COOKIE[$this->cookiename] = $this->cookie;
}
/**
* Create session cookie from session data
*
* @param int Time slot to use
*/
function _mkcookie($timeslot)
{
$auth_string = "$this->key,$this->secret,$timeslot";
return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
}
/**
* Writes debug information to the log
*/
function log($line)
{
if ($this->logging) {
rcube::write_log('session', $line);
}
}
}
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index e9a3607..0627807 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -1,420 +1,430 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2011-2013, Kolab Systems AG |
| Copyright (C) 2008-2013, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| 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 Framework
* @subpackage Utils
*/
class rcube_spellchecker
{
private $matches = array();
private $engine;
private $backend;
private $lang;
private $rc;
private $error;
private $options = array();
private $dict;
private $have_dict;
/**
* 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';
$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'),
);
$cls = 'rcube_spellcheck_' . $this->engine;
if (class_exists($cls)) {
$this->backend = new $cls($this, $this->lang);
$this->backend->options = $this->options;
}
else {
$this->error = "Unknown spellcheck engine '$this->engine'";
}
}
/**
* Return a list of supported languages
*/
function languages()
{
// trust configuration
$configured = $this->rc->config->get('spellcheck_languages');
if (!empty($configured) && is_array($configured) && !$configured[0]) {
return $configured;
}
else if (!empty($configured)) {
$langs = (array)$configured;
}
else if ($this->backend) {
$langs = $this->backend->languages();
}
// load index
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
// add correct labels
$languages = array();
foreach ($langs as $lang) {
$langc = strtolower(substr($lang, 0, 2));
$alias = $rcube_language_aliases[$langc];
if (!$alias) {
$alias = $langc.'_'.strtoupper($langc);
}
if ($rcube_languages[$lang]) {
$languages[$lang] = $rcube_languages[$lang];
}
else if ($rcube_languages[$alias]) {
$languages[$lang] = $rcube_languages[$alias];
}
else {
$languages[$lang] = ucfirst($lang);
}
}
// remove possible duplicates (#1489395)
$languages = array_unique($languages);
asort($languages);
return $languages;
}
/**
* 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->backend) {
$this->matches = $this->backend->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->backend) {
return $this->backend->get_suggestions($word);
}
return array();
}
/**
* Returns misspelled words
*
* @param string $text The content for spellchecking. If empty content
* used for check() method will be used.
*
* @return array List of misspelled words
*/
function get_words($text = null, $is_html=false)
{
if ($is_html) {
$text = $this->html2text($text);
}
if ($this->backend) {
return $this->backend->get_words($text);
}
return array();
}
/**
* Returns checking result in XML (Googiespell) format
*
* @return string XML content
*/
function get_xml()
{
// send output
$out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
foreach ((array)$this->matches as $item) {
$out .= '<c o="'.$item[1].'" l="'.$item[2].'">';
$out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
$out .= '</c>';
}
$out .= '</spellresult>';
return $out;
}
/**
* Returns checking result (misspelled words with suggestions)
*
* @return array Spellchecking result. An array indexed by word.
*/
function get()
{
$result = array();
foreach ((array)$this->matches as $item) {
if ($this->engine == 'pspell') {
$word = $item[0];
}
else {
$word = mb_substr($this->content, $item[1], $item[2], RCUBE_CHARSET);
}
- $result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
+
+ if (is_array($item[4])) {
+ $suggestions = $item[4];
+ }
+ else if (empty($item[4])) {
+ $suggestions = array();
+ }
+ else {
+ $suggestions = explode("\t", $item[4]);
+ }
+
+ $result[$word] = $suggestions;
}
return $result;
}
/**
* Returns error message
*
* @return string Error message
*/
function error()
{
return $this->error ? $this->error : ($this->backend ? $this->backend->error() : false);
}
private function html2text($text)
{
$h2t = new rcube_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->quote_identifier('language') . " = ?",
+ "UPDATE " . $this->rc->db->table_name('dictionary', true)
+ ." SET `data` = ?"
+ ." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+ ." AND `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->quote_identifier('language') . " = ?",
+ "DELETE FROM " . $this->rc->db->table_name('dictionary', true)
+ ." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+ ." AND `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->quote_identifier('language') . ", data) VALUES (?, ?, ?)",
+ "INSERT INTO " . $this->rc->db->table_name('dictionary', true)
+ ." (`user_id`, `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();
$sql_result = $this->rc->db->query(
- "SELECT data FROM ".$this->rc->db->table_name('dictionary')
- ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
- ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+ "SELECT `data` FROM " . $this->rc->db->table_name('dictionary', true)
+ ." WHERE `user_id` ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+ ." AND `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_storage.php b/lib/ext/Roundcube/rcube_storage.php
index c09f053..ccb28c6 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -1,1005 +1,1052 @@
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| Copyright (C) 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: |
| Mail Storage Engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Abstract class for accessing mail messages storage server
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
abstract class rcube_storage
{
/**
* Instance of connection object e.g. rcube_imap_generic
*
* @var mixed
*/
public $conn;
+ /**
+ * List of supported special folder types
+ *
+ * @var array
+ */
+ public static $folder_types = array('drafts', 'sent', 'junk', 'trash');
+
protected $folder = 'INBOX';
protected $default_charset = 'ISO-8859-1';
- protected $default_folders = array('INBOX');
protected $search_set;
protected $options = array('auth_type' => 'check');
protected $page_size = 10;
protected $threading = false;
/**
* All (additional) headers used (in any way) by Roundcube
* Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
* (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
*
* @var array
*/
protected $all_headers = array(
'IN-REPLY-TO',
'BCC',
'SENDER',
'MESSAGE-ID',
'CONTENT-TRANSFER-ENCODING',
'REFERENCES',
'X-DRAFT-INFO',
'MAIL-FOLLOWUP-TO',
'MAIL-REPLY-TO',
'RETURN-PATH',
);
const UNKNOWN = 0;
const NOPERM = 1;
const READONLY = 2;
const TRYCREATE = 3;
const INUSE = 4;
const OVERQUOTA = 5;
const ALREADYEXISTS = 6;
const NONEXISTENT = 7;
const CONTACTADMIN = 8;
/**
* Connect to the server
*
* @param string $host Host to connect
* @param string $user Username for IMAP account
* @param string $pass Password for IMAP account
* @param integer $port Port to connect to
* @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
*
* @return boolean TRUE on success, FALSE on failure
*/
abstract function connect($host, $user, $pass, $port = 143, $use_ssl = null);
/**
* Close connection. Usually done on script shutdown
*/
abstract function close();
/**
* Checks connection state.
*
* @return boolean TRUE on success, FALSE on failure
*/
abstract function is_connected();
/**
* Check connection state, connect if not connected.
*
* @return bool Connection state.
*/
abstract function check_connection();
/**
* Returns code of last error
*
* @return int Error code
*/
abstract function get_error_code();
/**
* Returns message of last error
*
* @return string Error message
*/
abstract function get_error_str();
/**
* Returns code of last command response
*
* @return int Response code (class constant)
*/
abstract function get_response_code();
/**
* Set connection and class options
*
* @param array $opt Options array
*/
public function set_options($opt)
{
$this->options = array_merge($this->options, (array)$opt);
}
+ /**
+ * Get connection/class option
+ *
+ * @param string $name Option name
+ *
+ * @param mixed Option value
+ */
+ public function get_option($name)
+ {
+ return $this->options[$name];
+ }
+
+
/**
* Activate/deactivate debug mode.
*
* @param boolean $dbg True if conversation with the server should be logged
*/
abstract function set_debug($dbg = true);
/**
* Set default message charset.
*
* This will be used for message decoding if a charset specification is not available
*
* @param string $cs Charset string
*/
public function set_charset($cs)
{
$this->default_charset = $cs;
}
- /**
- * This list of folders will be listed above all other folders
- *
- * @param array $arr Indexed list of folder names
- */
- public function set_default_folders($arr)
- {
- if (is_array($arr)) {
- $this->default_folders = $arr;
-
- // add inbox if not included
- if (!in_array('INBOX', $this->default_folders)) {
- array_unshift($this->default_folders, 'INBOX');
- }
- }
- }
-
-
/**
* Set internal folder reference.
* All operations will be perfomed on this folder.
*
* @param string $folder Folder name
*/
public function set_folder($folder)
{
if ($this->folder === $folder) {
return;
}
$this->folder = $folder;
}
/**
* Returns the currently used folder name
*
* @return string Name of the folder
*/
public function get_folder()
{
return $this->folder;
}
/**
* Set internal list page number.
*
* @param int $page Page number to list
*/
public function set_page($page)
{
$this->list_page = (int) $page;
}
/**
* Gets internal list page number.
*
* @return int Page number
*/
public function get_page()
{
return $this->list_page;
}
/**
* Set internal page size
*
* @param int $size Number of messages to display on one page
*/
public function set_pagesize($size)
{
$this->page_size = (int) $size;
}
/**
* Get internal page size
*
* @return int Number of messages to display on one page
*/
public function get_pagesize()
{
return $this->page_size;
}
/**
* Save a search result for future message listing methods.
*
* @param mixed $set Search set in driver specific format
*/
abstract function set_search_set($set);
/**
* Return the saved search set.
*
* @return array Search set in driver specific format, NULL if search wasn't initialized
*/
abstract function get_search_set();
/**
* Returns the storage server's (IMAP) capability
*
* @param string $cap Capability name
*
* @return mixed Capability value or TRUE if supported, FALSE if not
*/
abstract function get_capability($cap);
/**
* Sets threading flag to the best supported THREAD algorithm.
* Enable/Disable threaded mode.
*
* @param boolean $enable TRUE to enable and FALSE
*
* @return mixed Threading algorithm or False if THREAD is not supported
*/
public function set_threading($enable = false)
{
$this->threading = false;
if ($enable && ($caps = $this->get_capability('THREAD'))) {
$methods = array('REFS', 'REFERENCES', 'ORDEREDSUBJECT');
$methods = array_intersect($methods, $caps);
$this->threading = array_shift($methods);
}
return $this->threading;
}
/**
* Get current threading flag.
*
* @return mixed Threading algorithm or False if THREAD is not supported or disabled
*/
public function get_threading()
{
return $this->threading;
}
/**
* Checks the PERMANENTFLAGS capability of the current folder
* and returns true if the given flag is supported by the server.
*
* @param string $flag Permanentflag name
*
* @return boolean True if this flag is supported
*/
abstract function check_permflag($flag);
/**
* Returns the delimiter that is used by the server
* for folder hierarchy separation.
*
* @return string Delimiter string
*/
abstract function get_hierarchy_delimiter();
/**
* Get namespace
*
* @param string $name Namespace array index: personal, other, shared, prefix
*
* @return array Namespace data
*/
abstract function get_namespace($name = null);
/**
* Get messages count for a specific folder.
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
*/
abstract function count($folder = null, $mode = 'ALL', $force = false, $status = true);
/**
* Public method for listing message flags
*
* @param string $folder Folder name
* @param array $uids Message UIDs
* @param int $mod_seq Optional MODSEQ value
*
* @return array Indexed array with message flags
*/
abstract function list_flags($folder, $uids, $mod_seq = null);
/**
* Public method for listing headers.
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
abstract function list_messages($folder = null, $page = null, $sort_field = null, $sort_order = null, $slice = 0);
/**
* Return sorted list of message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
abstract function index($folder = null, $sort_field = null, $sort_order = null);
/**
* Invoke search request to the server.
*
* @param string $folder Folder name to search in
* @param string $str Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
*
* @todo: Search criteria should be provided in non-IMAP format, eg. array
*/
abstract function search($folder = null, $str = 'ALL', $charset = null, $sort_field = null);
/**
* Direct (real and simple) search request (without result sorting and caching).
*
* @param string $folder Folder name to search in
* @param string $str Search string
*
* @return rcube_result_index Search result (UIDs)
*/
abstract function search_once($folder = null, $str = 'ALL');
/**
* Refresh saved search set
*
* @return array Current search set
*/
abstract function refresh_search();
/* --------------------------------
* messages management
* --------------------------------*/
/**
* Fetch message headers and body structure from the server and build
* an object structure similar to the one generated by PEAR::Mail_mimeDecode
*
* @param int $uid Message UID to fetch
* @param string $folder Folder to read from
*
* @return object rcube_message_header Message data
*/
abstract function get_message($uid, $folder = null);
/**
* Return message headers object of a specific message
*
* @param int $id Message sequence ID or UID
* @param string $folder Folder to read from
* @param bool $force True to skip cache
*
* @return rcube_message_header Message headers
*/
abstract function get_message_headers($uid, $folder = null, $force = false);
/**
* Fetch message body of a specific message from the server
*
* @param int $uid Message UID
* @param string $part Part number
* @param rcube_message_part $o_part Part object created by get_structure()
* @param mixed $print True to print part, ressource to write part contents in
* @param resource $fp File pointer to save the message part
* @param boolean $skip_charset_conv Disables charset conversion
*
* @return string Message/part body if not printed
*/
abstract function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null, $skip_charset_conv = false);
/**
* Fetch message body of a specific message from the server
*
* @param int $uid Message UID
*
* @return string $part Message/part body
* @see rcube_imap::get_message_part()
*/
public function get_body($uid, $part = 1)
{
$headers = $this->get_message_headers($uid);
return rcube_charset::convert($this->get_message_part($uid, $part, null),
$headers->charset ? $headers->charset : $this->default_charset);
}
/**
* Returns the whole message source as string (or saves to a file)
*
* @param int $uid Message UID
* @param resource $fp File pointer to save the message
*
* @return string Message source string
*/
abstract function get_raw_body($uid, $fp = null);
/**
* Returns the message headers as string
*
* @param int $uid Message UID
*
* @return string Message headers string
*/
abstract function get_raw_headers($uid);
/**
* Sends the whole message source to stdout
*
* @param int $uid Message UID
* @param bool $formatted Enables line-ending formatting
*/
abstract function print_raw_body($uid, $formatted = true);
/**
* Set message flag to one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
* @param boolean $skip_cache True to skip message cache clean up
*
* @return bool Operation status
*/
abstract function set_flag($uids, $flag, $folder = null, $skip_cache = false);
/**
* Remove message flag for one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
*
* @return bool Operation status
* @see set_flag
*/
public function unset_flag($uids, $flag, $folder = null)
{
return $this->set_flag($uids, 'UN'.$flag, $folder);
}
/**
* Append a mail message (source) to a specific folder.
*
* @param string $folder Target folder
* @param string|array $message The message source string or filename
* or array (of strings and file pointers)
* @param string $headers Headers string if $message contains only the body
* @param boolean $is_file True if $message is a filename
* @param array $flags Message flags
* @param mixed $date Message internal date
*
* @return int|bool Appended message UID or True on success, False on error
*/
abstract function save_message($folder, &$message, $headers = '', $is_file = false, $flags = array(), $date = null);
/**
* Move message(s) from one folder to another.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to Target folder
* @param string $from Source folder
*
* @return boolean True on success, False on error
*/
abstract function move_message($uids, $to, $from = null);
/**
* Copy message(s) from one mailbox to another.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to Target folder
* @param string $from Source folder
*
* @return boolean True on success, False on error
*/
abstract function copy_message($uids, $to, $from = null);
/**
* Mark message(s) as deleted and expunge.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Source folder
*
* @return boolean True on success, False on error
*/
abstract function delete_message($uids, $folder = null);
/**
* Expunge message(s) and clear the cache.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on error
*/
abstract function expunge_message($uids, $folder = null, $clear_cache = true);
/**
* Parse message UIDs input
*
* @param mixed $uids UIDs array or comma-separated list or '*' or '1:*'
*
* @return array Two elements array with UIDs converted to list and ALL flag
*/
protected function parse_uids($uids)
{
if ($uids === '*' || $uids === '1:*') {
if (empty($this->search_set)) {
$uids = '1:*';
$all = true;
}
// get UIDs from current search set
else {
$uids = join(',', $this->search_set->get());
}
}
else {
if (is_array($uids)) {
$uids = join(',', $uids);
}
else if (strpos($uids, ':')) {
$uids = join(',', rcube_imap_generic::uncompressMessageSet($uids));
}
if (preg_match('/[^0-9,]/', $uids)) {
$uids = '';
}
}
return array($uids, (bool) $all);
}
/* --------------------------------
* folder managment
* --------------------------------*/
/**
* Get a list of subscribed folders.
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
* @param string $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array List of folders
*/
abstract function list_folders_subscribed($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false);
/**
* Get a list of all folders available on the server.
*
* @param string $root IMAP root dir
* @param string $name Optional name pattern
* @param mixed $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array Indexed array with folder names
*/
abstract function list_folders($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false);
/**
* Subscribe to a specific folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
abstract function subscribe($folders);
/**
* Unsubscribe folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
abstract function unsubscribe($folders);
/**
* Create a new folder on the server.
*
* @param string $folder New folder name
* @param boolean $subscribe True if the newvfolder should be subscribed
*
* @return boolean True on success, False on error
*/
abstract function create_folder($folder, $subscribe = false);
/**
* Set a new name to an existing folder
*
* @param string $folder Folder to rename
* @param string $new_name New folder name
*
* @return boolean True on success, False on error
*/
abstract function rename_folder($folder, $new_name);
/**
* Remove a folder from the server.
*
* @param string $folder Folder name
*
* @return boolean True on success, False on error
*/
abstract function delete_folder($folder);
/**
* Send expunge command and clear the cache.
*
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on error
*/
public function expunge_folder($folder = null, $clear_cache = true)
{
return $this->expunge_message('*', $folder, $clear_cache);
}
/**
* Remove all messages in a folder..
*
* @param string $folder Folder name
*
* @return boolean True on success, False on error
*/
public function clear_folder($folder = null)
{
return $this->delete_message('*', $folder);
}
/**
* Checks if folder exists and is subscribed
*
* @param string $folder Folder name
* @param boolean $subscription Enable subscription checking
*
* @return boolean True if folder exists, False otherwise
*/
abstract function folder_exists($folder, $subscription = false);
/**
* Get folder size (size of all messages in a folder)
*
* @param string $folder Folder name
*
* @return int Folder size in bytes, False on error
*/
abstract function folder_size($folder);
/**
* Returns the namespace where the folder is in
*
* @param string $folder Folder name
*
* @return string One of 'personal', 'other' or 'shared'
*/
abstract function folder_namespace($folder);
/**
* Gets folder attributes (from LIST response, e.g. \Noselect, \Noinferiors).
*
* @param string $folder Folder name
* @param bool $force Set to True if attributes should be refreshed
*
* @return array Options list
*/
abstract function folder_attributes($folder, $force = false);
/**
* Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
* PERMANENTFLAGS, UIDNEXT, UNSEEN
*
* @param string $folder Folder name
*
* @return array Data
*/
abstract function folder_data($folder);
/**
* Returns extended information about the folder.
*
* @param string $folder Folder name
*
* @return array Data
*/
abstract function folder_info($folder);
/**
* Returns current status of a folder (compared to the last time use)
*
* @param string $folder Folder name
* @param array $diff Difference data
*
* @return int Folder status
*/
abstract function folder_status($folder = null, &$diff = array());
/**
* Synchronizes messages cache.
*
* @param string $folder Folder name
*/
abstract function folder_sync($folder);
/**
* Modify folder name according to namespace.
* For output it removes prefix of the personal namespace if it's possible.
* For input it adds the prefix. Use it before creating a folder in root
* of the folders tree.
*
* @param string $folder Folder name
* @param string $mode Mode name (out/in)
*
* @return string Folder name
*/
abstract function mod_folder($folder, $mode = 'out');
/**
* Create all folders specified as default
*/
public function create_default_folders()
{
+ $rcube = rcube::get_instance();
+
// create default folders if they do not exist
- foreach ($this->default_folders as $folder) {
- if (!$this->folder_exists($folder)) {
- $this->create_folder($folder, true);
+ foreach (self::$folder_types as $type) {
+ if ($folder = $rcube->config->get($type . '_mbox')) {
+ if (!$this->folder_exists($folder)) {
+ $this->create_folder($folder, true, $type);
+ }
+ else if (!$this->folder_exists($folder, true)) {
+ $this->subscribe($folder);
+ }
}
- else if (!$this->folder_exists($folder, true)) {
- $this->subscribe($folder);
+ }
+ }
+
+
+ /**
+ * Check if specified folder is a special folder
+ */
+ public function is_special_folder($name)
+ {
+ return $name == 'INBOX' || in_array($name, $this->get_special_folders());
+ }
+
+
+ /**
+ * Return configured special folders
+ */
+ public function get_special_folders($forced = false)
+ {
+ // getting config might be expensive, store special folders in memory
+ if (!isset($this->icache['special-folders'])) {
+ $rcube = rcube::get_instance();
+ $this->icache['special-folders'] = array();
+
+ foreach (self::$folder_types as $type) {
+ if ($folder = $rcube->config->get($type . '_mbox')) {
+ $this->icache['special-folders'][$type] = $folder;
+ }
}
}
+
+ return $this->icache['special-folders'];
+ }
+
+
+ /**
+ * Set special folder associations stored in backend
+ */
+ public function set_special_folders($specials)
+ {
+ // should be overriden by storage class if backend supports special folders (SPECIAL-USE)
+ unset($this->icache['special-folders']);
}
/**
* Get mailbox quota information.
*
+ * @param string $folder Folder name
+ *
* @return mixed Quota info or False if not supported
*/
- abstract function get_quota();
+ abstract function get_quota($folder = null);
/* -----------------------------------------
* ACL and METADATA methods
* ----------------------------------------*/
/**
* Changes the ACL on the specified folder (SETACL)
*
* @param string $folder Folder name
* @param string $user User name
* @param string $acl ACL string
*
* @return boolean True on success, False on failure
*/
abstract function set_acl($folder, $user, $acl);
/**
* Removes any <identifier,rights> pair for the
* specified user from the ACL for the specified
* folder (DELETEACL).
*
* @param string $folder Folder name
* @param string $user User name
*
* @return boolean True on success, False on failure
*/
abstract function delete_acl($folder, $user);
/**
* Returns the access control list for a folder (GETACL).
*
* @param string $folder Folder name
*
* @return array User-rights array on success, NULL on error
*/
abstract function get_acl($folder);
/**
* Returns information about what rights can be granted to the
* user (identifier) in the ACL for the folder (LISTRIGHTS).
*
* @param string $folder Folder name
* @param string $user User name
*
* @return array List of user rights
*/
abstract function list_rights($folder, $user);
/**
* Returns the set of rights that the current user has to a folder (MYRIGHTS).
*
* @param string $folder Folder name
*
* @return array MYRIGHTS response on success, NULL on error
*/
abstract function my_rights($folder);
/**
* Sets metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
*/
abstract function set_metadata($folder, $entries);
/**
* Unsets metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
*/
abstract function delete_metadata($folder, $entries);
/**
* Returns folder metadata/annotations (GETMETADATA/GETANNOTATION).
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
*
* @return array Metadata entry-value hash array on success, NULL on error
*/
abstract function get_metadata($folder, $entries, $options = array());
/* -----------------------------------------
* Cache related functions
* ----------------------------------------*/
/**
* 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
*/
abstract function clear_cache($key = null, $prefix_mode = false);
/**
* Returns cached value
*
* @param string $key Cache key
*
* @return mixed Cached value
*/
abstract function get_cache($key);
/**
* Delete outdated cache entries
*/
abstract function cache_gc();
} // end class rcube_storage
diff --git a/lib/ext/tnef_decoder.php b/lib/ext/Roundcube/rcube_tnef_decoder.php
similarity index 88%
rename from lib/ext/tnef_decoder.php
rename to lib/ext/Roundcube/rcube_tnef_decoder.php
index e6ccc23..f047ba0 100644
--- a/lib/ext/tnef_decoder.php
+++ b/lib/ext/Roundcube/rcube_tnef_decoder.php
@@ -1,331 +1,341 @@
<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | Copyright (C) 2002-2010, The Horde Project (http://www.horde.org/) |
+ | |
+ | 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: |
+ | MS-TNEF format decoder |
+ +-----------------------------------------------------------------------+
+ | Author: Jan Schneider <jan@horde.org> |
+ | Author: Michael Slusarz <slusarz@horde.org> |
+ +-----------------------------------------------------------------------+
+*/
+
/**
- * The Horde's class allows MS-TNEF data to be displayed.
- *
- * The TNEF rendering is based on code by:
+ * MS-TNEF format decoder based on code by:
* Graham Norbury <gnorbury@bondcar.com>
* Original design by:
* Thomas Boll <tb@boll.ch>, Mark Simpson <damned@world.std.com>
*
- * Copyright 2002-2010 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Jan Schneider <jan@horde.org>
- * @author Michael Slusarz <slusarz@horde.org>
- * @package Horde_Compress
+ * @package Framework
+ * @subpackage Storage
*/
-class tnef_decoder
+class rcube_tnef_decoder
{
const SIGNATURE = 0x223e9f78;
const LVL_MESSAGE = 0x01;
const LVL_ATTACHMENT = 0x02;
const ASUBJECT = 0x88004;
const AMCLASS = 0x78008;
const ATTACHDATA = 0x6800f;
const AFILENAME = 0x18010;
const ARENDDATA = 0x69002;
const AMAPIATTRS = 0x69005;
const AVERSION = 0x89006;
const MAPI_NULL = 0x0001;
const MAPI_SHORT = 0x0002;
const MAPI_INT = 0x0003;
const MAPI_FLOAT = 0x0004;
const MAPI_DOUBLE = 0x0005;
const MAPI_CURRENCY = 0x0006;
const MAPI_APPTIME = 0x0007;
const MAPI_ERROR = 0x000a;
const MAPI_BOOLEAN = 0x000b;
const MAPI_OBJECT = 0x000d;
const MAPI_INT8BYTE = 0x0014;
const MAPI_STRING = 0x001e;
const MAPI_UNICODE_STRING = 0x001f;
const MAPI_SYSTIME = 0x0040;
const MAPI_CLSID = 0x0048;
const MAPI_BINARY = 0x0102;
const MAPI_ATTACH_LONG_FILENAME = 0x3707;
const MAPI_ATTACH_MIME_TAG = 0x370E;
const MAPI_NAMED_TYPE_ID = 0x0000;
const MAPI_NAMED_TYPE_STRING = 0x0001;
const MAPI_MV_FLAG = 0x1000;
/**
* Decompress the data.
*
* @param string $data The data to decompress.
* @param array $params An array of arguments needed to decompress the
* data.
*
* @return mixed The decompressed data.
*/
public function decompress($data, $params = array())
{
$out = array();
if ($this->_geti($data, 32) == self::SIGNATURE) {
$this->_geti($data, 16);
while (strlen($data) > 0) {
switch ($this->_geti($data, 8)) {
case self::LVL_MESSAGE:
$this->_decodeMessage($data);
break;
case self::LVL_ATTACHMENT:
$this->_decodeAttachment($data, $out);
break;
}
}
}
return array_reverse($out);
}
/**
* TODO
*
* @param string &$data The data string.
* @param integer $bits How many bits to retrieve.
*
* @return TODO
*/
protected function _getx(&$data, $bits)
{
$value = null;
if (strlen($data) >= $bits) {
$value = substr($data, 0, $bits);
$data = substr_replace($data, '', 0, $bits);
}
return $value;
}
/**
* TODO
*
* @param string &$data The data string.
* @param integer $bits How many bits to retrieve.
*
* @return TODO
*/
protected function _geti(&$data, $bits)
{
$bytes = $bits / 8;
$value = null;
if (strlen($data) >= $bytes) {
$value = ord($data[0]);
if ($bytes >= 2) {
$value += (ord($data[1]) << 8);
}
if ($bytes >= 4) {
$value += (ord($data[2]) << 16) + (ord($data[3]) << 24);
}
$data = substr_replace($data, '', 0, $bytes);
}
return $value;
}
/**
* TODO
*
* @param string &$data The data string.
* @param string $attribute TODO
*/
protected function _decodeAttribute(&$data, $attribute)
{
/* Data. */
$this->_getx($data, $this->_geti($data, 32));
/* Checksum. */
$this->_geti($data, 16);
}
/**
* TODO
*
* @param string $data The data string.
* @param array &$attachment_data TODO
*/
protected function _extractMapiAttributes($data, &$attachment_data)
{
/* Number of attributes. */
$number = $this->_geti($data, 32);
while ((strlen($data) > 0) && $number--) {
$have_mval = false;
$num_mval = 1;
$named_id = $value = null;
$attr_type = $this->_geti($data, 16);
$attr_name = $this->_geti($data, 16);
if (($attr_type & self::MAPI_MV_FLAG) != 0) {
$have_mval = true;
$attr_type = $attr_type & ~self::MAPI_MV_FLAG;
}
if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) {
$this->_getx($data, 16);
$named_type = $this->_geti($data, 32);
switch ($named_type) {
case self::MAPI_NAMED_TYPE_ID:
$named_id = $this->_geti($data, 32);
$attr_name = $named_id;
break;
case self::MAPI_NAMED_TYPE_STRING:
$attr_name = 0x9999;
$idlen = $this->_geti($data, 32);
$datalen = $idlen + ((4 - ($idlen % 4)) % 4);
$named_id = substr($this->_getx($data, $datalen), 0, $idlen);
break;
}
}
if ($have_mval) {
$num_mval = $this->_geti($data, 32);
}
switch ($attr_type) {
case self::MAPI_SHORT:
$value = $this->_geti($data, 16);
break;
case self::MAPI_INT:
case self::MAPI_BOOLEAN:
for ($i = 0; $i < $num_mval; $i++) {
$value = $this->_geti($data, 32);
}
break;
case self::MAPI_FLOAT:
case self::MAPI_ERROR:
$value = $this->_getx($data, 4);
break;
case self::MAPI_DOUBLE:
case self::MAPI_APPTIME:
case self::MAPI_CURRENCY:
case self::MAPI_INT8BYTE:
case self::MAPI_SYSTIME:
$value = $this->_getx($data, 8);
break;
case self::MAPI_STRING:
case self::MAPI_UNICODE_STRING:
case self::MAPI_BINARY:
case self::MAPI_OBJECT:
$num_vals = ($have_mval) ? $num_mval : $this->_geti($data, 32);
for ($i = 0; $i < $num_vals; $i++) {
$length = $this->_geti($data, 32);
/* Pad to next 4 byte boundary. */
$datalen = $length + ((4 - ($length % 4)) % 4);
if ($attr_type == self::MAPI_STRING) {
--$length;
}
/* Read and truncate to length. */
$value = substr($this->_getx($data, $datalen), 0, $length);
}
break;
}
/* Store any interesting attributes. */
switch ($attr_name) {
case self::MAPI_ATTACH_LONG_FILENAME:
$value = str_replace("\0", '', $value);
/* Used in preference to AFILENAME value. */
$attachment_data[0]['name'] = preg_replace('/.*[\/](.*)$/', '\1', $value);
break;
case self::MAPI_ATTACH_MIME_TAG:
$value = str_replace("\0", '', $value);
/* Is this ever set, and what is format? */
$attachment_data[0]['type'] = preg_replace('/^(.*)\/.*/', '\1', $value);
$attachment_data[0]['subtype'] = preg_replace('/.*\/(.*)$/', '\1', $value);
break;
}
}
}
/**
* TODO
*
* @param string &$data The data string.
*/
protected function _decodeMessage(&$data)
{
$this->_decodeAttribute($data, $this->_geti($data, 32));
}
/**
* TODO
*
* @param string &$data The data string.
* @param array &$attachment_data TODO
*/
protected function _decodeAttachment(&$data, &$attachment_data)
{
$attribute = $this->_geti($data, 32);
switch ($attribute) {
case self::ARENDDATA:
/* Marks start of new attachment. */
$this->_getx($data, $this->_geti($data, 32));
/* Checksum */
$this->_geti($data, 16);
/* Add a new default data block to hold details of this
attachment. Reverse order is easier to handle later! */
array_unshift($attachment_data, array('type' => 'application',
'subtype' => 'octet-stream',
'name' => 'unknown',
'stream' => ''));
break;
case self::AFILENAME:
$value = $this->_getx($data, $this->_geti($data, 32));
$value = str_replace("\0", '', $value);
/* Strip path. */
$attachment_data[0]['name'] = preg_replace('/.*[\/](.*)$/', '\1', $value);
/* Checksum */
$this->_geti($data, 16);
break;
case self::ATTACHDATA:
/* The attachment itself. */
$length = $this->_geti($data, 32);
$attachment_data[0]['size'] = $length;
$attachment_data[0]['stream'] = $this->_getx($data, $length);
/* Checksum */
$this->_geti($data, 16);
break;
case self::AMAPIATTRS:
$length = $this->_geti($data, 32);
$value = $this->_getx($data, $length);
/* Checksum */
$this->_geti($data, 16);
$this->_extractMapiAttributes($value, $attachment_data);
break;
default:
$this->_decodeAttribute($data, $attribute);
}
}
-
}
diff --git a/lib/ext/Roundcube/rcube_user.php b/lib/ext/Roundcube/rcube_user.php
index e232736..b2110df 100644
--- a/lib/ext/Roundcube/rcube_user.php
+++ b/lib/ext/Roundcube/rcube_user.php
@@ -1,752 +1,746 @@
<?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: |
| This class represents a system user linked and provides access |
| to the related database records. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class representing a system user
*
* @package Framework
* @subpackage Core
*/
class rcube_user
{
public $ID;
public $data;
public $language;
/**
* Holds database connection.
*
* @var rcube_db
*/
private $db;
/**
* Framework object.
*
* @var rcube
*/
private $rc;
/**
* Internal identities cache
*
* @var array
*/
private $identities = array();
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);
+ "SELECT * FROM " . $this->db->table_name('users', true)
+ . " 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', 'mail')
* @return string Full user name or its part
*/
function get_username($part = null)
{
if ($this->data['username']) {
// return real name
if (!$part) {
return $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()
{
$prefs = array();
if (!empty($this->language))
$prefs['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;
$plugin = $this->rc->plugins->exec_hook('preferences_update', array(
'userid' => $this->ID, 'prefs' => $a_user_prefs, 'old' => (array)$this->get_prefs()));
if (!empty($plugin['abort'])) {
return;
}
$a_user_prefs = $plugin['prefs'];
$old_prefs = $plugin['old'];
$config = $this->rc->config;
// merge (partial) prefs array with existing settings
$save_prefs = $a_user_prefs + $old_prefs;
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 ($value === null || (!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 = ?",
+ "UPDATE ".$this->db->table_name('users', true).
+ " 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;
}
/**
* Generate a unique hash to identify this user which
*/
function get_hash()
{
$key = substr($this->rc->config->get('des_key'), 1, 4);
return md5($this->data['user_id'] . $key . $this->data['username'] . '@' . $this->data['mail_host']);
}
/**
* Get default identity of this user
*
* @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)
{
$id = (int)$id;
// cache identities for better performance
if (!array_key_exists($id, $this->identities)) {
- $result = $this->list_identities($id ? 'AND identity_id = ' . $id : '');
+ $result = $this->list_identities($id ? "AND `identity_id` = $id" : '');
$this->identities[$id] = $result[0];
}
return $this->identities[$id];
}
/**
* Return a list of all identities linked with this user
*
* @param string $sql_add Optional WHERE clauses
* @param bool $formatted Format identity email and name
*
* @return array List of identities
*/
function list_identities($sql_add = '', $formatted = false)
{
$result = array();
$sql_result = $this->db->query(
- "SELECT * FROM ".$this->db->table_name('identities').
- " WHERE del <> 1 AND user_id = ?".
+ "SELECT * FROM ".$this->db->table_name('identities', true).
+ " WHERE `del` <> 1 AND `user_id` = ?".
($sql_add ? " ".$sql_add : "").
- " ORDER BY ".$this->db->quote_identifier('standard')." DESC, name ASC, identity_id ASC",
+ " ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
$this->ID);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($formatted) {
$ascii_email = format_email($sql_arr['email']);
$utf8_email = format_email(rcube_utils::idn_to_utf8($ascii_email));
$sql_arr['email_ascii'] = $ascii_email;
$sql_arr['email'] = $utf8_email;
$sql_arr['ident'] = format_email_recipient($ascii_email, $sql_arr['name']);
}
$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->quote_identifier($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";
+ $sql = "UPDATE ".$this->db->table_name('identities', true).
+ " 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));
$this->identities = array();
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->quote_identifier($col);
$insert_values[] = $value;
}
- $insert_cols[] = 'user_id';
+ $insert_cols[] = $this->db->quote_identifier('user_id');
$insert_values[] = $this->ID;
- $sql = "INSERT INTO ".$this->db->table_name('identities').
- " (changed, ".join(', ', $insert_cols).")".
+ $sql = "INSERT INTO ".$this->db->table_name('identities', true).
+ " (`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));
$this->identities = array();
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",
+ "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities', true).
+ " 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 = ?",
+ "UPDATE ".$this->db->table_name('identities', true).
+ " SET `del` = 1, `changed` = ".$this->db->now().
+ " WHERE `user_id` = ?".
+ " AND `identity_id` = ?",
$this->ID,
$iid);
$this->identities = array();
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->quote_identifier('standard')." = '0'".
- " WHERE user_id = ?".
- " AND identity_id <> ?".
- " AND del <> 1",
+ "UPDATE ".$this->db->table_name('identities', true).
+ " SET `standard` = '0'".
+ " WHERE `user_id` = ? AND `identity_id` <> ?",
$this->ID,
$iid);
unset($this->identities[0]);
}
}
/**
* 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 = ?",
+ "UPDATE ".$this->db->table_name('users', true).
+ " 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();
$config = rcube::get_instance()->config;
// query for matching user name
- $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users')
- ." WHERE mail_host = ? AND username = ?", $host, $user);
+ $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users', true)
+ ." 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);
+ ." FROM " . $dbh->table_name('users', true) . " u"
+ ." JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)"
+ ." WHERE `email` = ? AND `del` <> 1", 0, 1, $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();
$dbh = $rcube->get_dbh();
// 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(
'host' => $host,
'user' => $user,
'user_name' => $user_name,
'user_email' => $user_email,
'email_list' => $email_list,
'language' => $_SESSION['language'],
));
// plugin aborted this operation
if ($data['abort']) {
return false;
}
$dbh->query(
- "INSERT INTO ".$dbh->table_name('users').
- " (created, last_login, username, mail_host, language)".
+ "INSERT INTO ".$dbh->table_name('users', true).
+ " (`created`, `last_login`, `username`, `mail_host`, `language`)".
" VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)",
$data['user'],
$data['host'],
$data['language']);
if ($user_id = $dbh->insert_id('users')) {
// create rcube_user instance to make plugin hooks work
$user_instance = new rcube_user($user_id, array(
'user_id' => $user_id,
'username' => $data['user'],
'mail_host' => $data['host'],
'language' => $data['language'],
));
$rcube->user = $user_instance;
$mail_domain = $rcube->config->mail_domain($data['host']);
$user_name = $data['user_name'];
$user_email = $data['user_email'];
$email_list = $data['email_list'];
if (empty($email_list)) {
if (empty($user_email)) {
$user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain);
}
$email_list[] = $user_email;
}
// identities_level check
else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) {
$email_list = array($email_list[0]);
}
if (empty($user_name)) {
$user_name = $data['user'];
}
// create new identities records
$standard = 1;
foreach ($email_list as $row) {
$record = array();
if (is_array($row)) {
if (empty($row['email'])) {
continue;
}
$record = $row;
}
else {
$record['email'] = $row;
}
if (empty($record['name'])) {
$record['name'] = $user_name != $record['email'] ? $user_name : '';
}
$record['user_id'] = $user_id;
$record['standard'] = $standard;
$plugin = $rcube->plugins->exec_hook('identity_create',
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->quote_identifier('name')
- ." FROM ".$this->db->table_name('searches')
- ." WHERE user_id = ?"
- ." AND ".$this->db->quote_identifier('type')." = ?"
- ." ORDER BY ".$this->db->quote_identifier('name'),
+ "SELECT `search_id` AS id, `name`"
+ ." FROM ".$this->db->table_name('searches', true)
+ ." WHERE `user_id` = ? AND `type` = ?"
+ ." ORDER BY `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->quote_identifier('name')
- .", ".$this->db->quote_identifier('data')
- .", ".$this->db->quote_identifier('type')
- ." FROM ".$this->db->table_name('searches')
- ." WHERE user_id = ?"
- ." AND search_id = ?",
+ "SELECT `name`, `data`, `type`"
+ . " FROM ".$this->db->table_name('searches', true)
+ . " 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 = ?",
+ "DELETE FROM ".$this->db->table_name('searches', true)
+ ." 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->quote_identifier('type');
$insert_values[] = (int) $data['type'];
$insert_cols[] = $this->db->quote_identifier('name');
$insert_values[] = $data['name'];
$insert_cols[] = $this->db->quote_identifier('data');
$insert_values[] = serialize($data['data']);
- $sql = "INSERT INTO ".$this->db->table_name('searches')
+ $sql = "INSERT INTO ".$this->db->table_name('searches', true)
." (".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 00999ba..a51247e 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -1,1085 +1,1105 @@
<?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 Framework
* @subpackage Utils
*/
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 = $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;
}
// Validate domain part
if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) {
return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 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)) {
+ if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
return false;
}
}
// last domain part
- if (preg_match('/[^a-zA-Z]/', array_pop($domain_array))) {
+ $last_part = array_pop($domain_array);
+ if (strpos($last_part, 'xn--') !== 0 && preg_match('/[^a-zA-Z]/', $last_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 (!function_exists('getmxrr') || getmxrr($domain_part, $mx_records)) {
return true;
}
// find any DNS record
if (!function_exists('checkdnsrr') || checkdnsrr($domain_part, 'ANY')) {
return true;
}
}
return false;
}
/**
* Validates IPv4 or IPv6 address
*
* @param string $ip IP address in v4 or v6 format
*
* @return bool True if the address is valid
*/
public static function check_ip($ip)
{
// IPv6, but there's no build-in IPv6 support
if (strpos($ip, ':') !== false && !defined('AF_INET6')) {
$parts = explode(':', $ip);
$count = count($parts);
if ($count > 8 || $count < 2) {
return false;
}
foreach ($parts as $idx => $part) {
$length = strlen($part);
if (!$length) {
// there can be only one ::
if ($found_empty) {
return false;
}
$found_empty = true;
}
// last part can be an IPv4 address
else if ($idx == $count - 1) {
if (!preg_match('/^[0-9a-f]{1,4}$/i', $part)) {
return @inet_pton($part) !== false;
}
}
else if (!preg_match('/^[0-9a-f]{1,4}$/i', $part)) {
return false;
}
}
return true;
}
return @inet_pton($ip) !== 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(self::request_header('Referer'));
return $referer['host'] == self::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;
if (!is_string($str)) {
$str = strval($str);
}
// 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);
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 slashes if magic_quotes enabled
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
* @param boolean $allow_html Allow HTML tags in field value
*
* @return array Hash array with all request parameters
*/
public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false)
{
$out = array();
$src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
foreach (array_keys($src) as $key) {
$fname = $key[0] == '_' ? substr($key, 1) : $key;
if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
$out[$fname] = self::get_input_value($key, $mode, $allow_html);
}
}
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, '_');
}
}
/**
* 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! */';
}
$strict_url_regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
// cut out all contents between { and }
while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
$nested = strpos($source, '{', $pos+1);
if ($nested && $nested < $pos2) // when dealing with nested blocks (e.g. @media), take the inner one
$pos = $nested;
$length = $pos2 - $pos - 1;
$styles = substr($source, $pos+1, $length);
// 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
if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) {
$a_styles = array('/* evil! */');
break;
}
}
$styles = join(";\n", $a_styles);
}
$key = $replacements->add($styles);
$repl = $replacements->get_replacement($key);
$source = substr_replace($source, $repl, $pos+1, $length);
$last_pos = $pos2 - ($length - strlen($repl));
}
// remove html comments and add #container to each tag selector.
// also replace body definition because we also stripped off the <body> tag
$source = preg_replace(
array(
'/(^\s*<\!--)|(-->\s*$)/m',
'/(^\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
$source = $replacements->resolve($source);
return $source;
}
/**
* 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)
{
$mimetype = strtolower($mimetype);
$filename = strtolower($filename);
list($primary, $secondary) = explode('/', $mimetype);
$classes = array($primary ? $primary : 'unknown');
if ($secondary) {
$classes[] = $secondary;
}
if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) {
if (!in_array($m[1], $classes)) {
$classes[] = $m[1];
}
}
return 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)
{
if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'
&& in_array($_SERVER['REMOTE_ADDR'], rcube::get_instance()->config->get('proxy_whitelist', array()))) {
return true;
}
if ($port && $_SERVER['SERVER_PORT'] == $port) {
return true;
}
if ($use_https && rcube::get_instance()->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 = '')
{
if (!is_string($name)) {
return $name;
}
// %n - host
$n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
// %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
$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 = self::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;
}
}
return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $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;
}
/**
* Returns the real remote IP address
*
* @return string Remote IP address
*/
public static function remote_addr()
{
// Check if any of the headers are set first to improve performance
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) {
$proxy_whitelist = rcube::get_instance()->config->get('proxy_whitelist', array());
if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) {
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
foreach(array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) {
if (!in_array($forwarded_ip, $proxy_whitelist)) {
return $forwarded_ip;
}
}
}
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
return $_SERVER['HTTP_X_REAL_IP'];
}
}
}
if (!empty($_SERVER['REMOTE_ADDR'])) {
return $_SERVER['REMOTE_ADDR'];
}
return '';
}
/**
* 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[] = (string) substr($string, $p);
return $result;
}
/**
* Improved equivalent to strtotime()
*
* @param string $date Date string
*
* @return int Unix timestamp
*/
public static function strtotime($date)
{
$date = self::clean_datestr($date);
// unix timestamp
if (is_numeric($date)) {
return (int) $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 (int) $ts;
}
/**
* Date parsing function that turns the given value into a DateTime object
*
* @param string $date Date string
*
* @return object DateTime instance or false on failure
*/
- public static function anytodatetime($date)
+ public static function anytodatetime($date, $timezone = null)
{
if (is_object($date) && is_a($date, 'DateTime')) {
return $date;
}
$dt = false;
$date = self::clean_datestr($date);
// try to parse string with DateTime first
if (!empty($date)) {
try {
- $dt = new DateTime($date);
+ $dt = new DateTime($date, $timezone);
}
catch (Exception $e) {
// ignore
}
}
// try our advanced strtotime() method
if (!$dt && ($timestamp = self::strtotime($date))) {
try {
$dt = new DateTime("@".$timestamp);
}
catch (Exception $e) {
// ignore
}
}
return $dt;
}
/**
* Clean up date string for strtotime() input
*
* @param string $date Date string
*
* @return string Date string
*/
public static function clean_datestr($date)
{
$date = trim($date);
// check for MS Outlook vCard date format YYYYMMDD
if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3]));
}
// Clean malformed data
$date = preg_replace(
array(
'/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
'/[^a-z0-9\x20\x09:+-\/]/i', // remove any invalid characters
'/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
),
array(
'\\1',
'',
'',
), $date);
$date = trim($date);
// try to fix dd/mm vs. mm/dd discrepancy, we can't do more here
if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)) {
$mdy = $m[2] > 12 && $m[1] <= 12;
$day = $mdy ? $m[2] : $m[1];
$month = $mdy ? $m[1] : $m[2];
$date = sprintf('%04d-%02d-%02d 00:00:00', intval($m[3]), $month, $day);
}
// I've found that YYYY.MM.DD is recognized wrong, so here's a fix
else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/', $date)) {
$date = str_replace('.', '-', $date) . ' 00:00:00';
}
return $date;
}
/*
* Idn_to_ascii wrapper.
* Intl/Idn modules version of this function doesn't work with e-mail address
*/
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 ISO-8859-1 and ISO-8859-2 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)
{
// replace 4-byte unicode characters with '?' character,
// these are not supported in default utf-8 charset on mysql,
// the chance we'd need them in searching is very low
$str = preg_replace('/('
. '\xF0[\x90-\xBF][\x80-\xBF]{2}'
. '|[\xF1-\xF3][\x80-\xBF]{3}'
. '|\xF4[\x80-\x8F][\x80-\xBF]{2}'
. ')/', '?', $str);
// split by words
$arr = self::tokenize_string($str);
// detect character set
if (utf8_encode(utf8_decode($str)) == $str) {
// ISO-8859-1 (or ASCII)
preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys);
preg_match_all('/./', 'aaaaaaaceeeeiiiinoooooouuuuyy', $values);
$mapping = array_combine($keys[0], $values[0]);
$mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
}
else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) {
// ISO-8859-2
preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys);
preg_match_all('/./', 'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values);
$mapping = array_combine($keys[0], $values[0]);
$mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
}
foreach ($arr as $i => $part) {
$part = mb_strtolower($part);
if (!empty($mapping)) {
$part = strtr($part, $mapping);
}
$arr[$i] = $part;
}
return $as_array ? $arr : join(" ", $arr);
}
/**
* Parse commandline arguments into a hash array
*
* @param array $aliases Argument alias names
*
* @return array Argument values hash
*/
public static function get_opt($aliases = array())
{
$args = array();
for ($i=1; $i < count($_SERVER['argv']); $i++) {
$arg = $_SERVER['argv'][$i];
$value = true;
$key = null;
if ($arg[0] == '-') {
$key = preg_replace('/^-+/', '', $arg);
$sp = strpos($arg, '=');
if ($sp > 0) {
$key = substr($key, 0, $sp - 2);
$value = substr($arg, $sp+1);
}
else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') {
$value = $_SERVER['argv'][++$i];
}
$args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value;
}
else {
$args[] = $arg;
}
if ($alias = $aliases[$key]) {
$args[$alias] = $args[$key];
}
}
return $args;
}
/**
* Safe password prompt for command line
* from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/
*
* @return string Password
*/
public static function prompt_silent($prompt = "Password:")
{
if (preg_match('/^win/i', PHP_OS)) {
$vbscript = sys_get_temp_dir() . 'prompt_password.vbs';
$vbcontent = 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))';
file_put_contents($vbscript, $vbcontent);
$command = "cscript //nologo " . escapeshellarg($vbscript);
$password = rtrim(shell_exec($command));
unlink($vbscript);
return $password;
}
else {
$command = "/usr/bin/env bash -c 'echo OK'";
if (rtrim(shell_exec($command)) !== 'OK') {
echo $prompt;
$pass = trim(fgets(STDIN));
echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n";
return $pass;
}
$command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'";
$password = rtrim(shell_exec($command));
echo "\n";
return $password;
}
}
/**
* Find out if the string content means true or false
*
* @param string $str Input value
*
* @return boolean Boolean value
*/
public static function get_boolean($str)
{
$str = strtolower($str);
return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true);
}
/**
* OS-dependent absolute path detection
*/
public static function is_absolute_path($path)
{
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
}
else {
- return $path[0] == DIRECTORY_SEPARATOR;
+ return $path[0] == '/';
+ }
+ }
+
+ /**
+ * Resolve relative URL
+ *
+ * @param string $url Relative URL
+ *
+ * @return string Absolute URL
+ */
+ public static function resolve_url($url)
+ {
+ // prepend protocol://hostname:port
+ if (!preg_match('|^https?://|', $url)) {
+ $schema = 'http';
+ $default_port = 80;
+
+ if (self::https_check()) {
+ $schema = 'https';
+ $default_port = 443;
+ }
+
+ $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
+ if ($_SERVER['SERVER_PORT'] != $default_port) {
+ $prefix .= ':' . $_SERVER['SERVER_PORT'];
+ }
+
+ $url = $prefix . ($url[0] == '/' ? '' : '/') . $url;
}
+
+ return $url;
}
}
diff --git a/lib/ext/Roundcube/rcube_washtml.php b/lib/ext/Roundcube/rcube_washtml.php
index 9842943..97ab56c 100644
--- a/lib/ext/Roundcube/rcube_washtml.php
+++ b/lib/ext/Roundcube/rcube_washtml.php
@@ -1,620 +1,624 @@
<?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: |
| Utility class providing HTML sanityzer (based on Washtml class) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Frederic Motte <fmotte@ubixis.com> |
+-----------------------------------------------------------------------+
*/
/**
* Washtml, a HTML sanityzer.
*
* Copyright (c) 2007 Frederic Motte <fmotte@ubixis.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* OVERVIEW:
*
* Wahstml take an untrusted HTML and return a safe html string.
*
* SYNOPSIS:
*
* $washer = new washtml($config);
* $washer->wash($html);
* It return a sanityzed string of the $html parameter without html and head tags.
* $html is a string containing the html code to wash.
* $config is an array containing options:
* $config['allow_remote'] is a boolean to allow link to remote images.
* $config['blocked_src'] string with image-src to be used for blocked remote images
* $config['show_washed'] is a boolean to include washed out attributes as x-washed
* $config['cid_map'] is an array where cid urls index urls to replace them.
* $config['charset'] is a string containing the charset of the HTML document if it is not defined in it.
* $washer->extlinks is a reference to a boolean that is set to true if remote images were removed. (FE: show remote images link)
*
* INTERNALS:
*
* Only tags and attributes in the static lists $html_elements and $html_attributes
* are kept, inline styles are also filtered: all style identifiers matching
* /[a-z\-]/i are allowed. Values matching colors, sizes, /[a-z\-]/i and safe
* urls if allowed and cid urls if mapped are kept.
*
* Roundcube Changes:
* - added $block_elements
* - changed $ignore_elements behaviour
* - added RFC2397 support
* - base URL support
* - invalid HTML comments removal before parsing
* - "fixing" unitless CSS values for XHTML output
* - base url resolving
*/
/**
* Utility class providing HTML sanityzer
*
* @package Framework
* @subpackage Utils
*/
class rcube_washtml
{
/* Allowed HTML elements (default) */
static $html_elements = array('a', 'abbr', 'acronym', 'address', 'area', 'b',
'basefont', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center',
'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl',
'dt', 'em', 'fieldset', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i',
'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q',
's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img',
+ 'video', 'source',
// form elements
'button', 'input', 'textarea', 'select', 'option', 'optgroup'
);
/* Ignore these HTML tags and their content */
static $ignore_elements = array('script', 'applet', 'embed', 'object', 'style');
/* Allowed HTML attributes */
static $html_attribs = array('name', 'class', 'title', 'alt', 'width', 'height',
'align', 'nowrap', 'col', 'row', 'id', 'rowspan', 'colspan', 'cellspacing',
'cellpadding', 'valign', 'bgcolor', 'color', 'border', 'bordercolorlight',
'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border',
'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace',
'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media',
// attributes of form elements
'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value'
);
/* Elements which could be empty and be returned in short form (<tag />) */
static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr',
'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
);
/* State for linked objects in HTML */
public $extlinks = false;
/* Current settings */
private $config = array();
/* Registered callback functions for tags */
private $handlers = array();
/* Allowed HTML elements */
private $_html_elements = array();
/* Ignore these HTML tags but process their content */
private $_ignore_elements = array();
/* Elements which could be empty and be returned in short form (<tag />) */
private $_void_elements = array();
/* Allowed HTML attributes */
private $_html_attribs = array();
/* Max nesting level */
private $max_nesting_level;
/**
* Class constructor
*/
public function __construct($p = array())
{
$this->_html_elements = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements) ;
$this->_html_attribs = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs);
$this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements);
$this->_void_elements = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements);
unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']);
$this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array());
}
/**
* Register a callback function for a certain tag
*/
public function add_callback($tagName, $callback)
{
$this->handlers[$tagName] = $callback;
}
/**
* Check CSS style
*/
private function wash_style($style)
{
$result = array();
foreach (explode(';', $style) as $declaration) {
if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
$cssid = $match[1];
$str = $match[2];
$value = '';
foreach ($this->explode_style($str) as $val) {
if (preg_match('/^url\(/i', $val)) {
if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
$url = $match[1];
if (($src = $this->config['cid_map'][$url])
|| ($src = $this->config['cid_map'][$this->config['base_url'].$url])
) {
$value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
}
else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $url, $m)) {
if ($this->config['allow_remote']) {
$value .= ' url('.htmlspecialchars($m[0], ENT_QUOTES).')';
}
else {
$this->extlinks = true;
}
}
else if (preg_match('/^data:.+/i', $url)) { // RFC2397
$value .= ' url('.htmlspecialchars($url, ENT_QUOTES).')';
}
}
}
else if (!preg_match('/^(behavior|expression)/i', $val)) {
// whitelist ?
$value .= ' ' . $val;
// #1488535: Fix size units, so width:800 would be changed to width:800px
if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
&& preg_match('/^[0-9]+$/', $val)
) {
$value .= 'px';
}
}
}
if (isset($value[0])) {
$result[] = $cssid . ':' . $value;
}
}
}
return implode('; ', $result);
}
/**
* Take a node and return allowed attributes and check values
*/
private function wash_attribs($node)
{
$t = '';
$washed = '';
foreach ($node->attributes as $key => $plop) {
$key = strtolower($key);
$value = $node->getAttribute($key);
if (isset($this->_html_attribs[$key]) ||
($key == 'href' && ($value = trim($value))
&& !preg_match('!^(javascript|vbscript|data:text)!i', $value)
&& preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value))
) {
$t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
}
else if ($key == 'style' && ($style = $this->wash_style($value))) {
$quot = strpos($style, '"') !== false ? "'" : '"';
$t .= ' style=' . $quot . $style . $quot;
}
- else if ($key == 'background' || ($key == 'src' && strtolower($node->tagName) == 'img')) { //check tagName anyway
+ else if ($key == 'background'
+ || ($key == 'src' && preg_match('/^(img|source)$/i', $node->tagName))
+ || ($key == 'poster' && strtolower($node->tagName) == 'video')
+ ) {
if (($src = $this->config['cid_map'][$value])
|| ($src = $this->config['cid_map'][$this->config['base_url'].$value])
) {
$t .= ' ' . $key . '="' . htmlspecialchars($src, ENT_QUOTES) . '"';
}
else if (preg_match('/^(http|https|ftp):.+/i', $value)) {
if ($this->config['allow_remote']) {
$t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
}
else {
$this->extlinks = true;
if ($this->config['blocked_src']) {
$t .= ' ' . $key . '="' . htmlspecialchars($this->config['blocked_src'], ENT_QUOTES) . '"';
}
}
}
else if (preg_match('/^data:.+/i', $value)) { // RFC2397
$t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
}
}
else {
$washed .= ($washed ? ' ' : '') . $key;
}
}
return $t . ($washed && $this->config['show_washed'] ? ' x-washed="'.$washed.'"' : '');
}
/**
* The main loop that recurse on a node tree.
* It output only allowed tags with allowed attributes and allowed inline styles
*
* @param DOMNode $node HTML element
* @param int $level Recurrence level (safe initial value found empirically)
*/
private function dumpHtml($node, $level = 20)
{
if (!$node->hasChildNodes()) {
return '';
}
$level++;
if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) {
// log error message once
if (!$this->max_nesting_level_error) {
$this->max_nesting_level_error = true;
rcube::raise_error(array('code' => 500, 'type' => 'php',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})"),
true, false);
}
return '<!-- ignored -->';
}
$node = $node->firstChild;
$dump = '';
do {
switch($node->nodeType) {
case XML_ELEMENT_NODE: //Check element
$tagName = strtolower($node->tagName);
if ($callback = $this->handlers[$tagName]) {
$dump .= call_user_func($callback, $tagName,
$this->wash_attribs($node), $this->dumpHtml($node, $level), $this);
}
else if (isset($this->_html_elements[$tagName])) {
$content = $this->dumpHtml($node, $level);
$dump .= '<' . $tagName . $this->wash_attribs($node) .
($content === '' && isset($this->_void_elements[$tagName]) ? ' />' : ">$content</$tagName>");
}
else if (isset($this->_ignore_elements[$tagName])) {
$dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' not allowed -->';
}
else {
$dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' ignored -->';
$dump .= $this->dumpHtml($node, $level); // ignore tags not its content
}
break;
case XML_CDATA_SECTION_NODE:
$dump .= $node->nodeValue;
break;
case XML_TEXT_NODE:
$dump .= htmlspecialchars($node->nodeValue);
break;
case XML_HTML_DOCUMENT_NODE:
$dump .= $this->dumpHtml($node, $level);
break;
case XML_DOCUMENT_TYPE_NODE:
break;
default:
$dump .= '<!-- node type ' . $node->nodeType . ' -->';
}
} while($node = $node->nextSibling);
return $dump;
}
/**
* Main function, give it untrusted HTML, tell it if you allow loading
* remote images and give it a map to convert "cid:" urls.
*/
public function wash($html)
{
// Charset seems to be ignored (probably if defined in the HTML document)
$node = new DOMDocument('1.0', $this->config['charset']);
$this->extlinks = false;
$html = $this->cleanup($html);
// Find base URL for images
if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) {
$this->config['base_url'] = $matches[1];
}
else {
$this->config['base_url'] = '';
}
// Detect max nesting level (for dumpHTML) (#1489110)
$this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
// Use optimizations if supported
- if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+ if (PHP_VERSION_ID >= 50400) {
@$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
}
else {
@$node->loadHTML($html);
}
return $this->dumpHtml($node);
}
/**
* Getter for config parameters
*/
public function get_config($prop)
{
return $this->config[$prop];
}
/**
* Clean HTML input
*/
private function cleanup($html)
{
// special replacements (not properly handled by washtml class)
$html_search = array(
'/(<\/nobr>)(\s+)(<nobr>)/i', // space(s) between <NOBR>
'/<title[^>]*>[^<]*<\/title>/i', // PHP bug #32547 workaround: remove title tag
'/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/', // byte-order mark (only outlook?)
'/<html\s[^>]+>/i', // washtml/DOMDocument cannot handle xml namespaces
);
$html_replace = array(
'\\1'.' &nbsp; '.'\\3',
'',
'',
'<html>',
);
$html = preg_replace($html_search, $html_replace, trim($html));
//-> Replace all of those weird MS Word quotes and other high characters
$badwordchars = array(
"\xe2\x80\x98", // left single quote
"\xe2\x80\x99", // right single quote
"\xe2\x80\x9c", // left double quote
"\xe2\x80\x9d", // right double quote
"\xe2\x80\x94", // em dash
"\xe2\x80\xa6" // elipses
);
$fixedwordchars = array(
"'",
"'",
'"',
'"',
'&mdash;',
'...'
);
$html = str_replace($badwordchars, $fixedwordchars, $html);
// PCRE errors handling (#1486856), should we use something like for every preg_* use?
if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
$errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
$errstr .= " Consider raising pcre.backtrack_limit!";
}
if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
$errstr .= " Consider raising pcre.recursion_limit!";
}
rcube::raise_error(array('code' => 620, 'type' => 'php',
'line' => __LINE__, 'file' => __FILE__,
'message' => $errstr), true, false);
return '';
}
// fix (unknown/malformed) HTML tags before "wash"
$html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
// Remove invalid HTML comments (#1487759)
// Don't remove valid conditional comments
// Don't remove MSOutlook (<!-->) conditional comments (#1489004)
$html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html);
// fix broken nested lists
self::fix_broken_lists($html);
// turn relative into absolute urls
$html = self::resolve_base($html);
return $html;
}
/**
* Callback function for HTML tags fixing
*/
public static function html_tag_callback($matches)
{
$tagname = $matches[2];
$tagname = preg_replace(array(
'/:.*$/', // Microsoft's Smart Tags <st1:xxxx>
'/[^a-z0-9_\[\]\!-]/i', // forbidden characters
), '', $tagname);
// fix invalid closing tags - remove any attributes (#1489446)
if ($matches[1] == '</') {
$matches[3] = '';
}
return $matches[1] . $tagname . $matches[3];
}
/**
* Convert all relative URLs according to a <base> in HTML
*/
public static function resolve_base($body)
{
// check for <base href=...>
if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
$replacer = new rcube_base_replacer($regs[2]);
$body = $replacer->replace($body);
}
return $body;
}
/**
* Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
*/
public static function fix_broken_lists(&$html)
{
// do two rounds, one for <ol>, one for <ul>
foreach (array('ol', 'ul') as $tag) {
$pos = 0;
while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
$pos++;
// make sure this is an ol/ul tag
if (!in_array($html[$pos+2], array(' ', '>'))) {
continue;
}
$p = $pos;
$in_li = false;
$li_pos = 0;
while (($p = strpos($html, '<', $p)) !== false) {
$tt = strtolower(substr($html, $p, 4));
// li open tag
if ($tt == '<li>' || $tt == '<li ') {
$in_li = true;
$p += 4;
}
// li close tag
else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
$li_pos = $p;
$p += 4;
$in_li = false;
}
// ul/ol closing tag
else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
break;
}
// nested ol/ul element out of li
else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
// find closing tag of this ul/ol element
$element = substr($tt, 1, 2);
$cpos = $p;
do {
$tpos = stripos($html, '<' . $element, $cpos+1);
$cpos = stripos($html, '</' . $element, $cpos+1);
}
while ($tpos !== false && $cpos !== false && $cpos > $tpos);
// not found, this is invalid HTML, skip it
if ($cpos === false) {
break;
}
// get element content
$end = strpos($html, '>', $cpos);
$len = $end - $p + 1;
$element = substr($html, $p, $len);
// move element to the end of the last li
$html = substr_replace($html, '', $p, $len);
$html = substr_replace($html, $element, $li_pos, 0);
$p = $end;
}
else {
$p++;
}
}
}
}
}
/**
* Explode css style value
*/
protected function explode_style($style)
{
$style = trim($style);
// first remove comments
$pos = 0;
while (($pos = strpos($style, '/*', $pos)) !== false) {
$end = strpos($style, '*/', $pos+2);
if ($end === false) {
$style = substr($style, 0, $pos);
}
else {
$style = substr_replace($style, '', $pos, $end - $pos + 2);
}
}
$strlen = strlen($style);
$result = array();
// explode value
for ($p=$i=0; $i < $strlen; $i++) {
if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
if ($q == $style[$i]) {
$q = false;
}
else if (!$q) {
$q = $style[$i];
}
}
if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
$result[] = substr($style, $p, $i - $p);
$p = $i + 1;
}
}
$result[] = (string) substr($style, $p);
return $result;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Feb 3, 4:09 PM (12 h, 53 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
2c/0a/bfa03cbf67e8de33bef642c45cb8
Default Alt Text
(1 MB)

Event Timeline