Import Ruty
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
Roundcube Framework
|
||||
===================
|
||||
|
||||
INTRODUCTION
|
||||
------------
|
||||
The Roundcube Framework is the basic library used for the Roundcube Webmail
|
||||
application. It is an extract of classes providing the core functionality for
|
||||
an email system. They can be used individually or as package for the following
|
||||
tasks:
|
||||
|
||||
- IMAP mailbox access with optional caching
|
||||
- MIME message handling
|
||||
- Email message creation and sending through SMTP
|
||||
- General caching utilities using the local database
|
||||
- Database abstraction using PDO
|
||||
- VCard parsing and writing
|
||||
|
||||
|
||||
REQUIREMENTS
|
||||
------------
|
||||
PHP Version 7.3 or greater including:
|
||||
- PCRE, DOM, JSON, Session, Sockets, OpenSSL, Mbstring, Filter, Ctype, Intl (required)
|
||||
- PHP PDO with driver for either MySQL, PostgreSQL, SQL Server, Oracle or SQLite (required)
|
||||
- Iconv, Zip, Fileinfo, Exif (recommended)
|
||||
- LDAP for LDAP addressbook support (optional)
|
||||
|
||||
|
||||
INSTALLATION
|
||||
------------
|
||||
Copy all files of this directory to your project or install it in the default
|
||||
include_path directory of your webserver. Some classes of the framework require
|
||||
external libraries. See composer.json-dist for the list of required packages.
|
||||
|
||||
|
||||
USAGE
|
||||
-----
|
||||
The Roundcube Framework provides a bootstrapping file which registers an
|
||||
autoloader and sets up the environment necessary for the Roundcube classes.
|
||||
In order to make use of the framework, simply include the bootstrap.php file
|
||||
from this directory in your application and start using the classes by simply
|
||||
instantiating them.
|
||||
|
||||
If you wanna use more complex functionality like IMAP access with database
|
||||
caching or plugins, the rcube singleton helps you loading the necessary files:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
define('RCUBE_CONFIG_DIR', '<path-to-config-directory>');
|
||||
define('RCUBE_PLUGINS_DIR', '<path-to-roundcube-plugins-directory');
|
||||
|
||||
require_once '<path-to-roundcube-framework/bootstrap.php';
|
||||
|
||||
$rcube = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
|
||||
$imap = $rcube->get_storage();
|
||||
|
||||
// do cool stuff here...
|
||||
|
||||
?>
|
||||
```
|
||||
|
||||
LICENSE
|
||||
-------
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License (**with exceptions
|
||||
for plugins**) as published by the Free Software Foundation, either
|
||||
version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see [www.gnu.org/licenses/][gpl].
|
||||
|
||||
This file forms part of the Roundcube Webmail Framework for which the
|
||||
following exception is added: Plugins which merely make function calls to the
|
||||
Roundcube Webmail Framework, and for that purpose include it by reference
|
||||
shall not be considered modifications of the software.
|
||||
|
||||
If you wish to use this file in another project or create a modified
|
||||
version that will not be part of the Roundcube Webmail Framework, you
|
||||
may remove the exception above and use this source code under the
|
||||
original version of the license.
|
||||
|
||||
For more details about licensing and the exceptions for skins and plugins
|
||||
see [roundcube.net/license][license]
|
||||
|
||||
|
||||
CONTACT
|
||||
-------
|
||||
For bug reports or feature requests please refer to the tracking system
|
||||
at [Github][githubissues] or subscribe to our mailing list.
|
||||
See [roundcube.net/support][support] for details.
|
||||
|
||||
You're always welcome to send a message to the project admins:
|
||||
hello(at)roundcube(dot)net
|
||||
|
||||
|
||||
[pear]: http://pear.php.net
|
||||
[gpl]: http://www.gnu.org/licenses/
|
||||
[license]: http://roundcube.net/license
|
||||
[support]: http://roundcube.net/support
|
||||
[githubissues]: https://github.com/roundcube/roundcubemail/issues
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 = [
|
||||
'error_reporting' => E_ALL & ~E_NOTICE & ~E_STRICT,
|
||||
'display_errors' => false,
|
||||
'log_errors' => true,
|
||||
// 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,
|
||||
];
|
||||
|
||||
// check these additional ini settings if not called via CLI
|
||||
if (php_sapi_name() != 'cli') {
|
||||
$config += [
|
||||
'suhosin.session.encrypt' => false,
|
||||
'file_uploads' => true,
|
||||
'session.auto_start' => false,
|
||||
'zlib.output_compression' => false,
|
||||
];
|
||||
}
|
||||
|
||||
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) {
|
||||
$optval = !is_bool($optval) ? $optval : ($optval ? 'On' : 'Off');
|
||||
$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.6.3');
|
||||
define('RCUBE_CHARSET', 'UTF-8');
|
||||
define('RCUBE_TEMP_FILE_PREFIX', 'RCMTEMP');
|
||||
|
||||
if (!defined('RCUBE_LIB_DIR')) {
|
||||
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
|
||||
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)
|
||||
if (class_exists('PEAR')) {
|
||||
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, function($err) { rcube::raise_error($err, true); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar function as in_array() but case-insensitive with multibyte support.
|
||||
*
|
||||
* @param string $needle Needle value
|
||||
* @param array $heystack Array to search in
|
||||
*
|
||||
* @return bool True if found, False if not
|
||||
*/
|
||||
function in_array_nocase($needle, $haystack)
|
||||
{
|
||||
if (!is_string($needle) || !is_array($haystack)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// use much faster method for ascii
|
||||
if (is_ascii($needle)) {
|
||||
foreach ((array) $haystack as $value) {
|
||||
if (is_string($value) && strcasecmp($value, $needle) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$needle = mb_strtolower($needle);
|
||||
foreach ((array) $haystack as $value) {
|
||||
if (is_string($value) && $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);
|
||||
}
|
||||
|
||||
$bytes = 0;
|
||||
|
||||
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
|
||||
*
|
||||
* @param string $str A string
|
||||
*
|
||||
* @return string A string ending with a slash
|
||||
*/
|
||||
function slashify($str)
|
||||
{
|
||||
return unslashify($str) . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove slashes at the end of the string
|
||||
*
|
||||
* @param string $str A string
|
||||
*
|
||||
* @return string A string ending with no slash
|
||||
*/
|
||||
function unslashify($str)
|
||||
{
|
||||
return rtrim($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;
|
||||
|
||||
$prefix = mb_substr($str, 0, $first_part_length);
|
||||
$suffix = mb_substr($str, $second_starting_location);
|
||||
$str = $prefix . $placeholder . $suffix;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
if (!empty($array) && is_array($array)) {
|
||||
foreach ($array as $key => $child) {
|
||||
$keys[] = $key;
|
||||
foreach (array_keys_recursive($child) as $val) {
|
||||
$keys[] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first element from an array
|
||||
*
|
||||
* @param array $array Input array
|
||||
*
|
||||
* @return mixed First element if found, Null otherwise
|
||||
*/
|
||||
function array_first($array)
|
||||
{
|
||||
if (is_array($array)) {
|
||||
reset($array);
|
||||
foreach ($array as $element) {
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all non-ascii and non-word chars except ., -, _
|
||||
*
|
||||
* @param string $str A string
|
||||
* @param bool $css_id The result may be used as CSS identifier
|
||||
* @param string $replace_with Replacement character
|
||||
*
|
||||
* @return string Clean string
|
||||
*/
|
||||
function asciiwords($str, $css_id = false, $replace_with = '')
|
||||
{
|
||||
$allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
|
||||
return preg_replace("/[^$allowed]+/i", $replace_with, (string) $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string contains only ascii characters
|
||||
*
|
||||
* @param string $str String to check
|
||||
* @param bool $control_chars Includes control characters
|
||||
*
|
||||
* @return bool True if the string contains ASCII-only, False otherwise
|
||||
*/
|
||||
function is_ascii($str, $control_chars = true)
|
||||
{
|
||||
$regexp = $control_chars ? '/[^\x00-\x7F]/' : '/[^\x20-\x7E]/';
|
||||
return preg_match($regexp, (string) $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(
|
||||
['-stable', '-git'],
|
||||
['.0', '.99'],
|
||||
$version
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use PHP5 autoload for dynamic class loading
|
||||
*
|
||||
* @param string $classname Class name
|
||||
*
|
||||
* @return bool True when the class file has been found
|
||||
*
|
||||
* @todo Make Zend, PEAR etc play with this
|
||||
* @todo Make our classes conform to a more straight forward CS.
|
||||
*/
|
||||
function rcube_autoload($classname)
|
||||
{
|
||||
if (strpos($classname, 'rcube') === 0) {
|
||||
$classname = preg_replace('/^rcube_(cache|db|session|spellchecker)_/', '\\1/', $classname);
|
||||
$classname = 'Roundcube/' . $classname;
|
||||
}
|
||||
else if (strpos($classname, 'html_') === 0 || $classname === 'html') {
|
||||
$classname = 'Roundcube/html';
|
||||
}
|
||||
else if (strpos($classname, 'Mail_') === 0) {
|
||||
$classname = 'Mail/' . substr($classname, 5);
|
||||
}
|
||||
else if (strpos($classname, 'Net_') === 0) {
|
||||
$classname = 'Net/' . substr($classname, 4);
|
||||
}
|
||||
else if (strpos($classname, 'Auth_') === 0) {
|
||||
$classname = 'Auth/' . substr($classname, 5);
|
||||
}
|
||||
|
||||
// Translate PHP namespaces into directories,
|
||||
// i.e. use \Sabre\VObject; $vcf = VObject\Reader::read(...)
|
||||
// -> Sabre/VObject/Reader.php
|
||||
$classname = str_replace('\\', '/', $classname);
|
||||
|
||||
if ($fp = @fopen("$classname.php", 'r', true)) {
|
||||
fclose($fp);
|
||||
include_once "$classname.php";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 - APC |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface implementation class for accessing APC cache
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Cache
|
||||
*/
|
||||
class rcube_cache_apc extends rcube_cache
|
||||
{
|
||||
/**
|
||||
* Indicates if APC module is enabled and in a required version
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
parent::__construct($userid, $prefix, $ttl, $packed, $indexed);
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
$this->type = 'apc';
|
||||
$this->enabled = function_exists('apc_exists'); // APC 3.1.4 required
|
||||
$this->debug = $rcube->config->get('apc_debug');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache records older than ttl
|
||||
*/
|
||||
public function expunge()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired records of all caches
|
||||
*/
|
||||
public static function gc()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cache entry.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function get_item($key)
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = apc_fetch($key);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entry into memcache/apc/redis DB.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
* @param mixed $data Serialized cache data
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function add_item($key, $data)
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (apc_exists($key)) {
|
||||
apc_delete($key);
|
||||
}
|
||||
|
||||
$result = apc_store($key, $data, $this->ttl);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes entry from memcache/apc/redis DB.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function delete_item($key)
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = apc_delete($key);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 - SQL DB |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface implementation class for accessing SQL Database cache
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Cache
|
||||
*/
|
||||
class rcube_cache_db extends rcube_cache
|
||||
{
|
||||
/**
|
||||
* Instance of database handler
|
||||
*
|
||||
* @var rcube_db
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* (Escaped) Cache table name (cache or cache_shared)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
parent::__construct($userid, $prefix, $ttl, $packed, $indexed);
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
$this->type = 'db';
|
||||
$this->db = $rcube->get_dbh();
|
||||
$this->table = $this->db->table_name($userid ? 'cache' : 'cache_shared', true);
|
||||
|
||||
$this->refresh_time *= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache records older than ttl
|
||||
*/
|
||||
public function expunge()
|
||||
{
|
||||
if ($this->ttl) {
|
||||
$this->db->query(
|
||||
"DELETE FROM {$this->table} WHERE "
|
||||
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
|
||||
. "`cache_key` LIKE ?"
|
||||
. " AND `expires` < " . $this->db->now(),
|
||||
$this->prefix . '.%');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired records of all caches
|
||||
*/
|
||||
public static function gc()
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
$db = $rcube->get_dbh();
|
||||
|
||||
$db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
|
||||
$db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cache entry.
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function read_record($key)
|
||||
{
|
||||
$sql_result = $this->db->query(
|
||||
"SELECT `data`, `cache_key` FROM {$this->table} WHERE "
|
||||
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
|
||||
."`cache_key` = ?",
|
||||
$this->prefix . '.' . $key);
|
||||
|
||||
$data = null;
|
||||
|
||||
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
|
||||
if (strlen($sql_arr['data']) > 0) {
|
||||
$data = $this->unserialize($sql_arr['data']);
|
||||
}
|
||||
|
||||
$this->db->reset();
|
||||
}
|
||||
|
||||
if (!$this->indexed) {
|
||||
$this->cache[$key] = $data;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes single cache record into DB.
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
* @param mixed $data Serialized cache data
|
||||
* @param DateTime $ts Timestamp
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function store_record($key, $data, $ts = null)
|
||||
{
|
||||
$value = $this->serialize($data);
|
||||
$size = strlen($value);
|
||||
|
||||
// don't attempt to write too big data sets
|
||||
if ($size > $this->max_packet_size()) {
|
||||
trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write $size bytes", E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
$db_key = $this->prefix . '.' . $key;
|
||||
|
||||
// Remove NULL rows (here we don't need to check if the record exist)
|
||||
if ($value == 'N;') {
|
||||
$result = $this->db->query(
|
||||
"DELETE FROM {$this->table} WHERE "
|
||||
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
|
||||
."`cache_key` = ?",
|
||||
$db_key);
|
||||
|
||||
return !$this->db->is_error($result);
|
||||
}
|
||||
|
||||
$expires = $this->db->param($this->ttl ? $this->db->now($this->ttl) : 'NULL', rcube_db::TYPE_SQL);
|
||||
$pkey = ['cache_key' => $db_key];
|
||||
|
||||
if ($this->userid) {
|
||||
$pkey['user_id'] = $this->userid;
|
||||
}
|
||||
|
||||
$result = $this->db->insert_or_update(
|
||||
$this->table, $pkey, ['expires', 'data'], [$expires, $value]
|
||||
);
|
||||
|
||||
$count = $this->db->affected_rows($result);
|
||||
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the cache record(s).
|
||||
*
|
||||
* @param string $key Cache key name or pattern
|
||||
* @param bool $prefix_mode Enable it to clear all keys starting
|
||||
* with prefix specified in $key
|
||||
*/
|
||||
protected function remove_record($key = null, $prefix_mode = false)
|
||||
{
|
||||
// Remove all keys (in specified cache)
|
||||
if ($key === null) {
|
||||
$where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.%');
|
||||
$this->cache = [];
|
||||
}
|
||||
// Remove keys by name prefix
|
||||
else if ($prefix_mode) {
|
||||
$where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.' . $key . '%');
|
||||
foreach (array_keys($this->cache) as $k) {
|
||||
if (strpos($k, $key) === 0) {
|
||||
$this->cache[$k] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove one key by name
|
||||
else {
|
||||
$where = "`cache_key` = " . $this->db->quote($this->prefix . '.' . $key);
|
||||
$this->cache[$key] = null;
|
||||
}
|
||||
|
||||
$this->db->query(
|
||||
"DELETE FROM {$this->table} WHERE "
|
||||
. ($this->userid ? "`user_id` = {$this->userid} AND " : "") . $where
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes data for storing
|
||||
*/
|
||||
protected function serialize($data)
|
||||
{
|
||||
return $this->db->encode($data, $this->packed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserializes serialized data
|
||||
*/
|
||||
protected function unserialize($data)
|
||||
{
|
||||
return $this->db->decode($data, $this->packed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the maximum size for cache data to be written
|
||||
*/
|
||||
protected function max_packet_size()
|
||||
{
|
||||
if ($this->max_packet < 0) {
|
||||
$this->max_packet = 2097152; // default/max is 2 MB
|
||||
|
||||
if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) {
|
||||
$this->max_packet = $value;
|
||||
}
|
||||
|
||||
$this->max_packet -= 2000;
|
||||
}
|
||||
|
||||
return $this->max_packet;
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 - Memcache |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface implementation class for accessing Memcache cache
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Cache
|
||||
*/
|
||||
class rcube_cache_memcache extends rcube_cache
|
||||
{
|
||||
/**
|
||||
* Instance of memcache handler
|
||||
*
|
||||
* @var Memcache
|
||||
*/
|
||||
protected static $memcache;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
parent::__construct($userid, $prefix, $ttl, $packed, $indexed);
|
||||
|
||||
$this->type = 'memcache';
|
||||
$this->debug = rcube::get_instance()->config->get('memcache_debug');
|
||||
|
||||
self::engine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global handle for memcache access
|
||||
*
|
||||
* @return object Memcache
|
||||
*/
|
||||
public static function engine()
|
||||
{
|
||||
if (self::$memcache !== null) {
|
||||
return self::$memcache;
|
||||
}
|
||||
|
||||
// no memcache support in PHP
|
||||
if (!class_exists('Memcache')) {
|
||||
self::$memcache = false;
|
||||
|
||||
rcube::raise_error([
|
||||
'code' => 604,
|
||||
'type' => 'memcache',
|
||||
'line' => __LINE__,
|
||||
'file' => __FILE__,
|
||||
'message' => "Failed to find Memcache. Make sure php-memcache is included"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
// add all configured hosts to pool
|
||||
$rcube = rcube::get_instance();
|
||||
$pconnect = $rcube->config->get('memcache_pconnect', true);
|
||||
$timeout = $rcube->config->get('memcache_timeout', 1);
|
||||
$retry_interval = $rcube->config->get('memcache_retry_interval', 15);
|
||||
$seen = [];
|
||||
$available = 0;
|
||||
|
||||
// Callback for memcache failure
|
||||
$error_callback = function($host, $port) use ($seen, $available) {
|
||||
// only report once
|
||||
if (!$seen["$host:$port"]++) {
|
||||
$available--;
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'memcache',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Memcache failure on host $host:$port"
|
||||
],
|
||||
true, false);
|
||||
}
|
||||
};
|
||||
|
||||
self::$memcache = new Memcache;
|
||||
|
||||
foreach ((array) $rcube->config->get('memcache_hosts') as $host) {
|
||||
if (substr($host, 0, 7) != 'unix://') {
|
||||
list($host, $port) = explode(':', $host);
|
||||
if (!$port) $port = 11211;
|
||||
}
|
||||
else {
|
||||
$port = 0;
|
||||
}
|
||||
|
||||
$available += intval(self::$memcache->addServer(
|
||||
$host, $port, $pconnect, 1, $timeout, $retry_interval, false, $error_callback));
|
||||
}
|
||||
|
||||
// test connection and failover (will result in $available == 0 on complete failure)
|
||||
self::$memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist
|
||||
|
||||
if (!$available) {
|
||||
self::$memcache = false;
|
||||
}
|
||||
|
||||
return self::$memcache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache records older than ttl
|
||||
*/
|
||||
public function expunge()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired records of all caches
|
||||
*/
|
||||
public static function gc()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cache entry.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function get_item($key)
|
||||
{
|
||||
if (!self::$memcache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = self::$memcache->get($key);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entry into the cache.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
* @param mixed $data Serialized cache data
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function add_item($key, $data)
|
||||
{
|
||||
if (!self::$memcache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = self::$memcache->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
|
||||
|
||||
if (!$result) {
|
||||
$result = self::$memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes entry from the cache
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function delete_item($key)
|
||||
{
|
||||
if (!self::$memcache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// #1488592: use 2nd argument
|
||||
$result = self::$memcache->delete($key, 0);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 - Memcache |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface implementation class for accessing Memcached cache
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Cache
|
||||
*/
|
||||
class rcube_cache_memcached extends rcube_cache
|
||||
{
|
||||
/**
|
||||
* Instance of memcached handler
|
||||
*
|
||||
* @var Memcached
|
||||
*/
|
||||
protected static $memcache;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
parent::__construct($userid, $prefix, $ttl, $packed, $indexed);
|
||||
|
||||
$this->type = 'memcache';
|
||||
$this->debug = rcube::get_instance()->config->get('memcache_debug');
|
||||
|
||||
// Maximum TTL is 30 days, bigger values are treated by Memcached
|
||||
// as unix timestamp which is not what we want
|
||||
if ($this->ttl > 60*60*24*30) {
|
||||
$this->ttl = 60*60*24*30;
|
||||
}
|
||||
|
||||
self::engine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global handle for memcache access
|
||||
*
|
||||
* @return object Memcache
|
||||
*/
|
||||
public static function engine()
|
||||
{
|
||||
if (self::$memcache !== null) {
|
||||
return self::$memcache;
|
||||
}
|
||||
|
||||
// no memcache support in PHP
|
||||
if (!class_exists('Memcached')) {
|
||||
self::$memcache = false;
|
||||
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'memcache', 'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Failed to find Memcached. Make sure php-memcached is installed"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
// add all configured hosts to pool
|
||||
$rcube = rcube::get_instance();
|
||||
$pconnect = $rcube->config->get('memcache_pconnect', true);
|
||||
$timeout = $rcube->config->get('memcache_timeout', 1);
|
||||
$retry_interval = $rcube->config->get('memcache_retry_interval', 15);
|
||||
$hosts = $rcube->config->get('memcache_hosts');
|
||||
$persistent_id = $pconnect ? ('rc' . md5(serialize($hosts))) : null;
|
||||
|
||||
self::$memcache = new Memcached($persistent_id);
|
||||
|
||||
self::$memcache->setOptions([
|
||||
Memcached::OPT_CONNECT_TIMEOUT => $timeout * 1000,
|
||||
Memcached::OPT_RETRY_TIMEOUT => $timeout,
|
||||
Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
|
||||
Memcached::OPT_COMPRESSION => true,
|
||||
]);
|
||||
|
||||
if (!$pconnect || !count(self::$memcache->getServerList())) {
|
||||
foreach ((array) $hosts as $host) {
|
||||
if (substr($host, 0, 7) != 'unix://') {
|
||||
list($host, $port) = explode(':', $host);
|
||||
if (!$port) $port = 11211;
|
||||
}
|
||||
else {
|
||||
$host = substr($host, 7);
|
||||
$port = 0;
|
||||
}
|
||||
|
||||
self::$memcache->addServer($host, $port);
|
||||
}
|
||||
}
|
||||
|
||||
// test connection
|
||||
$result = self::$memcache->increment('__CONNECTIONTEST__');
|
||||
|
||||
if ($result === false && ($res_code = self::$memcache->getResultCode()) !== Memcached::RES_NOTFOUND) {
|
||||
self::$memcache = false;
|
||||
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'memcache', 'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Memcache connection failure (code: $res_code)."
|
||||
],
|
||||
true, false);
|
||||
}
|
||||
|
||||
return self::$memcache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache records older than ttl
|
||||
*/
|
||||
public function expunge()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired records of all caches
|
||||
*/
|
||||
public static function gc()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cache entry.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function get_item($key)
|
||||
{
|
||||
if (!self::$memcache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = self::$memcache->get($key);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entry into the cache.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
* @param mixed $data Serialized cache data
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function add_item($key, $data)
|
||||
{
|
||||
if (!self::$memcache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = self::$memcache->set($key, $data, $this->ttl);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes entry from the cache
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function delete_item($key)
|
||||
{
|
||||
if (!self::$memcache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// #1488592: use 2nd argument
|
||||
$result = self::$memcache->delete($key, 0);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 - Redis |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface implementation class for accessing Redis cache
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Cache
|
||||
*/
|
||||
class rcube_cache_redis extends rcube_cache
|
||||
{
|
||||
/**
|
||||
* Instance of Redis object
|
||||
*
|
||||
* @var Redis
|
||||
*/
|
||||
protected static $redis;
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
parent::__construct($userid, $prefix, $ttl, $packed, $indexed);
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
$this->type = 'redis';
|
||||
$this->debug = $rcube->config->get('redis_debug');
|
||||
|
||||
self::engine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global handle for redis access
|
||||
*
|
||||
* @return object Redis
|
||||
*/
|
||||
public static function engine()
|
||||
{
|
||||
if (self::$redis !== null) {
|
||||
return self::$redis;
|
||||
}
|
||||
|
||||
if (!class_exists('Redis')) {
|
||||
self::$redis = false;
|
||||
|
||||
rcube::raise_error([
|
||||
'code' => 604,
|
||||
'type' => 'redis',
|
||||
'line' => __LINE__,
|
||||
'file' => __FILE__,
|
||||
'message' => "Failed to find Redis. Make sure php-redis is included"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
$hosts = $rcube->config->get('redis_hosts');
|
||||
|
||||
// host config is wrong
|
||||
if (!is_array($hosts) || empty($hosts)) {
|
||||
rcube::raise_error([
|
||||
'code' => 604,
|
||||
'type' => 'redis',
|
||||
'line' => __LINE__,
|
||||
'file' => __FILE__,
|
||||
'message' => "Redis host not configured"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
// only allow 1 host for now until we support clustering
|
||||
if (count($hosts) > 1) {
|
||||
rcube::raise_error([
|
||||
'code' => 604,
|
||||
'type' => 'redis',
|
||||
'line' => __LINE__,
|
||||
'file' => __FILE__,
|
||||
'message' => "Redis cluster not yet supported"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
self::$redis = new Redis;
|
||||
$failures = 0;
|
||||
|
||||
foreach ($hosts as $redis_host) {
|
||||
// explode individual fields
|
||||
list($host, $port, $database, $password) = array_pad(explode(':', $redis_host, 4), 4, null);
|
||||
|
||||
if (substr($redis_host, 0, 7) === 'unix://') {
|
||||
$host = substr($port, 2);
|
||||
$port = 0;
|
||||
}
|
||||
else {
|
||||
// set default values if not set
|
||||
$host = $host ?: '127.0.0.1';
|
||||
$port = $port ?: 6379;
|
||||
}
|
||||
|
||||
try {
|
||||
if (self::$redis->connect($host, $port) === false) {
|
||||
throw new Exception("Could not connect to Redis server. Please check host and port.");
|
||||
}
|
||||
|
||||
if ($password !== null && self::$redis->auth($password) === false) {
|
||||
throw new Exception("Could not authenticate with Redis server. Please check password.");
|
||||
}
|
||||
|
||||
if ($database !== null && self::$redis->select($database) === false) {
|
||||
throw new Exception("Could not select Redis database. Please check database setting.");
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
$failures++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($hosts) === $failures) {
|
||||
self::$redis = false;
|
||||
}
|
||||
|
||||
if (self::$redis) {
|
||||
try {
|
||||
$ping = self::$redis->ping();
|
||||
if ($ping !== true && $ping !== "+PONG") {
|
||||
throw new Exception("Redis connection failure. Ping failed.");
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
self::$redis = false;
|
||||
rcube::raise_error($e, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache records older than ttl
|
||||
*/
|
||||
public function expunge()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired records
|
||||
*/
|
||||
public static function gc()
|
||||
{
|
||||
// No need for GC, entries are expunged automatically
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cache entry.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function get_item($key)
|
||||
{
|
||||
if (!self::$redis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = self::$redis->get($key);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entry into Redis.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
* @param mixed $data Serialized cache data
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function add_item($key, $data)
|
||||
{
|
||||
if (!self::$redis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = self::$redis->setEx($key, $this->ttl, $data);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes entry from Redis.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function delete_item($key)
|
||||
{
|
||||
if (!self::$redis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$fname = method_exists(self::$redis, 'del') ? 'del' : 'delete';
|
||||
$result = self::$redis->$fname($key);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Database wrapper class that implements PHP PDO functions |
|
||||
| for MS SQL Server database |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database independent query interface
|
||||
* This is a wrapper for the PHP PDO
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Database
|
||||
*/
|
||||
class rcube_db_mssql extends rcube_db
|
||||
{
|
||||
public $db_provider = 'mssql';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
|
||||
{
|
||||
parent::__construct($db_dsnw, $db_dsnr, $pconn);
|
||||
|
||||
$this->options['identifier_start'] = '[';
|
||||
$this->options['identifier_end'] = ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver-specific configuration of database connection
|
||||
*
|
||||
* @param array $dsn DSN for DB connections
|
||||
* @param PDO $dbh Connection handler
|
||||
*/
|
||||
protected function conn_configure($dsn, $dbh)
|
||||
{
|
||||
// Set date format in case of non-default language (#1488918)
|
||||
$dbh->query("SET DATEFORMAT ymd");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "dateadd(second, $interval, getdate())";
|
||||
}
|
||||
|
||||
return "getdate()";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract SQL statement for value concatenation
|
||||
*
|
||||
* @return string ...$args Values to concatenate
|
||||
*/
|
||||
public function concat(...$args)
|
||||
{
|
||||
if (count($args) == 1 && is_array($args[0])) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
return '(' . implode('+', $args) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds TOP (LIMIT,OFFSET) clause to the query
|
||||
*
|
||||
* @param string $query SQL query
|
||||
* @param int $limit Number of rows
|
||||
* @param int $offset Offset
|
||||
*
|
||||
* @return string SQL query
|
||||
*/
|
||||
protected function set_limit($query, $limit = 0, $offset = 0)
|
||||
{
|
||||
$limit = intval($limit);
|
||||
$offset = intval($offset);
|
||||
$end = $offset + $limit;
|
||||
|
||||
// query without OFFSET
|
||||
if (!$offset) {
|
||||
$query = preg_replace('/^SELECT\s/i', "SELECT TOP $limit ", $query);
|
||||
return $query;
|
||||
}
|
||||
|
||||
$orderby = stristr($query, 'ORDER BY');
|
||||
$offset += 1;
|
||||
|
||||
if ($orderby !== false) {
|
||||
$query = trim(substr($query, 0, -1 * strlen($orderby)));
|
||||
}
|
||||
else {
|
||||
// it shouldn't happen, paging without sorting has not much sense
|
||||
// @FIXME: I don't know how to build paging query without ORDER BY
|
||||
$orderby = "ORDER BY 1";
|
||||
}
|
||||
|
||||
$query = preg_replace('/^SELECT\s/i', '', $query);
|
||||
$query = "WITH paging AS (SELECT ROW_NUMBER() OVER ($orderby) AS [RowNumber], $query)"
|
||||
. " SELECT * FROM paging WHERE [RowNumber] BETWEEN $offset AND $end ORDER BY [RowNumber]";
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns PDO DSN string from DSN array
|
||||
*/
|
||||
protected function dsn_string($dsn)
|
||||
{
|
||||
$params = [];
|
||||
$result = $dsn['phptype'] . ':';
|
||||
|
||||
if (isset($dsn['hostspec'])) {
|
||||
$host = $dsn['hostspec'];
|
||||
if (isset($dsn['port'])) {
|
||||
$host .= ',' . $dsn['port'];
|
||||
}
|
||||
$params[] = 'host=' . $host;
|
||||
}
|
||||
|
||||
if (isset($dsn['database'])) {
|
||||
$params[] = 'dbname=' . $dsn['database'];
|
||||
}
|
||||
|
||||
if (!empty($params)) {
|
||||
$result .= implode(';', $params);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SQL file and fix table names according to table prefix
|
||||
*/
|
||||
protected function fix_table_names($sql)
|
||||
{
|
||||
if (!$this->options['table_prefix']) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// replace sequence names, and other postgres-specific commands
|
||||
$sql = preg_replace_callback(
|
||||
'/((TABLE|(?<!ON )UPDATE|INSERT INTO|FROM(?! deleted)| ON(?! (DELETE|UPDATE|\[PRIMARY\]))'
|
||||
. '|REFERENCES|CONSTRAINT|TRIGGER|INDEX)\s+(\[dbo\]\.)?[\[\]]*)([^\[\]\( \r\n]+)/',
|
||||
[$this, 'fix_table_names_callback'],
|
||||
$sql
|
||||
);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Database wrapper class that implements PHP PDO functions |
|
||||
| for MySQL database |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database independent query interface
|
||||
*
|
||||
* This is a wrapper for the PHP PDO
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Database
|
||||
*/
|
||||
class rcube_db_mysql extends rcube_db
|
||||
{
|
||||
public $db_provider = 'mysql';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
|
||||
{
|
||||
parent::__construct($db_dsnw, $db_dsnr, $pconn);
|
||||
|
||||
// SQL identifiers quoting
|
||||
$this->options['identifier_start'] = '`';
|
||||
$this->options['identifier_end'] = '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract SQL statement for value concatenation
|
||||
*
|
||||
* @return string ...$args Values to concatenate
|
||||
*/
|
||||
public function concat(...$args)
|
||||
{
|
||||
if (count($args) == 1 && is_array($args[0])) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
return 'CONCAT(' . implode(', ', $args) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns PDO DSN string from DSN array
|
||||
*
|
||||
* @param array $dsn DSN parameters
|
||||
*
|
||||
* @return string Connection string
|
||||
*/
|
||||
protected function dsn_string($dsn)
|
||||
{
|
||||
$params = [];
|
||||
$result = 'mysql:';
|
||||
|
||||
if (isset($dsn['database'])) {
|
||||
$params[] = 'dbname=' . $dsn['database'];
|
||||
}
|
||||
|
||||
if (isset($dsn['hostspec'])) {
|
||||
$params[] = 'host=' . $dsn['hostspec'];
|
||||
}
|
||||
|
||||
if (isset($dsn['port'])) {
|
||||
$params[] = 'port=' . $dsn['port'];
|
||||
}
|
||||
|
||||
if (isset($dsn['socket'])) {
|
||||
$params[] = 'unix_socket=' . $dsn['socket'];
|
||||
}
|
||||
|
||||
$params[] = 'charset=' . (!empty($dsn['charset']) ? $dsn['charset'] : 'utf8mb4');
|
||||
|
||||
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 = parent::dsn_options($dsn);
|
||||
|
||||
if (!empty($dsn['key'])) {
|
||||
$result[PDO::MYSQL_ATTR_SSL_KEY] = $dsn['key'];
|
||||
}
|
||||
|
||||
if (!empty($dsn['cipher'])) {
|
||||
$result[PDO::MYSQL_ATTR_SSL_CIPHER] = $dsn['cipher'];
|
||||
}
|
||||
|
||||
if (!empty($dsn['cert'])) {
|
||||
$result[PDO::MYSQL_ATTR_SSL_CERT] = $dsn['cert'];
|
||||
}
|
||||
|
||||
if (!empty($dsn['capath'])) {
|
||||
$result[PDO::MYSQL_ATTR_SSL_CAPATH] = $dsn['capath'];
|
||||
}
|
||||
|
||||
if (!empty($dsn['ca'])) {
|
||||
$result[PDO::MYSQL_ATTR_SSL_CA] = $dsn['ca'];
|
||||
}
|
||||
|
||||
if (isset($dsn['verify_server_cert'])) {
|
||||
$result[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = rcube_utils::get_boolean($dsn['verify_server_cert']);
|
||||
}
|
||||
|
||||
// Always return matching (not affected only) rows count
|
||||
$result[PDO::MYSQL_ATTR_FOUND_ROWS] = true;
|
||||
|
||||
// Enable AUTOCOMMIT mode (#1488902)
|
||||
$result[PDO::ATTR_AUTOCOMMIT] = true;
|
||||
|
||||
// Disable emulating of prepared statements
|
||||
$result[PDO::ATTR_EMULATE_PREPARES] = false;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"
|
||||
. " WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'"
|
||||
. " ORDER BY TABLE_NAME", $this->db_dsnw_array['database']);
|
||||
|
||||
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : [];
|
||||
}
|
||||
|
||||
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_SCHEMA = ? AND TABLE_NAME = ?",
|
||||
$this->db_dsnw_array['database'], $table);
|
||||
|
||||
if ($q) {
|
||||
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database runtime variables
|
||||
*
|
||||
* @param string $varname Variable name
|
||||
* @param mixed $default Default value if variable is not set
|
||||
*
|
||||
* @return mixed Variable value or default
|
||||
*/
|
||||
public function get_variable($varname, $default = null)
|
||||
{
|
||||
if (!isset($this->variables)) {
|
||||
$this->variables = [];
|
||||
}
|
||||
|
||||
if (array_key_exists($varname, $this->variables)) {
|
||||
return $this->variables[$varname];
|
||||
}
|
||||
|
||||
// configured value has higher prio
|
||||
$conf_value = rcube::get_instance()->config->get('db_' . $varname);
|
||||
if ($conf_value !== null) {
|
||||
return $this->variables[$varname] = $conf_value;
|
||||
}
|
||||
|
||||
$result = $this->query('SHOW VARIABLES LIKE ?', $varname);
|
||||
|
||||
while ($row = $this->fetch_array($result)) {
|
||||
$this->variables[$row[0]] = $row[1];
|
||||
}
|
||||
|
||||
// not found, use default
|
||||
if (!isset($this->variables[$varname])) {
|
||||
$this->variables[$varname] = $default;
|
||||
}
|
||||
|
||||
return $this->variables[$varname];
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT ... ON DUPLICATE KEY UPDATE (or equivalent).
|
||||
* When not supported by the engine we do UPDATE and INSERT.
|
||||
*
|
||||
* @param string $table Table name (should be already passed via table_name() with quoting)
|
||||
* @param array $keys Hash array (column => value) of the unique constraint
|
||||
* @param array $columns List of columns to update
|
||||
* @param array $values List of values to update (number of elements
|
||||
* should be the same as in $columns)
|
||||
*
|
||||
* @return PDOStatement|bool Query handle or False on error
|
||||
* @todo Multi-insert support
|
||||
*/
|
||||
public function insert_or_update($table, $keys, $columns, $values)
|
||||
{
|
||||
$columns = array_map(function($i) { return "`$i`"; }, $columns);
|
||||
$cols = implode(', ', array_map(function($i) { return "`$i`"; }, array_keys($keys)));
|
||||
$cols .= ', ' . implode(', ', $columns);
|
||||
$vals = implode(', ', array_map(function($i) { return $this->quote($i); }, $keys));
|
||||
$vals .= ', ' . rtrim(str_repeat('?, ', count($columns)), ', ');
|
||||
$update = implode(', ', array_map(function($i) { return "$i = VALUES($i)"; }, $columns));
|
||||
|
||||
return $this->query("INSERT INTO $table ($cols) VALUES ($vals)"
|
||||
. " ON DUPLICATE KEY UPDATE $update", $values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 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';
|
||||
public $in_transaction = false;
|
||||
|
||||
/**
|
||||
* 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([
|
||||
'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([
|
||||
'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 = [
|
||||
"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 bool 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 = [];
|
||||
|
||||
if (!empty($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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query = rtrim($query, " \t\n\r\0\x0B;");
|
||||
|
||||
// replace escaped '?' and quotes back to normal, see self::quote()
|
||||
$query = str_replace(
|
||||
['??', self::DEFAULT_QUOTE.self::DEFAULT_QUOTE],
|
||||
['?', self::DEFAULT_QUOTE],
|
||||
$query
|
||||
);
|
||||
|
||||
// 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 (array_keys($args) as $param) {
|
||||
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 overridden by a driver implementation
|
||||
*
|
||||
* @param string Query that triggered the error
|
||||
* @param resource Query result
|
||||
*
|
||||
* @return bool Result to be stored and returned
|
||||
*/
|
||||
protected function handle_error($query, $result = null)
|
||||
{
|
||||
$error = oci_error($result ?: $this->dbh);
|
||||
|
||||
// @TODO: Find error codes for key errors
|
||||
if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], ['23000', '23505'])) {
|
||||
$this->db_error = true;
|
||||
$this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']);
|
||||
|
||||
rcube::raise_error([
|
||||
'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 !empty($result[0]) ? $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 array|false 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 ($input instanceof DateTime) {
|
||||
return $this->quote($input->format($this->options['datetime_format']));
|
||||
}
|
||||
|
||||
if ($type == 'ident') {
|
||||
return $this->quote_identifier($input);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
case 'integer':
|
||||
return intval($input);
|
||||
default:
|
||||
return "'" . strtr($input, [
|
||||
'?' => '??',
|
||||
"'" => "''",
|
||||
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]+)/',
|
||||
[$this, 'fix_table_names_callback'],
|
||||
$sql
|
||||
);
|
||||
|
||||
$sql = preg_replace_callback(
|
||||
'/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/',
|
||||
[$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 = [];
|
||||
|
||||
if (isset($dsn['hostspec'])) {
|
||||
$host = $dsn['hostspec'];
|
||||
if (isset($dsn['port'])) {
|
||||
$host .= ':' . $dsn['port'];
|
||||
}
|
||||
|
||||
$params['database'] = $host . '/' . $dsn['database'];
|
||||
}
|
||||
|
||||
$params['charset'] = 'UTF8';
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given SQL script
|
||||
*
|
||||
* @param string $sql SQL queries to execute
|
||||
*
|
||||
* @return bool 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 == '' || $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 (@oci_rollback($this->dbh)) {
|
||||
$this->in_transaction = false;
|
||||
}
|
||||
else {
|
||||
$this->handle_error('ROLLBACK');
|
||||
}
|
||||
|
||||
return $this->last_result = $this->dbh->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate database connection.
|
||||
*/
|
||||
public function closeConnection()
|
||||
{
|
||||
// release statement and close connection(s)
|
||||
$this->last_result = null;
|
||||
foreach ($this->dbhs as $dbh) {
|
||||
oci_close($dbh);
|
||||
}
|
||||
|
||||
parent::closeConnection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 for query parameters |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database query parameter
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Database
|
||||
*/
|
||||
class rcube_db_param
|
||||
{
|
||||
protected $db;
|
||||
protected $type;
|
||||
protected $value;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_db $db Database driver
|
||||
* @param mixed $value Parameter value
|
||||
* @param string $type Parameter type (One of rcube_db::TYPE_* constants)
|
||||
*/
|
||||
public function __construct($db, $value, $type = null)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->value = $value;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value as string for inlining into SQL query
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
if ($this->type === rcube_db::TYPE_SQL) {
|
||||
return (string) $this->value;
|
||||
}
|
||||
|
||||
return (string) $this->db->quote($this->value, $this->type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Database wrapper class that implements PHP PDO functions |
|
||||
| for PostgreSQL database |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database independent query interface
|
||||
* This is a wrapper for the PHP PDO
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Database
|
||||
*/
|
||||
class rcube_db_pgsql extends rcube_db
|
||||
{
|
||||
public $db_provider = 'postgres';
|
||||
|
||||
// See https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS
|
||||
private static $libpq_connect_params = [
|
||||
'application_name',
|
||||
'sslmode',
|
||||
'sslcert',
|
||||
'sslkey',
|
||||
'sslrootcert',
|
||||
'sslcrl',
|
||||
'sslcompression',
|
||||
'service'
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
|
||||
{
|
||||
parent::__construct($db_dsnw, $db_dsnr, $pconn);
|
||||
|
||||
// use date/time input format with timezone spec.
|
||||
$this->options['datetime_format'] = 'c';
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver-specific configuration of database connection
|
||||
*
|
||||
* @param array $dsn DSN for DB connections
|
||||
* @param PDO $dbh Connection handler
|
||||
*/
|
||||
protected function conn_configure($dsn, $dbh)
|
||||
{
|
||||
$dbh->query("SET NAMES 'utf8'");
|
||||
$dbh->query("SET DATESTYLE TO ISO");
|
||||
|
||||
// if ?schema= is set in dsn, set the search_path
|
||||
if (!empty($dsn['schema'])) {
|
||||
$dbh->query("SET search_path TO " . $this->quote($dsn['schema']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last inserted record ID
|
||||
*
|
||||
* @param string $table Table name (to find the incremented sequence)
|
||||
*
|
||||
* @return mixed ID or false on failure
|
||||
*/
|
||||
public function insert_id($table = null)
|
||||
{
|
||||
if (!$this->db_connected || $this->db_mode == 'r') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($table) {
|
||||
$table = $this->sequence_name($table);
|
||||
}
|
||||
|
||||
return $this->dbh->lastInsertId($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 "EXTRACT (EPOCH FROM $field)";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$result = 'now()';
|
||||
|
||||
if ($interval) {
|
||||
$result .= ' ' . ($interval > 0 ? '+' : '-') . " interval '"
|
||||
. ($interval > 0 ? intval($interval) : intval($interval) * -1)
|
||||
. " seconds'";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return SQL statement for case insensitive LIKE
|
||||
*
|
||||
* @param string $column Field name
|
||||
* @param string $value Search value
|
||||
*
|
||||
* @return string SQL statement to use in query
|
||||
*/
|
||||
public function ilike($column, $value)
|
||||
{
|
||||
return $this->quote_identifier($column) . ' ILIKE ' . $this->quote($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database runtime variables
|
||||
*
|
||||
* @param string $varname Variable name
|
||||
* @param mixed $default Default value if variable is not set
|
||||
*
|
||||
* @return mixed Variable value or default
|
||||
*/
|
||||
public function get_variable($varname, $default = null)
|
||||
{
|
||||
// There's a known case when max_allowed_packet is queried
|
||||
// PostgreSQL doesn't have such limit, return immediately
|
||||
if ($varname == 'max_allowed_packet') {
|
||||
return rcube::get_instance()->config->get('db_' . $varname, $default);
|
||||
}
|
||||
|
||||
$this->variables[$varname] = rcube::get_instance()->config->get('db_' . $varname);
|
||||
|
||||
if (!isset($this->variables)) {
|
||||
$this->variables = [];
|
||||
|
||||
$result = $this->query('SHOW ALL');
|
||||
|
||||
while ($row = $this->fetch_array($result)) {
|
||||
$this->variables[$row[0]] = $row[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->variables[$varname] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* INSERT ... ON CONFLICT DO UPDATE.
|
||||
* When not supported by the engine we do UPDATE and INSERT.
|
||||
*
|
||||
* @param string $table Table name (should be already passed via table_name() with quoting)
|
||||
* @param array $keys Hash array (column => value) of the unique constraint
|
||||
* @param array $columns List of columns to update
|
||||
* @param array $values List of values to update (number of elements
|
||||
* should be the same as in $columns)
|
||||
*
|
||||
* @return PDOStatement|bool Query handle or False on error
|
||||
* @todo Multi-insert support
|
||||
*/
|
||||
public function insert_or_update($table, $keys, $columns, $values)
|
||||
{
|
||||
// Check if version >= 9.5, otherwise use fallback
|
||||
if ($this->get_variable('server_version_num') < 90500) {
|
||||
return parent::insert_or_update($table, $keys, $columns, $values);
|
||||
}
|
||||
|
||||
$columns = array_map([$this, 'quote_identifier'], $columns);
|
||||
$target = implode(', ', array_map([$this, 'quote_identifier'], array_keys($keys)));
|
||||
$cols = $target . ', ' . implode(', ', $columns);
|
||||
$vals = implode(', ', array_map(function($i) { return $this->quote($i); }, $keys));
|
||||
$vals .= ', ' . rtrim(str_repeat('?, ', count($columns)), ', ');
|
||||
$update = implode(', ', array_map(function($i) { return "$i = EXCLUDED.$i"; }, $columns));
|
||||
|
||||
return $this->query("INSERT INTO $table ($cols) VALUES ($vals)"
|
||||
. " ON CONFLICT ($target) DO UPDATE SET $update", $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (($schema = $this->options['table_prefix']) && $schema[strlen($schema)-1] === '.') {
|
||||
$add = " AND TABLE_SCHEMA = " . $this->quote(substr($schema, 0, -1));
|
||||
}
|
||||
else {
|
||||
$add = " AND TABLE_SCHEMA NOT IN ('pg_catalog', 'information_schema')";
|
||||
}
|
||||
|
||||
$q = $this->query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"
|
||||
. " WHERE TABLE_TYPE = 'BASE TABLE'" . $add
|
||||
. " ORDER BY TABLE_NAME");
|
||||
|
||||
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : [];
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$args = [$table];
|
||||
|
||||
if (($schema = $this->options['table_prefix']) && $schema[strlen($schema)-1] === '.') {
|
||||
$add = " AND TABLE_SCHEMA = ?";
|
||||
$args[] = substr($schema, 0, -1);
|
||||
}
|
||||
else {
|
||||
$add = " AND TABLE_SCHEMA NOT IN ('pg_catalog', 'information_schema')";
|
||||
}
|
||||
|
||||
$q = $this->query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS"
|
||||
. " WHERE TABLE_NAME = ?" . $add, $args);
|
||||
|
||||
if ($q) {
|
||||
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns PDO DSN string from DSN array
|
||||
*
|
||||
* @param array $dsn DSN parameters
|
||||
*
|
||||
* @return string DSN string
|
||||
*/
|
||||
protected function dsn_string($dsn)
|
||||
{
|
||||
$params = [];
|
||||
$result = 'pgsql:';
|
||||
|
||||
if (isset($dsn['hostspec'])) {
|
||||
$params[] = 'host=' . $dsn['hostspec'];
|
||||
}
|
||||
else if (isset($dsn['socket'])) {
|
||||
$params[] = 'host=' . $dsn['socket'];
|
||||
}
|
||||
|
||||
if (isset($dsn['port'])) {
|
||||
$params[] = 'port=' . $dsn['port'];
|
||||
}
|
||||
|
||||
if (isset($dsn['database'])) {
|
||||
$params[] = 'dbname=' . $dsn['database'];
|
||||
}
|
||||
|
||||
foreach (self::$libpq_connect_params as $param) {
|
||||
if (isset($dsn[$param])) {
|
||||
$params[] = $param . '=' . $dsn[$param];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($params)) {
|
||||
$result .= implode(';', $params);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 postgres-specific commands
|
||||
$sql = preg_replace_callback(
|
||||
'/((SEQUENCE |RENAME TO |nextval\()["\']*)([^"\' \r\n]+)/',
|
||||
[$this, 'fix_table_names_callback'],
|
||||
$sql
|
||||
);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Database wrapper class that implements PHP PDO functions |
|
||||
| for SQLite database |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database independent query interface
|
||||
* This is a wrapper for the PHP PDO
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Database
|
||||
*/
|
||||
class rcube_db_sqlite extends rcube_db
|
||||
{
|
||||
public $db_provider = 'sqlite';
|
||||
|
||||
/**
|
||||
* Prepare connection
|
||||
*/
|
||||
protected function conn_prepare($dsn)
|
||||
{
|
||||
// Create database file, required by PDO to exist on connection
|
||||
if (!empty($dsn['database']) && !file_exists($dsn['database'])) {
|
||||
$created = touch($dsn['database']);
|
||||
|
||||
// File mode setting, for compat. with MDB2
|
||||
if (!empty($dsn['mode']) && $created) {
|
||||
chmod($dsn['database'], octdec($dsn['mode']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure connection, create database if not exists
|
||||
*/
|
||||
protected function conn_configure($dsn, $dbh)
|
||||
{
|
||||
// Initialize database structure in file is empty
|
||||
if (!empty($dsn['database']) && !filesize($dsn['database'])) {
|
||||
$data = file_get_contents(RCUBE_INSTALL_PATH . 'SQL/sqlite.initial.sql');
|
||||
|
||||
if (strlen($data)) {
|
||||
$this->debug('INITIALIZE DATABASE');
|
||||
|
||||
$q = $dbh->exec($data);
|
||||
|
||||
if ($q === false) {
|
||||
$error = $dbh->errorInfo();
|
||||
$this->db_error = true;
|
||||
$this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
|
||||
|
||||
rcube::raise_error([
|
||||
'code' => 500, 'type' => 'db',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => $this->db_error_msg
|
||||
],
|
||||
true, false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable WAL mode to fix locking issues like #8035.
|
||||
$dbh->query("PRAGMA journal_mode = WAL");
|
||||
|
||||
// Enable foreign keys (requires sqlite 3.6.19 compiled with FK support)
|
||||
$dbh->query("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 "strftime('%s', $field)";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$add = '';
|
||||
|
||||
if ($interval) {
|
||||
$add = ($interval > 0 ? '+' : '') . intval($interval) . ' seconds';
|
||||
}
|
||||
|
||||
return "datetime('now'" . ($add ? ", '$add'" : "") . ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of tables in database
|
||||
*
|
||||
* @return array List of all tables of the current database
|
||||
*/
|
||||
public function list_tables()
|
||||
{
|
||||
if ($this->tables === null) {
|
||||
$q = $this->query('SELECT name FROM sqlite_master'
|
||||
.' WHERE type = \'table\' ORDER BY name');
|
||||
|
||||
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : [];
|
||||
}
|
||||
|
||||
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('PRAGMA table_info(?)', $table);
|
||||
|
||||
return $q ? $q->fetchAll(PDO::FETCH_COLUMN, 1) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build DSN string for PDO constructor
|
||||
*/
|
||||
protected function dsn_string($dsn)
|
||||
{
|
||||
return $dsn['phptype'] . ':' . $dsn['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns driver-specific connection options
|
||||
*
|
||||
* @param array $dsn DSN parameters
|
||||
*
|
||||
* @return array Connection options
|
||||
*/
|
||||
protected function dsn_options($dsn)
|
||||
{
|
||||
$result = parent::dsn_options($dsn);
|
||||
|
||||
// Change the default timeout (60) to a smaller value
|
||||
$result[PDO::ATTR_TIMEOUT] = isset($dsn['timeout']) ? intval($dsn['timeout']) : 10;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Database wrapper class that implements PHP PDO functions |
|
||||
| for MS SQL Server database |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Database independent query interface
|
||||
* This is a wrapper for the PHP PDO
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Database
|
||||
*/
|
||||
class rcube_db_sqlsrv extends rcube_db_mssql
|
||||
{
|
||||
|
||||
/**
|
||||
* Get last inserted record ID
|
||||
*
|
||||
* @param string $table Table name (to find the incremented sequence)
|
||||
*
|
||||
* @return string|false The ID or False on failure
|
||||
*/
|
||||
public function insert_id($table = '')
|
||||
{
|
||||
if (!$this->db_connected || $this->db_mode == 'r') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($table) {
|
||||
// For some unknown reason the constant described in the driver docs
|
||||
// might not exist, we'll fallback to PDO::ATTR_CLIENT_VERSION (#7564)
|
||||
if (defined('PDO::ATTR_DRIVER_VERSION')) {
|
||||
$driver_version = $this->dbh->getAttribute(PDO::ATTR_DRIVER_VERSION);
|
||||
}
|
||||
else if (defined('PDO::ATTR_CLIENT_VERSION')) {
|
||||
$client_version = $this->dbh->getAttribute(PDO::ATTR_CLIENT_VERSION);
|
||||
$driver_version = $client_version['ExtensionVer'];
|
||||
}
|
||||
else {
|
||||
$driver_version = 5;
|
||||
}
|
||||
|
||||
// Starting from version 5 of the driver lastInsertId() method expects
|
||||
// a sequence name instead of a table name. We'll unset the argument
|
||||
// to get the last insert sequence (#7564)
|
||||
if (version_compare($driver_version, '5', '>=')) {
|
||||
$table = null;
|
||||
}
|
||||
else {
|
||||
// resolve table name
|
||||
$table = $this->table_name($table);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->dbh->lastInsertId($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns PDO DSN string from DSN array
|
||||
*/
|
||||
protected function dsn_string($dsn)
|
||||
{
|
||||
$params = [];
|
||||
$result = 'sqlsrv:';
|
||||
|
||||
if (isset($dsn['hostspec'])) {
|
||||
$host = $dsn['hostspec'];
|
||||
|
||||
if (isset($dsn['port'])) {
|
||||
$host .= ',' . $dsn['port'];
|
||||
}
|
||||
|
||||
$params[] = 'Server=' . $host;
|
||||
}
|
||||
|
||||
if (isset($dsn['database'])) {
|
||||
$params[] = 'Database=' . $dsn['database'];
|
||||
}
|
||||
|
||||
if (!empty($params)) {
|
||||
$result .= implode(';', $params);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,890 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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;
|
||||
|
||||
// search modes
|
||||
const SEARCH_ALL = 0;
|
||||
const SEARCH_STRICT = 1;
|
||||
const SEARCH_PREFIX = 2;
|
||||
const SEARCH_GROUPS = 4;
|
||||
|
||||
// contact types, note: some of these are used as addressbook source identifiers
|
||||
const TYPE_CONTACT = 0;
|
||||
const TYPE_RECIPIENT = 1;
|
||||
const TYPE_TRUSTED_SENDER = 2;
|
||||
const TYPE_DEFAULT = 4;
|
||||
const TYPE_WRITEABLE = 8;
|
||||
const TYPE_READONLY = 16;
|
||||
|
||||
// public properties (mandatory)
|
||||
|
||||
/** @var string Name of the primary key field of this addressbook. Used to search for previously retrieved IDs. */
|
||||
public $primary_key;
|
||||
|
||||
/** @var bool True if the addressbook supports contact groups. */
|
||||
public $groups = false;
|
||||
|
||||
/**
|
||||
* @var bool True if the addressbook supports exporting contact groups. Requires the implementation of
|
||||
* get_record_groups().
|
||||
*/
|
||||
public $export_groups = true;
|
||||
|
||||
/** @var bool True if the addressbook is read-only. */
|
||||
public $readonly = true;
|
||||
|
||||
/**
|
||||
* @var bool True if the addressbook does not support listing all records but needs use of the search function.
|
||||
*/
|
||||
public $searchonly = false;
|
||||
|
||||
/** @var bool True if the addressbook supports restoring deleted contacts. */
|
||||
public $undelete = false;
|
||||
|
||||
/** @var bool True if the addressbook is ready to be used. See rcmail_action_contacts_index::$CONTACT_COLTYPES */
|
||||
public $ready = false;
|
||||
|
||||
/**
|
||||
* @var null|string|int If set, addressbook-specific identifier of the selected group. All contact listing and
|
||||
* contact searches will be limited to contacts that belong to this group.
|
||||
*/
|
||||
public $group_id = null;
|
||||
|
||||
/** @var int The current page of the listing. Numbering starts at 1. */
|
||||
public $list_page = 1;
|
||||
|
||||
/** @var int The maximum number of records shown on a page. */
|
||||
public $page_size = 10;
|
||||
|
||||
/** @var string Contact field by which to order listed records. */
|
||||
public $sort_col = 'name';
|
||||
|
||||
/** @var string Whether sorting of records by $sort_col is done in ascending (ASC) or descending (DESC) order. */
|
||||
public $sort_order = 'ASC';
|
||||
|
||||
/** @var string[] A list of record fields that contain dates. */
|
||||
public $date_cols = [];
|
||||
|
||||
/** @var array Definition of the contact fields supported by the addressbook. */
|
||||
public $coltypes = [
|
||||
'name' => ['limit' => 1],
|
||||
'firstname' => ['limit' => 1],
|
||||
'surname' => ['limit' => 1],
|
||||
'email' => ['limit' => 1]
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[] vCard additional fields mapping
|
||||
*/
|
||||
public $vcard_map = [];
|
||||
|
||||
/** @var ?array Error state - hash array with the following fields: type, message */
|
||||
protected $error;
|
||||
|
||||
|
||||
/**
|
||||
* Returns addressbook name (e.g. for addressbooks listing)
|
||||
* @return string
|
||||
*/
|
||||
abstract function get_name();
|
||||
|
||||
/**
|
||||
* Sets a search filter.
|
||||
*
|
||||
* This affects the contact set considered when using the count() and list_records() operations to those
|
||||
* contacts that match the filter conditions. If no search filter is set, all contacts in the addressbook are
|
||||
* considered.
|
||||
*
|
||||
* This filter mechanism is applied in addition to other filter mechanisms, see the description of the count()
|
||||
* operation.
|
||||
*
|
||||
* @param mixed $filter Search params to use in listing method, obtained by get_search_set()
|
||||
* @return void
|
||||
*/
|
||||
abstract function set_search_set($filter);
|
||||
|
||||
/**
|
||||
* Getter for saved search properties.
|
||||
*
|
||||
* The filter representation is opaque to roundcube, but can be set again using set_search_set().
|
||||
*
|
||||
* @return mixed Search properties used by this class
|
||||
*/
|
||||
abstract function get_search_set();
|
||||
|
||||
/**
|
||||
* Reset saved results and search parameters
|
||||
* @return void
|
||||
*/
|
||||
abstract function reset();
|
||||
|
||||
/**
|
||||
* Refresh saved search set after data has changed
|
||||
*
|
||||
* @return mixed New search set
|
||||
*/
|
||||
function refresh_search()
|
||||
{
|
||||
return $this->get_search_set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the current set of contact records.
|
||||
*
|
||||
* See the description of count() for the criteria determining which contacts are considered for the listing.
|
||||
*
|
||||
* The actual records returned may be fewer, as only the records for the current page are returned. The returned
|
||||
* records may be further limited by the $subset parameter, which means that only the first or last $subset records
|
||||
* of the page are returned, depending on whether $subset is positive or negative. If $subset is 0, all records of
|
||||
* the page are returned. The returned records are found in the $records property of the returned result set.
|
||||
*
|
||||
* Finally, the $first property of the returned result set contains the index into the total set of filtered records
|
||||
* (i.e. not considering the segmentation into pages) of the first returned record before applying the $subset
|
||||
* parameter (i.e., $first is always a multiple of the page size).
|
||||
*
|
||||
* The $nocount parameter is an optimization that allows to skip querying the total amount of records of the
|
||||
* filtered set if the caller is only interested in the records. In this case, the $count property of the returned
|
||||
* result set will simply contain the number of returned records, but the filtered set may contain more records than
|
||||
* this.
|
||||
*
|
||||
* The result of the operation is internally cached for later retrieval using get_result().
|
||||
*
|
||||
* @param ?array $cols List of columns to include in the returned records (null means all)
|
||||
* @param int $subset Only return this number of records of the current page, use negative values for tail
|
||||
* @param bool $nocount True to skip the count query (select only)
|
||||
*
|
||||
* @return rcube_result_set Indexed list of contact records, each a hash array
|
||||
*/
|
||||
abstract function list_records($cols = null, $subset = 0, $nocount = false);
|
||||
|
||||
/**
|
||||
* Search records
|
||||
*
|
||||
* Depending on the given parameters the search() function operates in different ways (in the order listed):
|
||||
*
|
||||
* "Direct ID search" - when $fields is either 'ID' or $this->primary_key
|
||||
* - $values is either a string of contact IDs separated by self::SEPARATOR (,) or an array of contact IDs
|
||||
* - Any contact with one of the given IDs is returned
|
||||
*
|
||||
* "Advanced search" - when $value is an array
|
||||
* - Each value in $values is the search value for the field in $fields at the same index
|
||||
* - All fields must match their value to be included in the result ("AND" semantics)
|
||||
*
|
||||
* "Search all fields" - when $fields is '*' (note: $value is a single string)
|
||||
* - Any field must match the value to be included in the result ("OR" semantics)
|
||||
*
|
||||
* "Search given fields" - if none of the above matches
|
||||
* - Any of the given fields must match the value to be included in the result ("OR" semantics)
|
||||
*
|
||||
* All matching is done case insensitive.
|
||||
*
|
||||
* The search settings are remembered (see set_search_set()) until reset using the reset() function. They can be
|
||||
* retrieved using get_search_set(). The remembered search settings must be considered by list_records() and
|
||||
* count().
|
||||
*
|
||||
* The search mode can be set by the admin via the config.inc.php setting addressbook_search_mode.
|
||||
* It is used as a bit mask, but the search modes are exclusive (SEARCH_GROUPS is combined with one of other modes):
|
||||
* SEARCH_ALL: substring search (*abc*)
|
||||
* SEARCH_STRICT: Exact match search (case insensitive =)
|
||||
* SEARCH_PREFIX: Prefix search (abc*)
|
||||
* SEARCH_GROUPS: include groups in search results (if supported)
|
||||
*
|
||||
* When records are requested in the returned rcube_result_set ($select is true), the results will only include the
|
||||
* contacts of the current page (see list_page, page_size). The behavior is as described with the list_records
|
||||
* function, and search() can be thought of as a sequence of set_search_set() and list_records() under that filter.
|
||||
*
|
||||
* If $nocount is true, the count property of the returned rcube_result_set will contain the amount of records
|
||||
* contained within that set. Calling search() with $select=false and $nocount=true is not a meaningful use case and
|
||||
* will result in an empty result set without records and a count property of 0, which gives no indication on the
|
||||
* actual record set matching the given filter.
|
||||
*
|
||||
* The result of the operation is internally cached for later retrieval using get_result().
|
||||
*
|
||||
* @param string|string[] $fields Field names to search in
|
||||
* @param string|string[] $value Search value, or array of values, one for each field in $fields
|
||||
* @param int $mode Search mode. Sum of rcube_addressbook::SEARCH_*.
|
||||
* @param bool $select True if records are requested in the result, false if count only
|
||||
* @param bool $nocount True to skip the count query (select only)
|
||||
* @param string|string[] $required Field or list of fields that cannot be empty
|
||||
*
|
||||
* @return rcube_result_set Contact records and 'count' value
|
||||
*/
|
||||
abstract function search($fields, $value, $mode = 0, $select = true, $nocount = false, $required = []);
|
||||
|
||||
/**
|
||||
* Count the number of contacts in the database matching the current filter criteria.
|
||||
*
|
||||
* The current filter criteria are defined by the search filter (see search()/set_search_set()) and the currently
|
||||
* active group (see set_group()), if applicable.
|
||||
*
|
||||
* @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 $id Record identifier(s)
|
||||
* @param bool $assoc True to return record as associative array, otherwise a result set is returned
|
||||
*
|
||||
* @return rcube_result_set|array Result object with all record fields
|
||||
*/
|
||||
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. Null if no error set.
|
||||
*/
|
||||
function get_error()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for errors for internal use
|
||||
*
|
||||
* @param int $type Error type (one of this class' error constants)
|
||||
* @param string $message Error message (name of a text label)
|
||||
*/
|
||||
protected function set_error($type, $message)
|
||||
{
|
||||
$this->error = ['type' => $type, 'message' => $message];
|
||||
}
|
||||
|
||||
/**
|
||||
* Close connection to source
|
||||
* Called on script shutdown
|
||||
*/
|
||||
function close() { }
|
||||
|
||||
/**
|
||||
* Set internal list page
|
||||
*
|
||||
* @param int $page Page number to list
|
||||
*/
|
||||
function set_page($page)
|
||||
{
|
||||
$this->list_page = (int) $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set internal page size
|
||||
*
|
||||
* @param int $size Number of messages to display on one page
|
||||
*/
|
||||
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 && (array_key_exists($sort_col, $this->coltypes) || in_array($sort_col, $this->coltypes))) {
|
||||
$this->sort_col = $sort_col;
|
||||
}
|
||||
|
||||
if ($sort_order) {
|
||||
$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 &$save_data Associative array with data to save
|
||||
* @param bool $autofix Attempt to fix/complete record automatically
|
||||
*
|
||||
* @return bool 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(['name' => 'emailformaterror', 'vars' => ['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', [
|
||||
'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 $save_data Associative 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 bool $check 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 rcube_result_set $recset Recordset to insert
|
||||
* @param bool $check True to check for duplicates first
|
||||
*
|
||||
* @return array List of created record IDs
|
||||
*/
|
||||
function insertMultiple($recset, $check = false)
|
||||
{
|
||||
$ids = [];
|
||||
if ($recset instanceof rcube_result_set) {
|
||||
while ($row = $recset->next()) {
|
||||
if ($insert = $this->insert($row, $check)) {
|
||||
$ids[] = $insert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific contact record
|
||||
*
|
||||
* @param mixed $id Record identifier
|
||||
* @param array $save_cols Associative 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 $ids Record identifiers
|
||||
* @param bool $force Remove records irreversible (see self::undelete)
|
||||
*
|
||||
* @return int|false Number of removed records, False on failure
|
||||
*/
|
||||
function delete($ids, $force = true)
|
||||
{
|
||||
// empty for read-only address books
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark delete flag on contact record(s)
|
||||
*
|
||||
* @param array $ids 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/clears the current group.
|
||||
*
|
||||
* This affects the contact set considered when using the count(), list_records() and search() operations to those
|
||||
* contacts that belong to the given group. If no current group is set, all contacts in the addressbook are
|
||||
* considered.
|
||||
*
|
||||
* This filter mechanism is applied in addition to other filter mechanisms, see the description of the count()
|
||||
* operation.
|
||||
*
|
||||
* @param null|int|string $gid Database identifier of the group. Use 0/"0"/null to reset the group filter.
|
||||
*/
|
||||
function set_group($group_id)
|
||||
{
|
||||
// empty for address books don't supporting groups
|
||||
}
|
||||
|
||||
/**
|
||||
* List all active contact groups of this source
|
||||
*
|
||||
* @param ?string $search Optional search string to match group name
|
||||
* @param int $mode Search mode. Sum of self::SEARCH_*
|
||||
*
|
||||
* @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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group properties such as name and email address(es)
|
||||
*
|
||||
* @param string $group_id Group identifier
|
||||
*
|
||||
* @return ?array Group properties as hash array, null in case of error.
|
||||
*/
|
||||
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 $name The group name
|
||||
*
|
||||
* @return array|false 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_id Group identifier
|
||||
*
|
||||
* @return bool True on success, false if no data was changed
|
||||
*/
|
||||
function delete_group($group_id)
|
||||
{
|
||||
// empty for address books don't supporting groups
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a specific contact group
|
||||
*
|
||||
* @param string $group_id Group identifier
|
||||
* @param string $newname New name to set for this group
|
||||
* @param string &$newid New group identifier (if changed, otherwise don't set)
|
||||
*
|
||||
* @return string|false New name on success, false if no data was changed
|
||||
*/
|
||||
function rename_group($group_id, $newname, &$newid)
|
||||
{
|
||||
// empty for address books don't supporting groups
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given contact records the a certain group
|
||||
*
|
||||
* @param string $group_id Group identifier
|
||||
* @param array|string $ids 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_id Group identifier
|
||||
* @param array|string $ids 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 $id Record identifier
|
||||
*
|
||||
* @return array List of assigned groups indexed by a group ID.
|
||||
* Every array element can be just a group name (string), or an array
|
||||
* with 'ID' and 'name' elements.
|
||||
* @since 0.5-beta
|
||||
*/
|
||||
function get_record_groups($id)
|
||||
{
|
||||
// empty for address books don't supporting groups
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to return all values of a certain data column
|
||||
* either as flat list or grouped by subtype
|
||||
*
|
||||
* @param string $col Col name
|
||||
* @param array $data Record data array as used for saving
|
||||
* @param bool $flat 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 = [];
|
||||
foreach ((array) $data as $c => $values) {
|
||||
if ($c === $col || strpos($c, $col.':') === 0) {
|
||||
if ($flat) {
|
||||
$out = array_merge($out, (array) $values);
|
||||
}
|
||||
else {
|
||||
list(, $type) = rcube_utils::explode(':', $c);
|
||||
if ($type !== null && isset($out[$type])) {
|
||||
$out[$type] = array_merge((array) $out[$type], (array) $values);
|
||||
}
|
||||
else {
|
||||
$out[$type] = (array) $values;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
if ($flat && !empty($out)) {
|
||||
$out = array_unique($out);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a valid display name from the given structured contact data
|
||||
*
|
||||
* @param array $contact Hash array with contact data as key-value pairs
|
||||
* @param bool $full_email 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'] ?? '';
|
||||
|
||||
// default display name composition according to vcard standard
|
||||
if (!$fn) {
|
||||
$keys = ['prefix', 'firstname', 'middlename', 'surname', 'suffix'];
|
||||
$fn = implode(' ', array_filter(array_intersect_key($contact, array_flip($keys))));
|
||||
$fn = trim(preg_replace('/\s+/u', ' ', $fn));
|
||||
}
|
||||
|
||||
// use email address part for name
|
||||
$email = self::get_col_values('email', $contact, true);
|
||||
$email = $email[0] ?? null;
|
||||
|
||||
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 contacts.
|
||||
*
|
||||
* @param array $contact 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)) {
|
||||
$compose_mode = (int) rcube::get_instance()->config->get('addressbook_name_listing', 0);
|
||||
}
|
||||
|
||||
$get_names = function ($contact, $fields) {
|
||||
$result = [];
|
||||
foreach ($fields as $field) {
|
||||
if (!empty($contact[$field])) {
|
||||
$result[] = $contact[$field];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
};
|
||||
|
||||
switch ($compose_mode) {
|
||||
case 3:
|
||||
$names = $get_names($contact, ['firstname', 'middlename']);
|
||||
if (!empty($contact['surname'])) {
|
||||
array_unshift($names, $contact['surname'] . ',');
|
||||
}
|
||||
$fn = implode(' ', $names);
|
||||
break;
|
||||
case 2:
|
||||
$keys = ['surname', 'firstname', 'middlename'];
|
||||
$fn = implode(' ', $get_names($contact, $keys));
|
||||
break;
|
||||
case 1:
|
||||
$keys = ['firstname', 'middlename', 'surname'];
|
||||
$fn = implode(' ', $get_names($contact, $keys));
|
||||
break;
|
||||
case 0:
|
||||
if (!empty($contact['name'])) {
|
||||
$fn = $contact['name'];
|
||||
}
|
||||
else {
|
||||
$keys = ['prefix', 'firstname', 'middlename', 'surname', 'suffix'];
|
||||
$fn = implode(' ', $get_names($contact, $keys));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', ['contact' => $contact]);
|
||||
$fn = $plugin['fn'];
|
||||
}
|
||||
|
||||
$fn = trim($fn, ', ');
|
||||
$fn = preg_replace('/\s+/u', ' ', $fn);
|
||||
|
||||
// fallbacks...
|
||||
if ($fn === '') {
|
||||
// ... display name
|
||||
if (isset($contact['name']) && ($name = trim($contact['name']))) {
|
||||
$fn = $name;
|
||||
}
|
||||
// ... organization
|
||||
else if (isset($contact['organization']) && ($org = trim($contact['organization']))) {
|
||||
$fn = $org;
|
||||
}
|
||||
// ... email address
|
||||
else if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
|
||||
$fn = $email[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build contact display name for autocomplete listing
|
||||
*
|
||||
* @param array $contact Hash array with contact data as key-value pairs
|
||||
* @param string $email Optional email address
|
||||
* @param string $name Optional name (self::compose_list_name() result)
|
||||
* @param string $templ 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);
|
||||
|
||||
// If name(s) are undefined compose_list_name() may return an email address
|
||||
// here we prevent from returning the same name and email
|
||||
if ($name === $email && strpos($result, '{email}') !== false) {
|
||||
$value = '';
|
||||
}
|
||||
|
||||
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) && isset($value[0])) {
|
||||
$value = $value[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
$value = '';
|
||||
}
|
||||
|
||||
$result = str_replace('{' . $key . '}', $value, $result);
|
||||
}
|
||||
}
|
||||
|
||||
$result = preg_replace('/\s+/u', ' ', $result);
|
||||
$result = preg_replace('/\s*(<>|\(\)|\[\])/u', '', $result);
|
||||
$result = trim($result, '/ ');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique key for sorting contacts
|
||||
*
|
||||
* @param array $contact Contact record
|
||||
* @param string $sort_col Sorting column name
|
||||
*
|
||||
* @return string Unique key
|
||||
*/
|
||||
public static function compose_contact_key($contact, $sort_col)
|
||||
{
|
||||
$key = isset($contact[$sort_col]) ? $contact[$sort_col] : null;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Make the key really unique (as we e.g. support contacts with no email)
|
||||
$key .= ':' . $contact['sourceid'] . ':' . $contact['ID'];
|
||||
|
||||
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 Comparison 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'));
|
||||
}
|
||||
|
||||
// Gender is a special value, must use strict comparison (#5757)
|
||||
if ($colname == 'gender') {
|
||||
$mode = self::SEARCH_STRICT;
|
||||
}
|
||||
|
||||
// composite field, e.g. address
|
||||
foreach ((array) $value as $val) {
|
||||
$val = mb_strtolower($val);
|
||||
|
||||
if ($mode & self::SEARCH_STRICT) {
|
||||
$got = ($val == $search);
|
||||
}
|
||||
else if ($mode & self::SEARCH_PREFIX) {
|
||||
$got = ($search == substr($val, 0, strlen($search)));
|
||||
}
|
||||
else {
|
||||
$got = (strpos($val, $search) !== false);
|
||||
}
|
||||
|
||||
if ($got) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 collected addresses database |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collected addresses database
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Addressbook
|
||||
*/
|
||||
class rcube_addresses extends rcube_contacts
|
||||
{
|
||||
protected $db_name = 'collected_addresses';
|
||||
protected $type = 0;
|
||||
protected $table_cols = ['name', 'email'];
|
||||
protected $fulltext_cols = ['name'];
|
||||
|
||||
// public properties
|
||||
public $primary_key = 'address_id';
|
||||
public $readonly = true;
|
||||
public $groups = false;
|
||||
public $undelete = false;
|
||||
public $deletable = true;
|
||||
public $coltypes = ['name', 'email'];
|
||||
public $date_cols = [];
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param object $dbconn Instance of the rcube_db class
|
||||
* @param int $user User-ID
|
||||
* @param int $type Type of the address (1 - recipient, 2 - trusted sender)
|
||||
*/
|
||||
public function __construct($dbconn, $user, $type)
|
||||
{
|
||||
$this->db = $dbconn;
|
||||
$this->user_id = $user;
|
||||
$this->type = $type;
|
||||
$this->ready = $this->db && !$this->db->is_error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns addressbook name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name()
|
||||
{
|
||||
if ($this->type == self::TYPE_RECIPIENT) {
|
||||
return rcube::get_instance()->gettext('collectedrecipients');
|
||||
}
|
||||
|
||||
if ($this->type == self::TYPE_TRUSTED_SENDER) {
|
||||
return rcube::get_instance()->gettext('trustedsenders');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* List the current set of contact records
|
||||
*
|
||||
* @param array $cols List of cols to show, Null means all
|
||||
* @param int $subset Only return this number of records, use negative values for tail
|
||||
* @param bool $nocount True to skip the count query (select only)
|
||||
*
|
||||
* @return array Indexed list of contact records, each a hash array
|
||||
*/
|
||||
public 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;
|
||||
|
||||
$sql_result = $this->db->limitquery(
|
||||
"SELECT * FROM " . $this->db->table_name($this->db_name, true)
|
||||
. " WHERE `user_id` = ? AND `type` = ?"
|
||||
. ($this->filter ? " AND ".$this->filter : "")
|
||||
. " ORDER BY `name` " . $this->sort_order . ", `email` " . $this->sort_order,
|
||||
$start_row,
|
||||
$length,
|
||||
$this->user_id,
|
||||
$this->type
|
||||
);
|
||||
|
||||
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
|
||||
$sql_arr['ID'] = $sql_arr[$this->primary_key];
|
||||
$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 or array of field names to search in
|
||||
* @param mixed $value Search value (or array of values when $fields is array)
|
||||
* @param int $mode Search mode. Sum of rcube_addressbook::SEARCH_*
|
||||
* @param bool $select True if results are requested, False if count only
|
||||
* @param bool $nocount True to skip the count query (select only)
|
||||
* @param array $required List of fields that cannot be empty
|
||||
*
|
||||
* @return rcube_result_set Contact records and 'count' value
|
||||
*/
|
||||
public function search($fields, $value, $mode = 0, $select = true, $nocount = false, $required = [])
|
||||
{
|
||||
if (!is_array($required) && !empty($required)) {
|
||||
$required = [$required];
|
||||
}
|
||||
|
||||
$where = $post_search = [];
|
||||
$mode = intval($mode);
|
||||
|
||||
// direct ID search
|
||||
if ($fields == 'ID' || $fields == $this->primary_key) {
|
||||
$ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value;
|
||||
$ids = $this->db->array2list($ids, 'integer');
|
||||
$where[] = $this->primary_key . ' IN (' . $ids . ')';
|
||||
}
|
||||
else if (is_array($value)) {
|
||||
foreach ((array) $fields as $idx => $col) {
|
||||
$val = $value[$idx];
|
||||
|
||||
if (!strlen($val)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// table column
|
||||
if ($col == 'email' && ($mode & rcube_addressbook::SEARCH_STRICT)) {
|
||||
$where[] = $this->db->ilike($col, $val);
|
||||
}
|
||||
else if (in_array($col, $this->table_cols)) {
|
||||
$where[] = $this->fulltext_sql_where($val, $mode, $col);
|
||||
}
|
||||
else {
|
||||
$where[] = '1 = 0'; // unsupported column
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// fulltext search in all fields
|
||||
if ($fields == '*') {
|
||||
$fields = ['name', 'email'];
|
||||
}
|
||||
|
||||
// require each word in to be present in one of the fields
|
||||
$words = ($mode & rcube_addressbook::SEARCH_STRICT) ? [$value] : rcube_utils::tokenize_string($value, 1);
|
||||
foreach ($words as $word) {
|
||||
$groups = [];
|
||||
foreach ((array) $fields as $idx => $col) {
|
||||
if ($col == 'email' && ($mode & rcube_addressbook::SEARCH_STRICT)) {
|
||||
$groups[] = $this->db->ilike($col, $word);
|
||||
}
|
||||
else if (in_array($col, $this->table_cols)) {
|
||||
$groups[] = $this->fulltext_sql_where($word, $mode, $col);
|
||||
}
|
||||
}
|
||||
$where[] = '(' . implode(' OR ', $groups) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_intersect($required, $this->table_cols) as $col) {
|
||||
$where[] = $this->db->quote_identifier($col) . ' <> ' . $this->db->quote('');
|
||||
}
|
||||
|
||||
if (!empty($where)) {
|
||||
// use AND operator for advanced searches
|
||||
$where = implode(' AND ', $where);
|
||||
|
||||
$this->set_search_set($where);
|
||||
|
||||
if ($select) {
|
||||
$this->list_records(null, 0, $nocount);
|
||||
}
|
||||
else {
|
||||
$this->result = $this->count();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->result = new rcube_result_set();
|
||||
}
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count number of available contacts in database
|
||||
*
|
||||
* @return int Contacts count
|
||||
*/
|
||||
protected function _count()
|
||||
{
|
||||
// count contacts for this user
|
||||
$sql_result = $this->db->query(
|
||||
"SELECT COUNT(`address_id`) AS cnt"
|
||||
. " FROM " . $this->db->table_name($this->db_name, true)
|
||||
. " WHERE `user_id` = ? AND `type` = ?"
|
||||
. ($this->filter ? " AND (" . $this->filter . ")" : ""),
|
||||
$this->user_id,
|
||||
$this->type
|
||||
);
|
||||
|
||||
$sql_arr = $this->db->fetch_assoc($sql_result);
|
||||
|
||||
$this->cache['count'] = (int) $sql_arr['cnt'];
|
||||
|
||||
return $this->cache['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific contact record
|
||||
*
|
||||
* @param mixed $id Record identifier(s)
|
||||
* @param bool $assoc Enables returning associative array
|
||||
*
|
||||
* @return rcube_result_set|array Result object with all record fields
|
||||
*/
|
||||
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, true)
|
||||
. " WHERE `address_id` = ? AND `user_id` = ?",
|
||||
$id,
|
||||
$this->user_id
|
||||
);
|
||||
|
||||
$this->result = null;
|
||||
|
||||
if ($record = $this->db->fetch_assoc()) {
|
||||
$record['ID'] = $record['address_id'];
|
||||
$this->result = new rcube_result_set(1);
|
||||
$this->result->add($record);
|
||||
}
|
||||
|
||||
return $assoc && !empty($record) ? $record : $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the given data before saving.
|
||||
* If input not valid, the message to display can be fetched using get_error()
|
||||
*
|
||||
* @param array &$save_data Associative array with data to save
|
||||
* @param bool $autofix Try to fix/complete record automatically
|
||||
*
|
||||
* @return bool True if input is valid, False if not.
|
||||
*/
|
||||
public function validate(&$save_data, $autofix = false)
|
||||
{
|
||||
$email = array_filter($this->get_col_values('email', $save_data, true));
|
||||
|
||||
// require email
|
||||
if (empty($email) || count($email) > 1) {
|
||||
$this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = $email[0];
|
||||
|
||||
// check validity of the email address
|
||||
if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
|
||||
$rcube = rcube::get_instance();
|
||||
$error = $rcube->gettext(['name' => 'emailformaterror', 'vars' => ['email' => $email]]);
|
||||
$this->set_error(self::ERROR_VALIDATE, $error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new contact record
|
||||
*
|
||||
* @param array $save_data Associative array with save data
|
||||
* @param bool $check Enables validity checks
|
||||
*
|
||||
* @return int|bool The created record ID on success, False on error
|
||||
*/
|
||||
function insert($save_data, $check = false)
|
||||
{
|
||||
if (!is_array($save_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($check && ($existing = $this->search('email', $save_data['email'], false, false))) {
|
||||
if ($existing->count) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache = null;
|
||||
|
||||
$this->db->query(
|
||||
"INSERT INTO " . $this->db->table_name($this->db_name, true)
|
||||
. " (`user_id`, `changed`, `type`, `name`, `email`)"
|
||||
. " VALUES (?, " . $this->db->now() . ", ?, ?, ?)",
|
||||
$this->user_id,
|
||||
$this->type,
|
||||
$save_data['name'],
|
||||
$save_data['email']
|
||||
);
|
||||
|
||||
return $this->db->insert_id($this->db_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific contact record
|
||||
*
|
||||
* @param mixed $id Record identifier
|
||||
* @param array $save_cols Associative array with save data
|
||||
*
|
||||
* @return bool True on success, False on error
|
||||
*/
|
||||
function update($id, $save_cols)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one or more contact records
|
||||
*
|
||||
* @param array $ids Record identifiers
|
||||
* @param bool $force Remove record(s) irreversible (unsupported)
|
||||
*
|
||||
* @return int|false Number of removed records
|
||||
*/
|
||||
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(
|
||||
"DELETE FROM " . $this->db->table_name($this->db_name, true)
|
||||
. " WHERE `user_id` = ? AND `type` = ? AND `address_id` IN ($ids)",
|
||||
$this->user_id, $this->type
|
||||
);
|
||||
|
||||
$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->db->query("DELETE FROM " . $this->db->table_name($this->db_name, true)
|
||||
. " WHERE `user_id` = ? AND `type` = ?",
|
||||
$this->user_id, $this->type
|
||||
);
|
||||
|
||||
$this->cache = null;
|
||||
|
||||
return $this->db->affected_rows();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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: |
|
||||
| Provide basic functions for base URL replacement |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper class to turn relative urls into absolute ones
|
||||
* using a predefined base
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_base_replacer
|
||||
{
|
||||
private $base_url;
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $base Base URL
|
||||
*/
|
||||
public function __construct($base)
|
||||
{
|
||||
$this->base_url = $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace callback
|
||||
*
|
||||
* @param array $matches Matching entries
|
||||
*
|
||||
* @return string Replaced text with absolute URL
|
||||
*/
|
||||
public function callback($matches)
|
||||
{
|
||||
return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert base URLs to absolute ones
|
||||
*
|
||||
* @param string $body Text body
|
||||
*
|
||||
* @return string Replaced text
|
||||
*/
|
||||
public function replace($body)
|
||||
{
|
||||
$regexp = [
|
||||
'/(src|background|href)=(["\']?)([^"\'\s>]+)(\2|\s|>)/i',
|
||||
'/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/i',
|
||||
];
|
||||
|
||||
return preg_replace_callback($regexp, [$this, 'callback'], $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert paths like ../xxx to an absolute path using a base url
|
||||
*
|
||||
* @param string $path Relative path
|
||||
* @param string $base_url Base URL
|
||||
*
|
||||
* @return string Absolute URL
|
||||
*/
|
||||
public static function absolute_url($path, $base_url)
|
||||
{
|
||||
// check if path is an absolute URL
|
||||
if (preg_match('/^[fhtps]+:\/\//', $path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
// check if path is a content-id scheme
|
||||
if (strpos($path, 'cid:') === 0) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$host_url = $base_url;
|
||||
$abs_path = $path;
|
||||
|
||||
// cut base_url to the last directory
|
||||
if (strrpos($base_url, '/') > 7) {
|
||||
$host_url = substr($base_url, 0, strpos($base_url, '/', 7));
|
||||
$base_url = substr($base_url, 0, strrpos($base_url, '/'));
|
||||
}
|
||||
|
||||
// $path is absolute
|
||||
if ($path && $path[0] == '/') {
|
||||
$abs_path = $host_url . $path;
|
||||
}
|
||||
else {
|
||||
// strip './' because its the same as ''
|
||||
$path = preg_replace('/^\.\//', '', $path);
|
||||
|
||||
if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
|
||||
$cnt = count($matches);
|
||||
while ($cnt--) {
|
||||
if ($pos = strrpos($base_url, '/')) {
|
||||
$base_url = substr($base_url, 0, $pos);
|
||||
}
|
||||
$path = substr($path, 3);
|
||||
}
|
||||
}
|
||||
|
||||
$abs_path = $base_url.'/'.$path;
|
||||
}
|
||||
|
||||
return $abs_path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Class representing the client browser's properties |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provide details about the client's browser based on the User-Agent header
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_browser
|
||||
{
|
||||
/** @var float $ver Browser version */
|
||||
public $ver = 0;
|
||||
|
||||
/** @var bool $win Browser OS is Windows */
|
||||
public $win = false;
|
||||
|
||||
/** @var bool $mac Browser OS is Mac */
|
||||
public $mac = false;
|
||||
|
||||
/** @var bool $linux Browser OS is Linux */
|
||||
public $linux = false;
|
||||
|
||||
/** @var bool $unix Browser OS is Unix */
|
||||
public $unix = false;
|
||||
|
||||
/** @var bool $webkit Browser uses WebKit engine */
|
||||
public $webkit = false;
|
||||
|
||||
/** @var bool $opera Browser is Opera */
|
||||
public $opera = false;
|
||||
|
||||
/** @var bool $chrome Browser is Chrome */
|
||||
public $chrome = false;
|
||||
|
||||
/** @var bool $ie Browser is Internet Explorer */
|
||||
public $ie = false;
|
||||
|
||||
/** @var bool $edge Browser is Edge */
|
||||
public $edge = false;
|
||||
|
||||
/** @var bool $safari Browser is Safari */
|
||||
public $safari = false;
|
||||
|
||||
/** @var bool $mz Browser is Mozilla Firefox */
|
||||
public $mz = false;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$HTTP_USER_AGENT = !empty($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '';
|
||||
|
||||
// Operating system detection
|
||||
$this->win = strpos($HTTP_USER_AGENT, 'win') != false;
|
||||
$this->mac = strpos($HTTP_USER_AGENT, 'mac') != false;
|
||||
$this->linux = strpos($HTTP_USER_AGENT, 'linux') != false;
|
||||
$this->unix = strpos($HTTP_USER_AGENT, 'unix') != false;
|
||||
|
||||
// Engine detection
|
||||
$this->webkit = strpos($HTTP_USER_AGENT, 'applewebkit') !== false;
|
||||
$this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false || ($this->webkit && strpos($HTTP_USER_AGENT, 'opr/') !== false);
|
||||
$this->edge = strpos($HTTP_USER_AGENT, 'edge/') !== false;
|
||||
$this->ie = !$this->opera && !$this->edge && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false);
|
||||
$this->chrome = !$this->opera && !$this->edge && strpos($HTTP_USER_AGENT, 'chrome') !== false;
|
||||
$this->safari = !$this->opera && !$this->chrome && !$this->edge
|
||||
&& ($this->webkit || strpos($HTTP_USER_AGENT, 'safari') !== false);
|
||||
$this->mz = !$this->ie && !$this->edge && !$this->safari && !$this->chrome && !$this->opera
|
||||
&& strpos($HTTP_USER_AGENT, 'mozilla') !== false;
|
||||
|
||||
// Version detection
|
||||
if ($this->edge && preg_match('/edge\/([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
|
||||
$this->ver = (float) $regs[1];
|
||||
}
|
||||
else if ($this->opera && preg_match('/(opera|opr)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
|
||||
$this->ver = (float) $regs[3];
|
||||
}
|
||||
else if ($this->safari && preg_match('/(version|safari)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
|
||||
$this->ver = (float) $regs[1];
|
||||
}
|
||||
else if (preg_match('/(chrome|khtml|version|msie|rv:)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
|
||||
$this->ver = (float) $regs[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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
|
||||
*/
|
||||
class rcube_cache
|
||||
{
|
||||
protected $type;
|
||||
protected $userid;
|
||||
protected $prefix;
|
||||
protected $ttl;
|
||||
protected $packed;
|
||||
protected $indexed;
|
||||
protected $index;
|
||||
protected $index_update;
|
||||
protected $cache = [];
|
||||
protected $updates = [];
|
||||
protected $exp_records = [];
|
||||
protected $refresh_time = 0.5; // how often to refresh/save the index and cache entries
|
||||
protected $debug = false;
|
||||
protected $max_packet = -1;
|
||||
|
||||
const MAX_EXP_LEVEL = 2;
|
||||
const DATE_FORMAT = 'Y-m-d H:i:s.u';
|
||||
const DATE_FORMAT_REGEX = '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{1,6}';
|
||||
|
||||
|
||||
/**
|
||||
* Object factory
|
||||
*
|
||||
* @param string $type Engine type ('db', 'memcache', 'apc', 'redis')
|
||||
* @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
|
||||
* @param bool $indexed Use indexed cache. Indexed cache is more appropriate for
|
||||
* storing big data with possibility to remove it by a key prefix.
|
||||
* Non-indexed cache does not remove data, but flags it for expiration,
|
||||
* also stores it in memory until close() method is called.
|
||||
*
|
||||
* @param rcube_cache Cache object
|
||||
*/
|
||||
public static function factory($type, $userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
$driver = strtolower($type) ?: 'db';
|
||||
$class = "rcube_cache_$driver";
|
||||
|
||||
if (!$driver || !class_exists($class)) {
|
||||
rcube::raise_error([
|
||||
'code' => 600, 'type' => 'db',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Configuration error. Unsupported cache driver: $driver"
|
||||
],
|
||||
true, true
|
||||
);
|
||||
}
|
||||
|
||||
return new $class($userid, $prefix, $ttl, $packed, $indexed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Object constructor.
|
||||
*
|
||||
* @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
|
||||
* @param bool $indexed Use indexed cache. Indexed cache is more appropriate for
|
||||
* storing big data with possibility to remove it by key prefix.
|
||||
* Non-indexed cache does not remove data, but flags it for expiration,
|
||||
* also stores it in memory until close() method is called.
|
||||
*/
|
||||
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true, $indexed = false)
|
||||
{
|
||||
$this->userid = (int) $userid;
|
||||
$this->ttl = min(get_offset_sec($ttl), 2592000);
|
||||
$this->prefix = $prefix;
|
||||
$this->packed = $packed;
|
||||
$this->indexed = $indexed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cached value.
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
public function get($key)
|
||||
{
|
||||
if (array_key_exists($key, $this->cache)) {
|
||||
return $this->cache[$key];
|
||||
}
|
||||
|
||||
return $this->read_record($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets (add/update) value in cache.
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
* @param mixed $data Cache data
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function set($key, $data)
|
||||
{
|
||||
return $this->write_record($key, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use self::get()
|
||||
*/
|
||||
public function read($key)
|
||||
{
|
||||
return $this->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use self::set()
|
||||
*/
|
||||
public function write($key, $data)
|
||||
{
|
||||
return $this->set($key, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache.
|
||||
*
|
||||
* @param string $key Cache key name or pattern
|
||||
* @param bool $prefix_mode Enable it to clear all keys starting
|
||||
* with prefix specified in $key
|
||||
*/
|
||||
public function remove($key = null, $prefix_mode = false)
|
||||
{
|
||||
// Remove record(s) from the backend
|
||||
$this->remove_record($key, $prefix_mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache records older than ttl
|
||||
*/
|
||||
public function expunge()
|
||||
{
|
||||
// to be overwritten by engine class
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired records of all caches
|
||||
*/
|
||||
public static function gc()
|
||||
{
|
||||
// Only DB cache requires an action to remove expired entries
|
||||
rcube_cache_db::gc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the cache back to the DB.
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->write_index(true);
|
||||
$this->index = null;
|
||||
$this->cache = [];
|
||||
$this->updates = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to build cache key for specified parameters.
|
||||
*
|
||||
* @param string $prefix Key prefix (Max. length 64 characters)
|
||||
* @param array $params Additional parameters
|
||||
*
|
||||
* @return string Key name
|
||||
*/
|
||||
public static function key_name($prefix, $params = [])
|
||||
{
|
||||
$cache_key = $prefix;
|
||||
|
||||
if (!empty($params)) {
|
||||
$func = function($v) {
|
||||
if (is_array($v)) {
|
||||
sort($v);
|
||||
}
|
||||
return is_string($v) ? $v : serialize($v);
|
||||
};
|
||||
|
||||
$params = array_map($func, $params);
|
||||
$cache_key .= '.' . md5(implode(':', $params));
|
||||
}
|
||||
|
||||
return $cache_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cache entry.
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function read_record($key)
|
||||
{
|
||||
$this->load_index();
|
||||
|
||||
// Consistency check (#1490390)
|
||||
if (is_array($this->index) && !in_array($key, $this->index)) {
|
||||
// we always check if the key exist in the index
|
||||
// to have data in consistent state. Keeping the index consistent
|
||||
// is needed for keys delete operation when we delete all keys or by prefix.
|
||||
return;
|
||||
}
|
||||
|
||||
$ckey = $this->ckey($key);
|
||||
$data = $this->get_item($ckey);
|
||||
|
||||
if ($this->indexed) {
|
||||
return $data !== false ? $this->unserialize($data) : null;
|
||||
}
|
||||
|
||||
if ($data !== false) {
|
||||
$timestamp = 0;
|
||||
$utc = new DateTimeZone('UTC');
|
||||
|
||||
// Extract timestamp from the data entry
|
||||
if (preg_match('/^(' . self::DATE_FORMAT_REGEX . '):/', $data, $matches)) {
|
||||
try {
|
||||
$timestamp = new DateTime($matches[1], $utc);
|
||||
$data = substr($data, strlen($matches[1]) + 1);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// invalid date = no timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the entry is still valid by comparing with EXP timestamps
|
||||
// For example for key 'mailboxes.123456789' we check entries:
|
||||
// 'EXP:*', 'EXP:mailboxes' and 'EXP:mailboxes.123456789'.
|
||||
if ($timestamp) {
|
||||
$path = explode('.', "*.$key");
|
||||
$path_len = min(self::MAX_EXP_LEVEL + 1, count($path));
|
||||
|
||||
for ($x = 1; $x <= $path_len; $x++) {
|
||||
$prefix = implode('.', array_slice($path, 0, $x));
|
||||
if ($x > 1) {
|
||||
$prefix = substr($prefix, 2); // remove "*." prefix
|
||||
}
|
||||
|
||||
if (($ts = $this->get_exp_timestamp($prefix)) && $ts > $timestamp) {
|
||||
$timestamp = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = $timestamp ? $this->unserialize($data) : null;
|
||||
}
|
||||
else {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
return $this->cache[$key] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes single cache record into DB.
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
* @param mixed $data Serialized cache data
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
protected function write_record($key, $data)
|
||||
{
|
||||
if ($this->indexed) {
|
||||
$result = $this->store_record($key, $data);
|
||||
|
||||
if ($result) {
|
||||
$this->load_index();
|
||||
$this->index[] = $key;
|
||||
|
||||
if (!$this->index_update) {
|
||||
$this->index_update = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// In this mode we do not save the entry to the database immediately
|
||||
// It's because we have cases where the same entry is updated
|
||||
// multiple times in one request (e.g. 'messagecount' entry rcube_imap).
|
||||
$this->updates[$key] = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$this->cache[$key] = $data;
|
||||
$result = true;
|
||||
}
|
||||
|
||||
$this->write_index();
|
||||
|
||||
return $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
|
||||
*/
|
||||
protected function remove_record($key = null, $prefix_mode = false)
|
||||
{
|
||||
if ($this->indexed) {
|
||||
return $this->remove_record_indexed($key, $prefix_mode);
|
||||
}
|
||||
|
||||
// "Remove" all keys
|
||||
if ($key === null) {
|
||||
$ts = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$this->add_item($this->ekey('*'), $ts->format(self::DATE_FORMAT));
|
||||
$this->cache = [];
|
||||
}
|
||||
// "Remove" keys by name prefix
|
||||
else if ($prefix_mode) {
|
||||
$ts = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$prefix = implode('.', array_slice(explode('.', trim($key, '. ')), 0, self::MAX_EXP_LEVEL));
|
||||
|
||||
$this->add_item($this->ekey($prefix), $ts->format(self::DATE_FORMAT));
|
||||
|
||||
foreach (array_keys($this->cache) as $k) {
|
||||
if (strpos($k, $key) === 0) {
|
||||
$this->cache[$k] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove one key by name
|
||||
else {
|
||||
$this->delete_item($this->ckey($key));
|
||||
$this->cache[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see self::remove_record()
|
||||
*/
|
||||
protected function remove_record_indexed($key = null, $prefix_mode = false)
|
||||
{
|
||||
$this->load_index();
|
||||
|
||||
// Remove all keys
|
||||
if ($key === null) {
|
||||
foreach ($this->index as $key) {
|
||||
$this->delete_item($this->ckey($key));
|
||||
if (!$this->index_update) {
|
||||
$this->index_update = time();
|
||||
}
|
||||
}
|
||||
|
||||
$this->index = [];
|
||||
}
|
||||
// Remove keys by name prefix
|
||||
else if ($prefix_mode) {
|
||||
foreach ($this->index as $idx => $k) {
|
||||
if (strpos($k, $key) === 0) {
|
||||
$this->delete_item($this->ckey($k));
|
||||
unset($this->index[$idx]);
|
||||
if (!$this->index_update) {
|
||||
$this->index_update = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove one key by name
|
||||
else {
|
||||
$this->delete_item($this->ckey($key));
|
||||
if (($idx = array_search($key, $this->index)) !== false) {
|
||||
unset($this->index[$idx]);
|
||||
if (!$this->index_update) {
|
||||
$this->index_update = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->write_index();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the index entry as well as updated entries into memcache/apc/redis DB.
|
||||
*/
|
||||
protected function write_index($force = null)
|
||||
{
|
||||
// Write updated/new entries when needed
|
||||
if (!$this->indexed) {
|
||||
$need_update = $force === true;
|
||||
|
||||
if (!$need_update && !empty($this->updates)) {
|
||||
$now = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$need_update = floatval(min($this->updates)->format('U.u')) < floatval($now->format('U.u')) - $this->refresh_time;
|
||||
}
|
||||
|
||||
if ($need_update) {
|
||||
foreach ($this->updates as $key => $ts) {
|
||||
if (isset($this->cache[$key])) {
|
||||
$this->store_record($key, $this->cache[$key], $ts);
|
||||
}
|
||||
}
|
||||
|
||||
$this->updates = [];
|
||||
}
|
||||
}
|
||||
// Write index entry when needed
|
||||
else {
|
||||
$need_update = $this->index_update && $this->index !== null
|
||||
&& ($force === true || $this->index_update > time() - $this->refresh_time);
|
||||
|
||||
if ($need_update) {
|
||||
$index = serialize(array_values(array_unique($this->index)));
|
||||
|
||||
$this->add_item($this->ikey(), $index);
|
||||
$this->index_update = null;
|
||||
$this->index = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index entry from memcache/apc/redis DB.
|
||||
*/
|
||||
protected function load_index()
|
||||
{
|
||||
if (!$this->indexed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->index !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->get_item($this->ikey());
|
||||
$this->index = $data ? unserialize($data) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data entry into cache
|
||||
*/
|
||||
protected function store_record($key, $data, $ts = null)
|
||||
{
|
||||
$value = $this->serialize($data);
|
||||
|
||||
if (!$this->indexed) {
|
||||
if (!$ts) {
|
||||
$ts = new DateTime('now', new DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
$value = $ts->format(self::DATE_FORMAT) . ':' . $value;
|
||||
}
|
||||
|
||||
$size = strlen($value);
|
||||
|
||||
// don't attempt to write too big data sets
|
||||
if ($size > $this->max_packet_size()) {
|
||||
trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write $size bytes", E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->add_item($this->ckey($key), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches cache entry.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @return mixed Cached value
|
||||
*/
|
||||
protected function get_item($key)
|
||||
{
|
||||
// to be overwritten by engine class
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entry into memcache/apc/redis DB.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
* @param mixed $data Serialized cache data
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function add_item($key, $data)
|
||||
{
|
||||
// to be overwritten by engine class
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes entry from memcache/apc/redis DB.
|
||||
*
|
||||
* @param string $key Cache internal key name
|
||||
*
|
||||
* @param bool True on success, False on failure
|
||||
*/
|
||||
protected function delete_item($key)
|
||||
{
|
||||
// to be overwritten by engine class
|
||||
}
|
||||
|
||||
/**
|
||||
* Get EXP:<key> record value from cache
|
||||
*/
|
||||
protected function get_exp_timestamp($key)
|
||||
{
|
||||
if (!array_key_exists($key, $this->exp_records)) {
|
||||
$data = $this->get_item($this->ekey($key));
|
||||
|
||||
$this->exp_records[$key] = $data ? new DateTime($data, new DateTimeZone('UTC')) : null;
|
||||
}
|
||||
|
||||
return $this->exp_records[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates per-user index cache key name (for memcache, apc, redis)
|
||||
*
|
||||
* @return string Cache key
|
||||
*/
|
||||
protected function ikey()
|
||||
{
|
||||
$key = $this->prefix . 'INDEX';
|
||||
|
||||
if ($this->userid) {
|
||||
$key = $this->userid . ':' . $key;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates per-user cache key name (for memcache, apc, redis)
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
*
|
||||
* @return string Cache key
|
||||
*/
|
||||
protected function ckey($key)
|
||||
{
|
||||
$key = $this->prefix . ':' . $key;
|
||||
|
||||
if ($this->userid) {
|
||||
$key = $this->userid . ':' . $key;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates per-user cache key name for expiration time entry
|
||||
*
|
||||
* @param string $key Cache key name
|
||||
*
|
||||
* @return string Cache key
|
||||
*/
|
||||
protected function ekey($key, $prefix = null)
|
||||
{
|
||||
$key = $this->prefix . 'EXP:' . $key;
|
||||
|
||||
if ($this->userid) {
|
||||
$key = $this->userid . ':' . $key;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes data for storing
|
||||
*/
|
||||
protected function serialize($data)
|
||||
{
|
||||
return $this->packed ? serialize($data) : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserializes serialized data
|
||||
*/
|
||||
protected function unserialize($data)
|
||||
{
|
||||
return $this->packed ? @unserialize($data) : $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the maximum size for cache data to be written
|
||||
*/
|
||||
protected function max_packet_size()
|
||||
{
|
||||
if ($this->max_packet < 0) {
|
||||
$config = rcube::get_instance()->config;
|
||||
$max_packet = $config->get($this->type . '_max_allowed_packet');
|
||||
$this->max_packet = parse_bytes($max_packet) ?: 2097152; // default/max is 2 MB
|
||||
}
|
||||
|
||||
return $this->max_packet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write memcache/apc/redis debug info to the log
|
||||
*/
|
||||
protected function debug($type, $key, $data = null, $result = null)
|
||||
{
|
||||
$line = strtoupper($type) . ' ' . $key;
|
||||
|
||||
if ($data !== null) {
|
||||
$line .= ' ' . ($this->packed ? $data : serialize($data));
|
||||
}
|
||||
|
||||
rcube::debug($this->type, $line, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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> |
|
||||
| Author: Edmund Grimley Evans <edmundo@rano.org> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Character sets conversion functionality
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_charset
|
||||
{
|
||||
/**
|
||||
* Character set aliases (some of them from HTML5 spec.)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
static public $aliases = [
|
||||
'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',
|
||||
'WINDOWS31J' => 'CP932',
|
||||
'WINDOWS949' => 'UHC',
|
||||
'WINDOWS1257' => 'ISO-8859-13',
|
||||
'ISO2022JP' => 'ISO-2022-JP-MS',
|
||||
];
|
||||
|
||||
/**
|
||||
* Windows codepages
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
static public $windows_codepages = [
|
||||
37 => 'IBM037', // IBM EBCDIC US-Canada
|
||||
437 => 'IBM437', // OEM United States
|
||||
500 => 'IBM500', // IBM EBCDIC International
|
||||
708 => 'ASMO-708', // Arabic (ASMO 708)
|
||||
720 => 'DOS-720', // Arabic (Transparent ASMO); Arabic (DOS)
|
||||
737 => 'IBM737', // OEM Greek (formerly 437G); Greek (DOS)
|
||||
775 => 'IBM775', // OEM Baltic; Baltic (DOS)
|
||||
850 => 'IBM850', // OEM Multilingual Latin 1; Western European (DOS)
|
||||
852 => 'IBM852', // OEM Latin 2; Central European (DOS)
|
||||
855 => 'IBM855', // OEM Cyrillic (primarily Russian)
|
||||
857 => 'IBM857', // OEM Turkish; Turkish (DOS)
|
||||
858 => 'IBM00858', // OEM Multilingual Latin 1 + Euro symbol
|
||||
860 => 'IBM860', // OEM Portuguese; Portuguese (DOS)
|
||||
861 => 'IBM861', // OEM Icelandic; Icelandic (DOS)
|
||||
862 => 'DOS-862', // OEM Hebrew; Hebrew (DOS)
|
||||
863 => 'IBM863', // OEM French Canadian; French Canadian (DOS)
|
||||
864 => 'IBM864', // OEM Arabic; Arabic (864)
|
||||
865 => 'IBM865', // OEM Nordic; Nordic (DOS)
|
||||
866 => 'cp866', // OEM Russian; Cyrillic (DOS)
|
||||
869 => 'IBM869', // OEM Modern Greek; Greek, Modern (DOS)
|
||||
870 => 'IBM870', // IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2
|
||||
874 => 'windows-874', // ANSI/OEM Thai (ISO 8859-11); Thai (Windows)
|
||||
875 => 'cp875', // IBM EBCDIC Greek Modern
|
||||
932 => 'shift_jis', // ANSI/OEM Japanese; Japanese (Shift-JIS)
|
||||
936 => 'gb2312', // ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
|
||||
950 => 'big5', // ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)
|
||||
1026 => 'IBM1026', // IBM EBCDIC Turkish (Latin 5)
|
||||
1047 => 'IBM01047', // IBM EBCDIC Latin 1/Open System
|
||||
1140 => 'IBM01140', // IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)
|
||||
1141 => 'IBM01141', // IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)
|
||||
1142 => 'IBM01142', // IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)
|
||||
1143 => 'IBM01143', // IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)
|
||||
1144 => 'IBM01144', // IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)
|
||||
1145 => 'IBM01145', // IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)
|
||||
1146 => 'IBM01146', // IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)
|
||||
1147 => 'IBM01147', // IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)
|
||||
1148 => 'IBM01148', // IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)
|
||||
1149 => 'IBM01149', // IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)
|
||||
1200 => 'UTF-16', // Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
|
||||
1201 => 'UTF-16BE', // Unicode UTF-16, big endian byte order; available only to managed applications
|
||||
1250 => 'windows-1250', // ANSI Central European; Central European (Windows)
|
||||
1251 => 'windows-1251', // ANSI Cyrillic; Cyrillic (Windows)
|
||||
1252 => 'windows-1252', // ANSI Latin 1; Western European (Windows)
|
||||
1253 => 'windows-1253', // ANSI Greek; Greek (Windows)
|
||||
1254 => 'windows-1254', // ANSI Turkish; Turkish (Windows)
|
||||
1255 => 'windows-1255', // ANSI Hebrew; Hebrew (Windows)
|
||||
1256 => 'windows-1256', // ANSI Arabic; Arabic (Windows)
|
||||
1257 => 'windows-1257', // ANSI Baltic; Baltic (Windows)
|
||||
1258 => 'windows-1258', // ANSI/OEM Vietnamese; Vietnamese (Windows)
|
||||
10000 => 'macintosh', // MAC Roman; Western European (Mac)
|
||||
12000 => 'UTF-32', // Unicode UTF-32, little endian byte order; available only to managed applications
|
||||
12001 => 'UTF-32BE', // Unicode UTF-32, big endian byte order; available only to managed applications
|
||||
20127 => 'US-ASCII', // US-ASCII (7-bit)
|
||||
20273 => 'IBM273', // IBM EBCDIC Germany
|
||||
20277 => 'IBM277', // IBM EBCDIC Denmark-Norway
|
||||
20278 => 'IBM278', // IBM EBCDIC Finland-Sweden
|
||||
20280 => 'IBM280', // IBM EBCDIC Italy
|
||||
20284 => 'IBM284', // IBM EBCDIC Latin America-Spain
|
||||
20285 => 'IBM285', // IBM EBCDIC United Kingdom
|
||||
20290 => 'IBM290', // IBM EBCDIC Japanese Katakana Extended
|
||||
20297 => 'IBM297', // IBM EBCDIC France
|
||||
20420 => 'IBM420', // IBM EBCDIC Arabic
|
||||
20423 => 'IBM423', // IBM EBCDIC Greek
|
||||
20424 => 'IBM424', // IBM EBCDIC Hebrew
|
||||
20838 => 'IBM-Thai', // IBM EBCDIC Thai
|
||||
20866 => 'koi8-r', // Russian (KOI8-R); Cyrillic (KOI8-R)
|
||||
20871 => 'IBM871', // IBM EBCDIC Icelandic
|
||||
20880 => 'IBM880', // IBM EBCDIC Cyrillic Russian
|
||||
20905 => 'IBM905', // IBM EBCDIC Turkish
|
||||
20924 => 'IBM00924', // IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
|
||||
20932 => 'EUC-JP', // Japanese (JIS 0208-1990 and 0212-1990)
|
||||
20936 => 'cp20936', // Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)
|
||||
20949 => 'cp20949', // Korean Wansung
|
||||
21025 => 'cp1025', // IBM EBCDIC Cyrillic Serbian-Bulgarian
|
||||
21866 => 'koi8-u', // Ukrainian (KOI8-U); Cyrillic (KOI8-U)
|
||||
28591 => 'iso-8859-1', // ISO 8859-1 Latin 1; Western European (ISO)
|
||||
28592 => 'iso-8859-2', // ISO 8859-2 Central European; Central European (ISO)
|
||||
28593 => 'iso-8859-3', // ISO 8859-3 Latin 3
|
||||
28594 => 'iso-8859-4', // ISO 8859-4 Baltic
|
||||
28595 => 'iso-8859-5', // ISO 8859-5 Cyrillic
|
||||
28596 => 'iso-8859-6', // ISO 8859-6 Arabic
|
||||
28597 => 'iso-8859-7', // ISO 8859-7 Greek
|
||||
28598 => 'iso-8859-8', // ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
|
||||
28599 => 'iso-8859-9', // ISO 8859-9 Turkish
|
||||
28603 => 'iso-8859-13', // ISO 8859-13 Estonian
|
||||
28605 => 'iso-8859-15', // ISO 8859-15 Latin 9
|
||||
38598 => 'iso-8859-8-i', // ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
|
||||
50220 => 'iso-2022-jp', // ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)
|
||||
50221 => 'csISO2022JP', // ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)
|
||||
50222 => 'iso-2022-jp', // ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)
|
||||
50225 => 'iso-2022-kr', // ISO 2022 Korean
|
||||
51932 => 'EUC-JP', // EUC Japanese
|
||||
51936 => 'EUC-CN', // EUC Simplified Chinese; Chinese Simplified (EUC)
|
||||
51949 => 'EUC-KR', // EUC Korean
|
||||
52936 => 'hz-gb-2312', // HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
|
||||
54936 => 'GB18030', // Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)
|
||||
65000 => 'UTF-7',
|
||||
65001 => 'UTF-8',
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse and validate charset name string.
|
||||
* 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 = [];
|
||||
|
||||
$charset = strtoupper((string) $input);
|
||||
|
||||
if (isset($charsets[$input])) {
|
||||
return $charsets[$input];
|
||||
}
|
||||
|
||||
$charset = preg_replace([
|
||||
'/^[^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)
|
||||
'/\*.*$/' // lang code according to RFC 2231.5
|
||||
], '', $charset);
|
||||
|
||||
if ($charset == 'BINARY') {
|
||||
return $charsets[$input] = null;
|
||||
}
|
||||
|
||||
// allow A-Z and 0-9 only
|
||||
$str = preg_replace('/[^A-Z0-9]/', '', $charset);
|
||||
|
||||
$result = $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] . (!empty($m[2]) ? $m[2] : '');
|
||||
}
|
||||
// ISO-8859
|
||||
else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
|
||||
$iso = 'ISO-8859-' . ($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 = ['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';
|
||||
}
|
||||
// we need ISO labels
|
||||
else if (!empty($aliases[$m[1]])) {
|
||||
$result = 'ISO-8859-'.$aliases[$m[1]];
|
||||
}
|
||||
}
|
||||
|
||||
$charsets[$input] = $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string from one charset to another.
|
||||
*
|
||||
* @param string $str Input string
|
||||
* @param string $from Suspected charset of the input string
|
||||
* @param string $to Target charset to convert to; defaults to RCUBE_CHARSET
|
||||
*
|
||||
* @return string Converted string
|
||||
*/
|
||||
public static function convert($str, $from, $to = null)
|
||||
{
|
||||
static $iconv_options;
|
||||
|
||||
$to = empty($to) ? RCUBE_CHARSET : self::parse_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;
|
||||
}
|
||||
|
||||
$out = false;
|
||||
$error_handler = function() { throw new \Exception(); };
|
||||
|
||||
// Ignore invalid characters
|
||||
$mbstring_sc = mb_substitute_character();
|
||||
mb_substitute_character('none');
|
||||
|
||||
// If mbstring reports an illegal character in input via E_WARNING.
|
||||
// FIXME: Is this really true with substitute character 'none'?
|
||||
// A warning is thrown in PHP<8 also on unsupported encoding, in PHP>=8 ValueError
|
||||
// is thrown instead (therefore we catch Throwable below)
|
||||
set_error_handler($error_handler, E_WARNING);
|
||||
|
||||
try {
|
||||
$out = mb_convert_encoding($str, $to, $from);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$out = false;
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
mb_substitute_character($mbstring_sc);
|
||||
|
||||
if ($out !== false) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to iconv module, it is slower, but supports much more charsets than mbstring
|
||||
if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP'
|
||||
&& $from !== 'ISO-2022-JP'
|
||||
) {
|
||||
// If iconv reports an illegal character in input it means that input string
|
||||
// has been truncated. It's reported as E_NOTICE.
|
||||
// PHP8 will also throw E_WARNING on unsupported encoding.
|
||||
set_error_handler($error_handler, E_NOTICE | E_WARNING);
|
||||
|
||||
try {
|
||||
$out = iconv($from, $to . $iconv_options, $str);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$out = false;
|
||||
}
|
||||
|
||||
restore_error_handler();
|
||||
|
||||
if ($out !== false) {
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
// return the original string
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the specified input string matches one of the provided charsets.
|
||||
* This includes UTF-32, UTF-16, RCUBE_CHARSET and default_charset.
|
||||
*
|
||||
* @param string $str Input string
|
||||
* @param array $from Suspected charsets of the input string
|
||||
*
|
||||
* @return string|null First matching charset
|
||||
*/
|
||||
public static function check($str, $charsets = [])
|
||||
{
|
||||
$chunk = strlen($str) > 100 * 1024 ? substr($str, 0, 100 * 1024) : $str;
|
||||
|
||||
// Add dehault charset, system charset and easily detectable charset to the list
|
||||
if (substr($chunk, 0, 4) == "\0\0\xFE\xFF") $charsets[] = 'UTF-32BE';
|
||||
if (substr($chunk, 0, 4) == "\xFF\xFE\0\0") $charsets[] = 'UTF-32LE';
|
||||
if (substr($chunk, 0, 2) == "\xFE\xFF") $charsets[] = 'UTF-16BE';
|
||||
if (substr($chunk, 0, 2) == "\xFF\xFE") $charsets[] = 'UTF-16LE';
|
||||
|
||||
// heuristics
|
||||
if (preg_match('/\x00\x00\x00[^\x00]/', $chunk)) $charsets[] = 'UTF-32BE';
|
||||
if (preg_match('/[^\x00]\x00\x00\x00/', $chunk)) $charsets[] = 'UTF-32LE';
|
||||
if (preg_match('/\x00[^\x00]\x00[^\x00]/', $chunk)) $charsets[] = 'UTF-16BE';
|
||||
if (preg_match('/[^\x00]\x00[^\x00]\x00/', $chunk)) $charsets[] = 'UTF-16LE';
|
||||
|
||||
$charsets[] = RCUBE_CHARSET;
|
||||
$charsets[] = (string) rcube::get_instance()->config->get('default_charset');
|
||||
|
||||
$charsets = array_map(['rcube_charset', 'parse_charset'], $charsets);
|
||||
$charsets = array_unique(array_filter($charsets));
|
||||
|
||||
foreach ($charsets as $charset) {
|
||||
$ret = self::convert($chunk, $charset);
|
||||
|
||||
if ($ret === rcube_charset::clean($ret)) {
|
||||
return $charset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string from standard UTF-7 (RFC 2152) to UTF-8.
|
||||
*
|
||||
* @param string $str Input string (UTF-7)
|
||||
*
|
||||
* @return string Converted string (UTF-8)
|
||||
* @deprecated use self::convert()
|
||||
*/
|
||||
public static function utf7_to_utf8($str)
|
||||
{
|
||||
return self::convert($str, 'UTF-7', 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
|
||||
*
|
||||
* @param string $str Input string
|
||||
*
|
||||
* @return string The converted string
|
||||
* @deprecated use self::convert()
|
||||
*/
|
||||
public static function utf16_to_utf8($str)
|
||||
{
|
||||
return self::convert($str, 'UTF-16BE', 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-).
|
||||
*
|
||||
* @param string $str Input string (UTF7-IMAP)
|
||||
*
|
||||
* @return string Output string (UTF-8)
|
||||
* @deprecated use self::convert()
|
||||
*/
|
||||
public static function utf7imap_to_utf8($str)
|
||||
{
|
||||
return self::convert($str, 'UTF7-IMAP', 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param string $str Input string (UTF-8)
|
||||
*
|
||||
* @return string Output string (UTF7-IMAP)
|
||||
* @deprecated use self::convert()
|
||||
*/
|
||||
public static function utf8_to_utf7imap($str)
|
||||
{
|
||||
return self::convert($str, 'UTF-8', 'UTF7-IMAP');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @deprecated
|
||||
*/
|
||||
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 (strlen($string) >= 4) {
|
||||
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 (empty($language)) {
|
||||
$rcube = rcube::get_instance();
|
||||
$language = $rcube->get_user_language();
|
||||
}
|
||||
|
||||
// Prioritize charsets according to the current language (#1485669)
|
||||
$prio = null;
|
||||
switch ($language) {
|
||||
case 'ja_JP':
|
||||
$prio = ['ISO-2022-JP', 'JIS', 'UTF-8', 'EUC-JP', 'eucJP-win', 'SJIS'];
|
||||
break;
|
||||
|
||||
case 'zh_CN':
|
||||
case 'zh_TW':
|
||||
$prio = ['UTF-8', 'BIG-5', 'EUC-TW', 'GB18030'];
|
||||
break;
|
||||
|
||||
case 'ko_KR':
|
||||
$prio = ['UTF-8', 'EUC-KR', 'ISO-2022-KR'];
|
||||
break;
|
||||
|
||||
case 'ru_RU':
|
||||
$prio = ['UTF-8', 'WINDOWS-1251', 'KOI8-R'];
|
||||
break;
|
||||
|
||||
case 'tr_TR':
|
||||
$prio = ['UTF-8', 'ISO-8859-9', 'WINDOWS-1254'];
|
||||
break;
|
||||
}
|
||||
|
||||
// mb_detect_encoding() is not reliable for some charsets (#1490135)
|
||||
// use mb_check_encoding() to make charset priority lists really working
|
||||
if (!empty($prio) && function_exists('mb_check_encoding')) {
|
||||
foreach ($prio as $encoding) {
|
||||
if (mb_check_encoding($string, $encoding)) {
|
||||
return $encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('mb_detect_encoding')) {
|
||||
$exclude = 'BASE64,UUENCODE,HTML-ENTITIES,Quoted-Printable,'
|
||||
. '7bit,8bit,pass,wchar,byte2be,byte2le,byte4be,byte4le,'
|
||||
. 'UCS-4,UCS-4BE,UCS-4LE,UCS-2,UCS-2BE,UCS-2LE';
|
||||
|
||||
if (empty($prio)) {
|
||||
$prio = [
|
||||
'UTF-8',
|
||||
'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4',
|
||||
'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9',
|
||||
'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16',
|
||||
'WINDOWS-1252', 'WINDOWS-1251', 'WINDOWS-1254',
|
||||
'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG-5', 'ISO-2022-KR', 'ISO-2022-JP', 'GB18030',
|
||||
];
|
||||
}
|
||||
|
||||
// We have to remove unwanted/uncommon encodings from the list.
|
||||
// This is needed especially on PHP >= 8.1
|
||||
$all_encodings = array_diff(mb_list_encodings(), explode(',', $exclude));
|
||||
|
||||
$encodings = array_unique(array_merge($prio, $all_encodings));
|
||||
|
||||
if ($encoding = mb_detect_encoding($string, $encodings, true)) {
|
||||
return $encoding;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
* If the input is an array, both values and keys will be cleaned up.
|
||||
*
|
||||
* @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 (array_keys($input) as $key) {
|
||||
$k = is_string($key) ? self::clean($key) : $key;
|
||||
$v = self::clean($input[$key]);
|
||||
|
||||
if ($k !== $key) {
|
||||
unset($input[$key]);
|
||||
if (!array_key_exists($k, $input)) {
|
||||
$input[$k] = $v;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$input[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
if (!is_string($input) || $input == '') {
|
||||
return $input;
|
||||
}
|
||||
|
||||
$msch = mb_substitute_character();
|
||||
mb_substitute_character('none');
|
||||
$res = mb_convert_encoding($input, 'UTF-8', 'UTF-8');
|
||||
mb_substitute_character($msch);
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,944 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Class to read configuration settings |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration class for Roundcube
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_config
|
||||
{
|
||||
const DEFAULT_SKIN = 'elastic';
|
||||
|
||||
/** @var string A skin configured in the config file (before being replaced by a user preference) */
|
||||
public $system_skin = 'elastic';
|
||||
|
||||
private $env = '';
|
||||
private $paths = [];
|
||||
private $prop = [];
|
||||
private $errors = [];
|
||||
private $userprefs = [];
|
||||
private $immutable = [];
|
||||
private $client_tz;
|
||||
|
||||
|
||||
/**
|
||||
* Renamed options
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $legacy_props = [
|
||||
// new name => old name
|
||||
'mail_pagesize' => 'pagesize',
|
||||
'addressbook_pagesize' => 'pagesize',
|
||||
'reply_mode' => 'top_posting',
|
||||
'refresh_interval' => 'keep_alive',
|
||||
'min_refresh_interval' => 'min_keep_alive',
|
||||
'messages_cache_ttl' => 'message_cache_lifetime',
|
||||
'mail_read_time' => 'preview_pane_mark_read',
|
||||
'session_debug' => 'log_session',
|
||||
'redundant_attachments_cache_ttl' => 'redundant_attachments_memcache_ttl',
|
||||
'imap_host' => 'default_host',
|
||||
'smtp_host' => 'smtp_server',
|
||||
];
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param string $env Environment suffix for config files to load
|
||||
*/
|
||||
public function __construct($env = '')
|
||||
{
|
||||
$this->env = $env;
|
||||
|
||||
if ($paths = getenv('RCUBE_CONFIG_PATH')) {
|
||||
$this->paths = explode(PATH_SEPARATOR, $paths);
|
||||
// make all paths absolute
|
||||
foreach ($this->paths as $i => $path) {
|
||||
if (!rcube_utils::is_absolute_path($path)) {
|
||||
if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
|
||||
$this->paths[$i] = unslashify($realpath) . '/';
|
||||
}
|
||||
else {
|
||||
unset($this->paths[$i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->paths[$i] = unslashify($path) . '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) {
|
||||
$this->paths[] = RCUBE_CONFIG_DIR;
|
||||
}
|
||||
|
||||
if (empty($this->paths)) {
|
||||
$this->paths[] = RCUBE_INSTALL_PATH . 'config/';
|
||||
}
|
||||
|
||||
$this->load();
|
||||
|
||||
// Defaults, that we do not require you to configure,
|
||||
// but contain information that is used in various locations in the code:
|
||||
if (empty($this->prop['contactlist_fields'])) {
|
||||
$this->set('contactlist_fields', ['name', 'firstname', 'surname', 'email']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks inside the string to determine what type might be best as a container.
|
||||
*
|
||||
* @param string $value The value to inspect
|
||||
*
|
||||
* @return string The guessed type.
|
||||
*/
|
||||
private function guess_type($value)
|
||||
{
|
||||
if (preg_match('/^\d+$/', $value)) {
|
||||
return 'int';
|
||||
}
|
||||
|
||||
if (preg_match('/^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$/', $value)) {
|
||||
return 'float';
|
||||
}
|
||||
|
||||
if (preg_match('/^(t(rue)?)|(f(alse)?)$/i', $value)) {
|
||||
return 'bool';
|
||||
}
|
||||
|
||||
// TODO: array/object
|
||||
|
||||
return 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse environment variable into PHP type.
|
||||
*
|
||||
* @param string $string String to parse into PHP type
|
||||
* @param string $type Type of value to return
|
||||
*
|
||||
* @return mixed Appropriately typed interpretation of $string.
|
||||
*/
|
||||
private function parse_env($string, $type = null)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
return (bool) $string;
|
||||
|
||||
case 'int':
|
||||
return (int) $string;
|
||||
|
||||
case 'float':
|
||||
return (float) $string;
|
||||
|
||||
case 'string':
|
||||
return $string;
|
||||
|
||||
case 'array':
|
||||
return json_decode($string, true);
|
||||
|
||||
case 'object':
|
||||
return json_decode($string, false);
|
||||
}
|
||||
|
||||
return $this->parse_env($string, $this->guess_type($string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variable value.
|
||||
*
|
||||
* Retrieve an environment variable's value or if it's not found, return the
|
||||
* provided default value.
|
||||
*
|
||||
* @param string $varname Environment variable name
|
||||
* @param mixed $default_value Default value to return if necessary
|
||||
* @param string $type Type of value to return
|
||||
*
|
||||
* @return mixed Value of the environment variable or default if not found.
|
||||
*/
|
||||
private function getenv_default($varname, $default_value, $type = null)
|
||||
{
|
||||
$value = getenv($varname);
|
||||
|
||||
if ($value === false) {
|
||||
$value = $default_value;
|
||||
}
|
||||
else {
|
||||
$value = $this->parse_env($value, $type ?: gettype($default_value));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config from local config file
|
||||
*/
|
||||
private function load()
|
||||
{
|
||||
// Load default settings
|
||||
if (!$this->load_from_file('defaults.inc.php')) {
|
||||
$this->errors[] = 'defaults.inc.php was not found.';
|
||||
}
|
||||
|
||||
// load main config file
|
||||
if (!$this->load_from_file('config.inc.php')) {
|
||||
// Old configuration files
|
||||
if (!$this->load_from_file('main.inc.php') || !$this->load_from_file('db.inc.php')) {
|
||||
$this->errors[] = 'config.inc.php was not found.';
|
||||
}
|
||||
else if (rand(1,100) == 10) { // log warning on every 100th request (average)
|
||||
trigger_error("config.inc.php was not found. Please migrate your config by running bin/update.sh", E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
// load host-specific configuration
|
||||
$this->load_host_config();
|
||||
|
||||
// set skin (with fallback to old 'skin_path' property)
|
||||
if (empty($this->prop['skin'])) {
|
||||
if (!empty($this->prop['skin_path'])) {
|
||||
$this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
|
||||
}
|
||||
else {
|
||||
$this->prop['skin'] = self::DEFAULT_SKIN;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->prop['skin'] == 'default') {
|
||||
$this->prop['skin'] = self::DEFAULT_SKIN;
|
||||
}
|
||||
|
||||
$this->system_skin = $this->prop['skin'];
|
||||
|
||||
// fix paths
|
||||
foreach (['log_dir' => 'logs', 'temp_dir' => 'temp'] as $key => $dir) {
|
||||
foreach ([$this->prop[$key], '../' . $this->prop[$key], RCUBE_INSTALL_PATH . $dir] as $path) {
|
||||
if ($path && ($realpath = realpath(unslashify($path)))) {
|
||||
$this->prop[$key] = $realpath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix default imap folders encoding
|
||||
foreach (['drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox'] as $folder) {
|
||||
$this->prop[$folder] = rcube_charset::convert($this->prop[$folder], RCUBE_CHARSET, 'UTF7-IMAP');
|
||||
}
|
||||
|
||||
// set PHP error logging according to config
|
||||
$error_log = $this->prop['log_driver'] ?: 'file';
|
||||
if ($error_log == 'file') {
|
||||
$error_log = $this->prop['log_dir'] . '/errors';
|
||||
$error_log .= $this->prop['log_file_ext'] ?? '.log';
|
||||
}
|
||||
|
||||
if ($error_log && $error_log != 'stdout') {
|
||||
ini_set('error_log', $error_log);
|
||||
}
|
||||
|
||||
// set default screen layouts
|
||||
$this->prop['supported_layouts'] = ['widescreen', 'desktop', 'list'];
|
||||
|
||||
// remove deprecated properties
|
||||
unset($this->prop['dst_active']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a host-specific config file if configured
|
||||
* This will merge the host specific configuration with the given one
|
||||
*/
|
||||
private function load_host_config()
|
||||
{
|
||||
if (empty($this->prop['include_host_config'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (['HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR'] as $key) {
|
||||
if (empty($_SERVER[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fname = null;
|
||||
$name = $_SERVER[$key];
|
||||
|
||||
if (!empty($this->prop['include_host_config']) && is_array($this->prop['include_host_config'])) {
|
||||
if (isset($this->prop['include_host_config'][$name])) {
|
||||
$fname = $this->prop['include_host_config'][$name];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $name) . '.inc.php';
|
||||
}
|
||||
|
||||
if ($fname && $this->load_from_file($fname)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read configuration from a file
|
||||
* and merge with the already stored config values
|
||||
*
|
||||
* @param string $file Name of the config file to be loaded
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function load_from_file($file)
|
||||
{
|
||||
$success = false;
|
||||
|
||||
foreach ($this->resolve_paths($file) as $fpath) {
|
||||
if ($fpath && is_file($fpath) && is_readable($fpath)) {
|
||||
// use output buffering, we don't need any output here
|
||||
ob_start();
|
||||
include($fpath);
|
||||
ob_end_clean();
|
||||
|
||||
if (isset($config) && is_array($config)) {
|
||||
$this->merge($config);
|
||||
$success = true;
|
||||
}
|
||||
// deprecated name of config variable
|
||||
if (isset($rcmail_config) && is_array($rcmail_config)) {
|
||||
$this->merge($rcmail_config);
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to resolve absolute paths to the given config file.
|
||||
* This also takes the 'env' property into account.
|
||||
*
|
||||
* @param string $file Filename or absolute file path
|
||||
* @param bool $use_env Return -$env file path if exists
|
||||
*
|
||||
* @return array List of candidates in config dir path(s)
|
||||
*/
|
||||
public function resolve_paths($file, $use_env = true)
|
||||
{
|
||||
$files = [];
|
||||
$abs_path = rcube_utils::is_absolute_path($file);
|
||||
|
||||
foreach ($this->paths as $basepath) {
|
||||
$realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
|
||||
|
||||
// check if <file>-<env>.inc.php exists
|
||||
if ($use_env && !empty($this->env)) {
|
||||
$envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $file);
|
||||
$envfile = $abs_path ? $envfile : realpath($basepath . '/' . $envfile);
|
||||
|
||||
if (is_file($envfile)) {
|
||||
$realpath = $envfile;
|
||||
}
|
||||
}
|
||||
|
||||
if ($realpath) {
|
||||
$files[] = $realpath;
|
||||
|
||||
// no need to continue the loop if an absolute file path is given
|
||||
if ($abs_path) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for a specific config parameter
|
||||
*
|
||||
* @param string $name Parameter name
|
||||
* @param mixed $def Default value if not set
|
||||
*
|
||||
* @return mixed The requested config value
|
||||
*/
|
||||
public function get($name, $def = null)
|
||||
{
|
||||
if (isset($this->prop[$name])) {
|
||||
$result = $this->prop[$name];
|
||||
}
|
||||
else {
|
||||
$result = $def;
|
||||
}
|
||||
|
||||
$result = $this->getenv_default('ROUNDCUBE_' . strtoupper($name), $result);
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
if ($name == 'timezone') {
|
||||
if (empty($result) || $result == 'auto') {
|
||||
$result = $this->client_timezone();
|
||||
}
|
||||
}
|
||||
else if ($name == 'client_mimetypes') {
|
||||
if (!$result && !$def) {
|
||||
$result = 'text/plain,text/html'
|
||||
. ',image/jpeg,image/gif,image/png,image/bmp,image/tiff,image/webp'
|
||||
. ',application/x-javascript,application/pdf,application/x-shockwave-flash';
|
||||
}
|
||||
if ($result && is_string($result)) {
|
||||
$result = explode(',', $result);
|
||||
}
|
||||
}
|
||||
else if ($name == 'layout') {
|
||||
if (!in_array($result, $this->prop['supported_layouts'])) {
|
||||
$result = $this->prop['supported_layouts'][0];
|
||||
}
|
||||
}
|
||||
else if ($name == 'collected_senders') {
|
||||
if (is_bool($result)) {
|
||||
$result = $result ? rcube_addressbook::TYPE_TRUSTED_SENDER : '';
|
||||
}
|
||||
$result = (string) $result;
|
||||
}
|
||||
else if ($name == 'collected_recipients') {
|
||||
if (is_bool($result)) {
|
||||
$result = $result ? rcube_addressbook::TYPE_RECIPIENT : '';
|
||||
}
|
||||
$result = (string) $result;
|
||||
}
|
||||
|
||||
$plugin = $rcube->plugins->exec_hook('config_get', [
|
||||
'name' => $name,
|
||||
'default' => $def,
|
||||
'result' => $result
|
||||
]);
|
||||
|
||||
return $plugin['result'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for a config parameter
|
||||
*
|
||||
* @param string $name Parameter name
|
||||
* @param mixed $value Parameter value
|
||||
* @param bool $immutable Make the value immutable
|
||||
*/
|
||||
public function set($name, $value, $immutable = false)
|
||||
{
|
||||
$this->prop[$name] = $value;
|
||||
|
||||
if ($immutable) {
|
||||
$this->immutable[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override config options with the given values (e.g. user prefs)
|
||||
*
|
||||
* @param array $prefs Hash array with config props to merge over
|
||||
*/
|
||||
public function merge($prefs)
|
||||
{
|
||||
$prefs = $this->fix_legacy_props($prefs);
|
||||
$this->prop = array_merge($this->prop, $prefs, $this->userprefs, $this->immutable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given prefs over the current config
|
||||
* and make sure that they survive further merging.
|
||||
*
|
||||
* @param array $prefs Hash array with user prefs
|
||||
*/
|
||||
public function set_user_prefs($prefs)
|
||||
{
|
||||
$prefs = $this->fix_legacy_props($prefs);
|
||||
|
||||
// Honor the dont_override setting for any existing user preferences
|
||||
$dont_override = $this->get('dont_override');
|
||||
if (is_array($dont_override) && !empty($dont_override)) {
|
||||
foreach ($dont_override as $key) {
|
||||
unset($prefs[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($prefs['skin']) && $prefs['skin'] == 'default') {
|
||||
$prefs['skin'] = $this->system_skin;
|
||||
}
|
||||
|
||||
$skins_allowed = $this->get('skins_allowed');
|
||||
|
||||
if (!empty($prefs['skin']) && !empty($skins_allowed) && !in_array($prefs['skin'], (array) $skins_allowed)) {
|
||||
unset($prefs['skin']);
|
||||
}
|
||||
|
||||
$this->userprefs = $prefs;
|
||||
$this->prop = array_merge($this->prop, $prefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for all config options.
|
||||
*
|
||||
* Unlike get() this method does not resolve any special
|
||||
* values like e.g. 'timezone'.
|
||||
*
|
||||
* It is discouraged to use this method outside of Roundcube core.
|
||||
*
|
||||
* @return array Hash array containing all config properties
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
$props = $this->prop;
|
||||
|
||||
foreach ($props as $prop_name => $prop_value) {
|
||||
$props[$prop_name] = $this->getenv_default('ROUNDCUBE_' . strtoupper($prop_name), $prop_value);
|
||||
}
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
$plugin = $rcube->plugins->exec_hook('config_get', ['name' => '*', 'result' => $props]);
|
||||
|
||||
return $plugin['result'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Some options set as immutable that are also listed
|
||||
* in dont_override should not be stored permanently
|
||||
* in user preferences. Here's the list of these
|
||||
*
|
||||
* @return array List of transient options
|
||||
*/
|
||||
public function transient_options()
|
||||
{
|
||||
return array_intersect(array_keys($this->immutable), (array) $this->get('dont_override'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Special getter for user's timezone offset including DST
|
||||
*
|
||||
* @return float Timezone offset (in hours)
|
||||
* @deprecated
|
||||
*/
|
||||
public function get_timezone()
|
||||
{
|
||||
if ($tz = $this->get('timezone')) {
|
||||
try {
|
||||
$tz = new DateTimeZone($tz);
|
||||
return $tz->getOffset(new DateTime('now')) / 3600;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return requested DES crypto key.
|
||||
*
|
||||
* @param string $key Crypto key name
|
||||
*
|
||||
* @return string Crypto key
|
||||
*/
|
||||
public function get_crypto_key($key)
|
||||
{
|
||||
// Bomb out if the requested key does not exist
|
||||
if (!array_key_exists($key, $this->prop) || empty($this->prop[$key])) {
|
||||
rcube::raise_error([
|
||||
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Request for unconfigured crypto key \"$key\""
|
||||
], true, true);
|
||||
}
|
||||
|
||||
return $this->prop[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return configured crypto method.
|
||||
*
|
||||
* @return string Crypto method
|
||||
*/
|
||||
public function get_crypto_method()
|
||||
{
|
||||
return $this->get('cipher_method') ?: 'DES-EDE3-CBC';
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to autodetect operating system and find the correct line endings
|
||||
*
|
||||
* @return string The appropriate mail header delimiter
|
||||
* @deprecated Since 1.3 we don't use mail()
|
||||
*/
|
||||
public function header_delimiter()
|
||||
{
|
||||
// use the configured delimiter for headers
|
||||
if (!empty($this->prop['mail_header_delimiter'])) {
|
||||
$delim = $this->prop['mail_header_delimiter'];
|
||||
if ($delim == "\n" || $delim == "\r\n") {
|
||||
return $delim;
|
||||
}
|
||||
else {
|
||||
rcube::raise_error([
|
||||
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Invalid mail_header_delimiter setting"
|
||||
], true, false);
|
||||
}
|
||||
}
|
||||
|
||||
$php_os = strtolower(substr(PHP_OS, 0, 3));
|
||||
|
||||
if ($php_os == 'win') {
|
||||
return "\r\n";
|
||||
}
|
||||
|
||||
if ($php_os == 'mac') {
|
||||
return "\r\n";
|
||||
}
|
||||
|
||||
return "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of configured PGP key servers
|
||||
*
|
||||
* @return array|null List of keyservers' URLs
|
||||
*/
|
||||
public function keyservers()
|
||||
{
|
||||
$list = (array) $this->prop['keyservers'];
|
||||
|
||||
foreach ($list as $idx => $host) {
|
||||
if (!preg_match('|^[a-z]+://|', $host)) {
|
||||
$host = "https://$host";
|
||||
}
|
||||
|
||||
$list[$idx] = slashify($host);
|
||||
}
|
||||
|
||||
return !empty($list) ? $list : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mail domain configured for the given host
|
||||
*
|
||||
* @param string $host IMAP host
|
||||
* @param bool $encode If true, domain name will be converted to IDN ASCII
|
||||
*
|
||||
* @return string Resolved SMTP host
|
||||
*/
|
||||
public function mail_domain($host, $encode = true)
|
||||
{
|
||||
$domain = $host;
|
||||
|
||||
if (!empty($this->prop['mail_domain'])) {
|
||||
if (is_array($this->prop['mail_domain'])) {
|
||||
if (isset($this->prop['mail_domain'][$host])) {
|
||||
$domain = $this->prop['mail_domain'][$host];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$domain = rcube_utils::parse_host($this->prop['mail_domain']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($encode) {
|
||||
$domain = rcube_utils::idn_to_ascii($domain);
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for error state
|
||||
*
|
||||
* @return mixed Error message on error, False if no errors
|
||||
*/
|
||||
public function get_error()
|
||||
{
|
||||
return empty($this->errors) ? false : implode("\n", $this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal getter for client's (browser) timezone identifier
|
||||
*/
|
||||
private function client_timezone()
|
||||
{
|
||||
if ($this->client_tz) {
|
||||
return $this->client_tz;
|
||||
}
|
||||
|
||||
// @TODO: remove this legacy timezone handling in the future
|
||||
if (isset($_SESSION['timezone'])) {
|
||||
$props = $this->fix_legacy_props(['timezone' => $_SESSION['timezone']]);
|
||||
}
|
||||
|
||||
if (!empty($props['timezone'])) {
|
||||
// Prevent from using deprecated timezone names
|
||||
$props['timezone'] = $this->resolve_timezone_alias($props['timezone']);
|
||||
|
||||
try {
|
||||
$tz = new DateTimeZone($props['timezone']);
|
||||
return $this->client_tz = $tz->getName();
|
||||
}
|
||||
catch (Exception $e) { /* gracefully ignore */ }
|
||||
}
|
||||
|
||||
// fallback to server's timezone
|
||||
return date_default_timezone_get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert legacy options into new ones
|
||||
*
|
||||
* @param array $props Hash array with config props
|
||||
*
|
||||
* @return array Converted config props
|
||||
*/
|
||||
private function fix_legacy_props($props)
|
||||
{
|
||||
foreach ($this->legacy_props as $new => $old) {
|
||||
if (isset($props[$old])) {
|
||||
if (!isset($props[$new])) {
|
||||
$props[$new] = $props[$old];
|
||||
}
|
||||
unset($props[$old]);
|
||||
}
|
||||
}
|
||||
|
||||
// convert deprecated numeric timezone value
|
||||
if (isset($props['timezone']) && is_numeric($props['timezone'])) {
|
||||
if ($tz = self::timezone_name_from_abbr($props['timezone'])) {
|
||||
$props['timezone'] = $tz;
|
||||
}
|
||||
else {
|
||||
unset($props['timezone']);
|
||||
}
|
||||
}
|
||||
|
||||
// translate old `preview_pane` settings to `layout`
|
||||
if (isset($props['preview_pane']) && !isset($props['layout'])) {
|
||||
$props['layout'] = $props['preview_pane'] ? 'desktop' : 'list';
|
||||
unset($props['preview_pane']);
|
||||
}
|
||||
|
||||
// translate old `display_version` settings to `display_product_info`
|
||||
if (isset($props['display_version']) && !isset($props['display_product_info'])) {
|
||||
$props['display_product_info'] = $props['display_version'] ? 2 : 1;
|
||||
unset($props['display_version']);
|
||||
}
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* timezone_name_from_abbr() replacement. Converts timezone offset
|
||||
* into timezone name abbreviation.
|
||||
*
|
||||
* @param float $offset Timezone offset (in hours)
|
||||
*
|
||||
* @return string|null Timezone abbreviation
|
||||
*/
|
||||
static public function timezone_name_from_abbr($offset)
|
||||
{
|
||||
// List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780
|
||||
if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) {
|
||||
return $tz;
|
||||
}
|
||||
|
||||
// try with more complete list (#1489261)
|
||||
$timezones = [
|
||||
'-660' => "Pacific/Apia",
|
||||
'-600' => "Pacific/Honolulu",
|
||||
'-570' => "Pacific/Marquesas",
|
||||
'-540' => "America/Anchorage",
|
||||
'-480' => "America/Los_Angeles",
|
||||
'-420' => "America/Denver",
|
||||
'-360' => "America/Chicago",
|
||||
'-300' => "America/New_York",
|
||||
'-270' => "America/Caracas",
|
||||
'-240' => "America/Halifax",
|
||||
'-210' => "Canada/Newfoundland",
|
||||
'-180' => "America/Sao_Paulo",
|
||||
'-60' => "Atlantic/Azores",
|
||||
'0' => "Europe/London",
|
||||
'60' => "Europe/Paris",
|
||||
'120' => "Europe/Helsinki",
|
||||
'180' => "Europe/Moscow",
|
||||
'210' => "Asia/Tehran",
|
||||
'240' => "Asia/Dubai",
|
||||
'270' => "Asia/Kabul",
|
||||
'300' => "Asia/Karachi",
|
||||
'330' => "Asia/Kolkata",
|
||||
'345' => "Asia/Katmandu",
|
||||
'360' => "Asia/Yekaterinburg",
|
||||
'390' => "Asia/Rangoon",
|
||||
'420' => "Asia/Krasnoyarsk",
|
||||
'480' => "Asia/Shanghai",
|
||||
'525' => "Australia/Eucla",
|
||||
'540' => "Asia/Tokyo",
|
||||
'570' => "Australia/Adelaide",
|
||||
'600' => "Australia/Melbourne",
|
||||
'630' => "Australia/Lord_Howe",
|
||||
'660' => "Asia/Vladivostok",
|
||||
'690' => "Pacific/Norfolk",
|
||||
'720' => "Pacific/Auckland",
|
||||
'765' => "Pacific/Chatham",
|
||||
'780' => "Pacific/Enderbury",
|
||||
'840' => "Pacific/Kiritimati",
|
||||
];
|
||||
|
||||
$key = (string) intval($offset * 60);
|
||||
|
||||
return !empty($timezones[$key]) ? $timezones[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace deprecated timezone name with a valid one.
|
||||
*
|
||||
* @param string $tzname Timezone name
|
||||
*
|
||||
* @return string Timezone name
|
||||
*/
|
||||
static public function resolve_timezone_alias($tzname)
|
||||
{
|
||||
// http://www.php.net/manual/en/timezones.others.php
|
||||
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
$deprecated_timezones = [
|
||||
'Australia/ACT' => 'Australia/Sydney',
|
||||
'Australia/LHI' => 'Australia/Lord_Howe',
|
||||
'Australia/North' => 'Australia/Darwin',
|
||||
'Australia/NSW' => 'Australia/Sydney',
|
||||
'Australia/Queensland' => 'Australia/Brisbane',
|
||||
'Australia/South' => 'Australia/Adelaide',
|
||||
'Australia/Adelaide' => 'Australia/Hobart',
|
||||
'Australia/Tasmania' => 'Australia/Hobart',
|
||||
'Australia/Victoria' => 'Australia/Melbourne',
|
||||
'Australia/West' => 'Australia/Perth',
|
||||
'Brazil/Acre' => 'America/Rio_Branco',
|
||||
'Brazil/DeNoronha' => 'America/Noronha',
|
||||
'Brazil/East' => 'America/Sao_Paulo',
|
||||
'Brazil/West' => 'America/Manaus',
|
||||
'Canada/Atlantic' => 'America/Halifax',
|
||||
'Canada/Central' => 'America/Winnipeg',
|
||||
'Canada/Eastern' => 'America/Toronto',
|
||||
'Canada/Mountain' => 'America/Edmonton',
|
||||
'Canada/Newfoundland' => 'America/St_Johns',
|
||||
'Canada/Pacific' => 'America/Vancouver',
|
||||
'Canada/Saskatchewan' => 'America/Regina',
|
||||
'Canada/Yukon' => 'America/Whitehorse',
|
||||
'CET' => 'Europe/Berlin',
|
||||
'Chile/Continental' => 'America/Santiago',
|
||||
'Chile/EasterIsland' => 'Pacific/Easter',
|
||||
'CST6CDT' => 'America/Chicago',
|
||||
'Cuba' => ' America/Havana',
|
||||
'EET' => 'Europe/Berlin',
|
||||
'Egypt' => 'Africa/Cairo',
|
||||
'Eire' => 'Europe/Dublin',
|
||||
'EST' => 'America/New_York',
|
||||
'EST5EDT' => 'America/New_York',
|
||||
'Factory' => 'UTC', // ?
|
||||
'GB' => 'Europe/London',
|
||||
'GB-Eire' => 'Europe/London',
|
||||
'GMT' => 'UTC',
|
||||
'GMT+0' => 'UTC',
|
||||
'GMT-0' => 'UTC',
|
||||
'GMT0' => 'UTC',
|
||||
'Greenwich' => 'UTC',
|
||||
'Hongkong' => 'Asia/Hong_Kong',
|
||||
'HST' => 'Pacific/Honolulu',
|
||||
'Iceland' => 'Atlantic/Reykjavik',
|
||||
'Iran' => 'Asia/Tehran',
|
||||
'Israel' => 'Asia/Jerusalem',
|
||||
'Jamaica' => 'America/Jamaica',
|
||||
'Japan' => 'Asia/Tokyo',
|
||||
'Kwajalein' => 'Pacific/Kwajalein',
|
||||
'Libya' => 'Africa/Tripoli',
|
||||
'MET' => 'Europe/Berlin',
|
||||
'Mexico/BajaNorte' => 'America/Tijuana',
|
||||
'Mexico/BajaSur' => 'America/Mazatlan',
|
||||
'Mexico/General' => 'America/Mexico_City',
|
||||
'MST' => 'America/Denver',
|
||||
'MST7MDT' => 'America/Denver',
|
||||
'Navajo' => 'America/Denver',
|
||||
'NZ' => 'Pacific/Auckland',
|
||||
'NZ-CHAT' => 'Pacific/Chatham',
|
||||
'Poland' => 'Europe/Warsaw',
|
||||
'Portugal' => 'Europe/Lisbon',
|
||||
'PRC' => 'Asia/Shanghai',
|
||||
'PST8PDT' => 'America/Los_Angeles',
|
||||
'ROC' => 'Asia/Taipei',
|
||||
'ROK' => 'Asia/Seoul',
|
||||
'Singapore' => 'Asia/Singapore',
|
||||
'Turkey' => 'Europe/Istanbul',
|
||||
'UCT' => 'UTC',
|
||||
'Universal' => 'UTC',
|
||||
'US/Alaska' => 'America/Anchorage',
|
||||
'US/Aleutian' => 'America/Adak',
|
||||
'US/Arizona' => 'America/Phoenix',
|
||||
'US/Central' => 'America/Chicago',
|
||||
'US/East-Indiana' => 'America/Indiana/Indianapolis',
|
||||
'US/Eastern' => 'America/New_York',
|
||||
'US/Hawaii' => 'Pacific/Honolulu',
|
||||
'US/Indiana-Starke' => 'America/Indiana/Knox',
|
||||
'US/Michigan' => 'America/Detroit',
|
||||
'US/Mountain' => 'America/Denver',
|
||||
'US/Pacific' => 'America/Los_Angeles',
|
||||
'US/Pacific-New' => 'America/Los_Angeles',
|
||||
'US/Samoa' => 'Pacific/Pago_Pago',
|
||||
'W-SU' => 'Europe/Moscow',
|
||||
'WET' => 'Europe/Berlin',
|
||||
'Z' => 'UTC',
|
||||
'Zulu' => 'UTC',
|
||||
// Some of these Etc/X zones are not deprecated, but still problematic
|
||||
'Etc/GMT' => 'UTC',
|
||||
'Etc/GMT+0' => 'UTC',
|
||||
'Etc/GMT+1' => 'Atlantic/Azores',
|
||||
'Etc/GMT+10' => 'Pacific/Honolulu',
|
||||
'Etc/GMT+11' => 'Pacific/Midway',
|
||||
'Etc/GMT+12' => 'Pacific/Auckland',
|
||||
'Etc/GMT+2' => 'America/Noronha',
|
||||
'Etc/GMT+3' => 'America/Argentina/Buenos_Aires',
|
||||
'Etc/GMT+4' => 'America/Manaus',
|
||||
'Etc/GMT+5' => 'America/New_York',
|
||||
'Etc/GMT+6' => 'America/Chicago',
|
||||
'Etc/GMT+7' => 'America/Denver',
|
||||
'Etc/GMT+8' => 'America/Los_Angeles',
|
||||
'Etc/GMT+9' => 'America/Anchorage',
|
||||
'Etc/GMT-0' => 'UTC',
|
||||
'Etc/GMT-1' => 'Europe/Berlin',
|
||||
'Etc/GMT-10' => 'Australia/Sydney',
|
||||
'Etc/GMT-11' => 'Pacific/Norfolk',
|
||||
'Etc/GMT-12' => 'Pacific/Auckland',
|
||||
'Etc/GMT-13' => 'Pacific/Apia',
|
||||
'Etc/GMT-14' => 'Pacific/Kiritimati',
|
||||
'Etc/GMT-2' => 'Africa/Cairo',
|
||||
'Etc/GMT-3' => 'Europe/Moscow',
|
||||
'Etc/GMT-4' => 'Europe/Samara',
|
||||
'Etc/GMT-5' => 'Asia/Yekaterinburg',
|
||||
'Etc/GMT-6' => 'Asia/Almaty',
|
||||
'Etc/GMT-7' => 'Asia/Bangkok',
|
||||
'Etc/GMT-8' => 'Asia/Hong_Kong',
|
||||
'Etc/GMT-9' => 'Asia/Tokyo',
|
||||
'Etc/GMT0' => 'UTC',
|
||||
'Etc/Greenwich' => 'UTC',
|
||||
'Etc/UCT' => 'UTC',
|
||||
'Etc/Universal' => 'UTC',
|
||||
'Etc/UTC' => 'UTC',
|
||||
'Etc/Zulu' => 'UTC',
|
||||
];
|
||||
|
||||
return $deprecated_timezones[$tzname] ?? $tzname;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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: |
|
||||
| PHP stream filter to detect evil content in mail attachments |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* PHP stream filter to detect html/javascript code in attachments
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_content_filter extends php_user_filter
|
||||
{
|
||||
private $buffer = '';
|
||||
private $cutoff = 2048;
|
||||
|
||||
public function onCreate(): bool
|
||||
{
|
||||
$this->cutoff = rand(2048, 3027);
|
||||
return true;
|
||||
}
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function filter($in, $out, &$consumed, $closing)
|
||||
{
|
||||
while ($bucket = stream_bucket_make_writeable($in)) {
|
||||
$this->buffer .= $bucket->data;
|
||||
|
||||
// check for evil content and abort
|
||||
if (preg_match('/<(script|iframe|object)/i', $this->buffer)) {
|
||||
return PSFS_ERR_FATAL;
|
||||
}
|
||||
|
||||
// keep buffer small enough
|
||||
if (strlen($this->buffer) > 4096) {
|
||||
$this->buffer = substr($this->buffer, $this->cutoff);
|
||||
}
|
||||
|
||||
$consumed += $bucket->datalen;
|
||||
stream_bucket_append($out, $bucket);
|
||||
}
|
||||
|
||||
return PSFS_PASS_ON;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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
|
||||
*/
|
||||
class rcube_csv2vcard
|
||||
{
|
||||
/**
|
||||
* CSV to vCard fields mapping
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $csv2vcard_map = [
|
||||
// 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' => 'email:other',
|
||||
//'email_2_type' => '',
|
||||
'email_3_address' => 'email:other',
|
||||
//'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' => 'department',
|
||||
'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',
|
||||
|
||||
// Format of Letter Hub test files from
|
||||
// https://letterhub.com/sample-csv-file-with-contacts/
|
||||
'company_name' => 'organization',
|
||||
'address' => 'street:home',
|
||||
'city' => 'locality:home',
|
||||
//'county' => '',
|
||||
'state' => 'region:home',
|
||||
'zip' => 'zipcode:home',
|
||||
'phone1' => 'phone:home',
|
||||
'phone' => 'phone:work',
|
||||
'email' => 'email:home',
|
||||
];
|
||||
|
||||
/**
|
||||
* CSV label to text mapping for English
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $label_map = [
|
||||
// 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 Number",
|
||||
'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",
|
||||
'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 = [
|
||||
'E-mail' => [
|
||||
'Value' => [
|
||||
'home' => 'email:home',
|
||||
'work' => 'email:work',
|
||||
'other' => 'email:other',
|
||||
'' => 'email:other',
|
||||
],
|
||||
],
|
||||
'Phone' => [
|
||||
'Value' => [
|
||||
'home' => 'phone:home',
|
||||
'homefax' => 'phone:homefax',
|
||||
'main' => 'phone:pref',
|
||||
'pager' => 'phone:pager',
|
||||
'mobile' => 'phone:cell',
|
||||
'work' => 'phone:work',
|
||||
'workfax' => 'phone:workfax',
|
||||
],
|
||||
],
|
||||
'Relation' => [
|
||||
'Value' => [
|
||||
'spouse' => 'spouse',
|
||||
],
|
||||
],
|
||||
'Website' => [
|
||||
'Value' => [
|
||||
'profile' => 'website:profile',
|
||||
'blog' => 'website:blog',
|
||||
'homepage' => 'website:homepage',
|
||||
'work' => 'website:work',
|
||||
],
|
||||
],
|
||||
'Address' => [
|
||||
'Street' => [
|
||||
'home' => 'street:home',
|
||||
'work' => 'street:work',
|
||||
],
|
||||
'City' => [
|
||||
'home' => 'locality:home',
|
||||
'work' => 'locality:work',
|
||||
],
|
||||
'Region' => [
|
||||
'home' => 'region:home',
|
||||
'work' => 'region:work',
|
||||
],
|
||||
'Postal Code' => [
|
||||
'home' => 'zipcode:home',
|
||||
'work' => 'zipcode:work',
|
||||
],
|
||||
'Country' => [
|
||||
'home' => 'country:home',
|
||||
'work' => 'country:work',
|
||||
],
|
||||
],
|
||||
'Organization' => [
|
||||
'Name' => [
|
||||
'' => 'organization',
|
||||
],
|
||||
'Title' => [
|
||||
'' => 'jobtitle',
|
||||
],
|
||||
'Department' => [
|
||||
'' => 'department',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** @var array Localized labels map */
|
||||
protected $local_label_map = [];
|
||||
|
||||
/** @var rcube_vcard[] List of contacts as vCards */
|
||||
protected $vcards = [];
|
||||
|
||||
/** @var array Field mapping */
|
||||
protected $map = [];
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import contacts from CSV file
|
||||
*
|
||||
* @param string $csv Content of the CSV file
|
||||
* @param bool $dry_run Generate automatic field mapping
|
||||
* @param bool $skip_head Skip header line
|
||||
*
|
||||
* @return array Field mapping info (dry run only)
|
||||
*/
|
||||
public function import($csv, $dry_run = false, $skip_head = true)
|
||||
{
|
||||
// convert to UTF-8 (supports default_charset and RCUBE_CHARSET as input)
|
||||
// TODO: If the input charset is invalid we should probably just abort here
|
||||
if ($charset = rcube_charset::check($csv)) {
|
||||
$csv = rcube_charset::convert($csv, $charset);
|
||||
}
|
||||
$csv = preg_replace(['/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'], '', $csv); // also remove BOM
|
||||
|
||||
// Split CSV file into lines
|
||||
$lines = rcube_utils::explode_quoted_string('[\r\n]+', $csv);
|
||||
|
||||
// Parse first 2 lines of file to identify fields
|
||||
// 2 lines because for gmail CSV we need to get the value from the "Type" fields to identify which is which
|
||||
if (empty($this->map)) {
|
||||
$this->parse_header(array_slice($lines, 0, 2));
|
||||
}
|
||||
|
||||
// Parse the fields
|
||||
foreach ($lines as $n => $line) {
|
||||
$elements = $this->parse_line($line);
|
||||
|
||||
if ($dry_run) {
|
||||
return ['source' => $elements, 'destination' => $this->map];
|
||||
}
|
||||
|
||||
if (empty($elements)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// first line is the headers so do not import unless explicitly set
|
||||
if (!$skip_head || $n > 0) {
|
||||
$this->csv_to_vcard($elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field mapping info
|
||||
*
|
||||
* @param array Field mapping
|
||||
*/
|
||||
public function set_map($elements)
|
||||
{
|
||||
// sanitize input
|
||||
$elements = array_filter($elements, function($val) {
|
||||
return in_array($val, $this->csv2vcard_map);
|
||||
});
|
||||
|
||||
$this->map = $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field mapping info
|
||||
*
|
||||
* @return array Array of vcard fields and localized names
|
||||
*/
|
||||
public function get_fields()
|
||||
{
|
||||
// get all vcard fields
|
||||
$fields = array_unique($this->csv2vcard_map);
|
||||
$local_field_names = $this->local_label_map ?: $this->label_map;
|
||||
$local_field_names = array_flip($local_field_names);
|
||||
|
||||
// translate with the local map
|
||||
$map = [];
|
||||
foreach ($fields as $csv => $vcard) {
|
||||
if ($vcard == '_auto_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$vcard] = $local_field_names[$csv];
|
||||
}
|
||||
|
||||
// small fix to prevent "Groups" displaying as "Categories"
|
||||
$map['groups'] = $local_field_names['groups'];
|
||||
|
||||
asort($map);
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export vCards
|
||||
*
|
||||
* @return array rcube_vcard List of vcards
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
return $this->vcards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CSV file line
|
||||
*
|
||||
* @param string $line Line of text from CSV file
|
||||
*
|
||||
* @return array CSV data extracted from the line
|
||||
*/
|
||||
protected function parse_line($line)
|
||||
{
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = str_getcsv($line);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CSV header line, detect fields mapping
|
||||
*
|
||||
* @param array $lines One or two header lines in CSV file
|
||||
*/
|
||||
protected function parse_header($lines)
|
||||
{
|
||||
$elements = $this->parse_line($lines[0]);
|
||||
|
||||
if (count($lines) == 2) {
|
||||
// first line of contents needed to properly identify fields in gmail CSV
|
||||
$contents = $this->parse_line($lines[1]);
|
||||
}
|
||||
|
||||
$map1 = [];
|
||||
$map2 = [];
|
||||
$size = count($elements);
|
||||
|
||||
// check English labels
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
if (!empty($this->label_map[$elements[$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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing recognized fallback to simple non-localized labels
|
||||
if (empty($map1) && empty($map2)) {
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
$label = str_replace(' ', '_', strtolower($elements[$i]));
|
||||
if (!empty($this->csv2vcard_map[$label])) {
|
||||
$map1[$i] = $this->csv2vcard_map[$label];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->map = count($map1) >= count($map2) ? $map1 : $map2;
|
||||
|
||||
if (!empty($contents)) {
|
||||
foreach ($this->gmail_label_map as $key => $items) {
|
||||
$num = 1;
|
||||
while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
|
||||
$type = $contents[$found];
|
||||
$type = preg_replace('/[^a-z]/', '', strtolower($type));
|
||||
|
||||
foreach ($items as $item_key => $vcard_fields) {
|
||||
$_key = "$key $num - $item_key";
|
||||
if (($found = array_search($_key, $elements)) !== false) {
|
||||
$this->map[$found] = $vcard_fields[$type];
|
||||
}
|
||||
}
|
||||
|
||||
$num++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CSV data row to vCard
|
||||
*
|
||||
* @param array $data CSV data array
|
||||
*/
|
||||
protected function csv_to_vcard($data)
|
||||
{
|
||||
$contact = [];
|
||||
|
||||
foreach ($this->map as $idx => $name) {
|
||||
if ($name == '_auto_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $data[$idx];
|
||||
if ($value !== null && $value !== '') {
|
||||
if (!empty($contact[$name])) {
|
||||
$contact[$name] = (array) $contact[$name];
|
||||
$contact[$name][] = $value;
|
||||
}
|
||||
else {
|
||||
$contact[$name] = $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'];
|
||||
}
|
||||
|
||||
if (!empty($contact['groups'])) {
|
||||
// categories/groups separator in vCard is ',' not ';'
|
||||
$contact['groups'] = str_replace(';', ',', $contact['groups']);
|
||||
|
||||
// remove "* " added by GMail
|
||||
$contact['groups'] = str_replace('* ', '', $contact['groups']);
|
||||
// replace strange delimiter added by GMail
|
||||
$contact['groups'] = str_replace(' ::: ', ',', $contact['groups']);
|
||||
}
|
||||
|
||||
// Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
|
||||
foreach (['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, ['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], ['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);
|
||||
if (is_array($value) && $name[0] != 'address') {
|
||||
foreach ((array) $value as $val) {
|
||||
$vcard->set($name[0], $val, $name[1] ?? null);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$vcard->set($name[0], $value, $name[1] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
// add to the list
|
||||
$this->vcards[] = $vcard;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 convert Enriched to HTML format (RFC 1523, 1896) |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
| Author: Ryo Chijiiwa (IlohaMail) |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for Enriched to HTML conversion
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_enriched
|
||||
{
|
||||
protected static function convert_newlines($body)
|
||||
{
|
||||
// remove single newlines, convert N newlines to N-1
|
||||
$body = str_replace("\r\n", "\n", $body);
|
||||
$len = strlen($body);
|
||||
$nl = 0;
|
||||
$out = '';
|
||||
|
||||
for ($i=0; $i<$len; $i++) {
|
||||
$c = $body[$i];
|
||||
if (ord($c) == 10) {
|
||||
$nl++;
|
||||
}
|
||||
if ($nl && ord($c) != 10) {
|
||||
$nl = 0;
|
||||
}
|
||||
if ($nl != 1) {
|
||||
$out .= $c;
|
||||
}
|
||||
else {
|
||||
$out .= ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected static function convert_formatting($body)
|
||||
{
|
||||
$replace = [
|
||||
'<bold>' => '<b>', '</bold>' => '</b>',
|
||||
'<italic>' => '<i>', '</italic>' => '</i>',
|
||||
'<fixed>' => '<tt>', '</fixed>' => '</tt>',
|
||||
'<smaller>' => '<font size=-1>', '</smaller>'=> '</font>',
|
||||
'<bigger>' => '<font size=+1>', '</bigger>' => '</font>',
|
||||
'<underline>' => '<span style="text-decoration: underline">', '</underline>' => '</span>',
|
||||
'<flushleft>' => '<span style="text-align: left">', '</flushleft>' => '</span>',
|
||||
'<flushright>' => '<span style="text-align: right">', '</flushright>' => '</span>',
|
||||
'<flushboth>' => '<span style="text-align: justified">', '</flushboth>' => '</span>',
|
||||
'<indent>' => '<span style="padding-left: 20px">', '</indent>' => '</span>',
|
||||
'<indentright>' => '<span style="padding-right: 20px">', '</indentright>' => '</span>',
|
||||
];
|
||||
|
||||
return str_ireplace(array_keys($replace), array_values($replace), $body);
|
||||
}
|
||||
|
||||
protected static function convert_font($body)
|
||||
{
|
||||
$pattern = '/(.*)\<fontfamily\>\<param\>(.*)\<\/param\>(.*)\<\/fontfamily\>(.*)/ims';
|
||||
|
||||
while (preg_match($pattern, $body, $a)) {
|
||||
if (count($a) != 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = $a[1].'<span style="font-family: '.$a[2].'">'.$a[3].'</span>'.$a[4];
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
protected static function convert_color($body)
|
||||
{
|
||||
$pattern = '/(.*)\<color\>\<param\>(.*)\<\/param\>(.*)\<\/color\>(.*)/ims';
|
||||
|
||||
while (preg_match($pattern, $body, $a)) {
|
||||
if (count($a) != 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// extract color (either by name, or ####,####,####)
|
||||
if (strpos($a[2],',')) {
|
||||
$rgb = explode(',', $a[2]);
|
||||
$color = '#';
|
||||
for ($i=0; $i<3; $i++) {
|
||||
$color .= substr($rgb[$i], 0, 2); // just take first 2 bytes
|
||||
}
|
||||
}
|
||||
else {
|
||||
$color = $a[2];
|
||||
}
|
||||
|
||||
// put it all together
|
||||
$body = $a[1].'<span style="color: '.$color.'">'.$a[3].'</span>'.$a[4];
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
protected static function convert_excerpt($body)
|
||||
{
|
||||
$pattern = '/(.*)\<excerpt\>(.*)\<\/excerpt\>(.*)/i';
|
||||
|
||||
while (preg_match($pattern, $body, $a)) {
|
||||
if (count($a) != 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quoted = '';
|
||||
$lines = explode('<br>', $a[2]);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$quoted .= '>'.$line.'<br>';
|
||||
}
|
||||
|
||||
$body = $a[1].'<span class="quotes">'.$quoted.'</span>'.$a[3];
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Enriched text into HTML format
|
||||
*
|
||||
* @param string $body Enriched text
|
||||
*
|
||||
* @return string HTML text
|
||||
*/
|
||||
public static function to_html($body)
|
||||
{
|
||||
$body = str_replace('<<','<',$body);
|
||||
$body = self::convert_newlines($body);
|
||||
$body = str_replace("\n", '<br>', $body);
|
||||
$body = self::convert_formatting($body);
|
||||
$body = self::convert_color($body);
|
||||
$body = self::convert_font($body);
|
||||
$body = self::convert_excerpt($body);
|
||||
//$body = nl2br($body);
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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
|
||||
{
|
||||
const LINKS_NONE = 0;
|
||||
const LINKS_END = 1;
|
||||
const LINKS_INLINE = 2;
|
||||
const LINKS_DEFAULT = self::LINKS_END;
|
||||
|
||||
/**
|
||||
* 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 int $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 self::$replace
|
||||
*/
|
||||
protected $search = [
|
||||
'/\r/', // Non-legal carriage return
|
||||
'/\n*<\/?html>\n*/is', // <html>
|
||||
'/\n*<head[^>]*>.*?<\/head>\n*/is', // <head>
|
||||
'/\n*<script[^>]*>.*?<\/script>\n*/is', // <script>
|
||||
'/\n*<style[^>]*>.*?<\/style>\n*/is', // <style>
|
||||
'/[\n\t]+/', // Newlines and tabs
|
||||
'/<p[^>]*>/i', // <p>
|
||||
'/<\/p>[\s\n\t]*<div[^>]*>/i', // </p> before <div>
|
||||
'/<br[^>]*>[\s\n\t]*<div[^>]*>/i', // <br> before <div>
|
||||
'/<br[^>]*>\s*/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 self::$search
|
||||
*/
|
||||
protected $replace = [
|
||||
'', // Non-legal carriage return
|
||||
'', // <html>|</html>
|
||||
'', // <head>
|
||||
'', // <script>
|
||||
'', // <style>
|
||||
' ', // Newlines and tabs
|
||||
"\n\n", // <p>
|
||||
"\n<div>", // </p> before <div>
|
||||
'<div>', // <br> before <div>
|
||||
"\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 self::$ent_replace
|
||||
*/
|
||||
protected $ent_search = [
|
||||
'/&(nbsp|#160);/i', // Non-breaking space
|
||||
'/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i', // Double quotes
|
||||
'/&(apos|rsquo|lsquo|#8216|#8217);/i', // Single quotes
|
||||
'/>/i', // Greater-than
|
||||
'/</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 self::$ent_search
|
||||
*/
|
||||
protected $ent_replace = [
|
||||
"\xC2\xA0", // 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 = [
|
||||
'/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i', // <a href="">
|
||||
'/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i', // h1 - h6
|
||||
'/<(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 self::$pre_replace
|
||||
*/
|
||||
protected $pre_search = [
|
||||
"/\n/",
|
||||
"/\t/",
|
||||
'/ /',
|
||||
'/<pre[^>]*>/',
|
||||
'/<\/pre>/'
|
||||
];
|
||||
|
||||
/**
|
||||
* List of pattern replacements corresponding to patterns searched for PRE body.
|
||||
*
|
||||
* @var array $pre_replace
|
||||
* @see self::$pre_search
|
||||
*/
|
||||
protected $pre_replace = [
|
||||
'<br>',
|
||||
' ',
|
||||
' ',
|
||||
'',
|
||||
''
|
||||
];
|
||||
|
||||
/**
|
||||
* Temp. PRE content
|
||||
*
|
||||
* @var string $pre_content
|
||||
*/
|
||||
protected $pre_content = '';
|
||||
|
||||
/**
|
||||
* Contains a list of HTML tags to allow in the resulting text.
|
||||
*
|
||||
* @var string $allowed_tags
|
||||
* @see self::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 bool $_converted
|
||||
* @see self::$html
|
||||
* @see self::$text
|
||||
*/
|
||||
protected $_converted = false;
|
||||
|
||||
/**
|
||||
* Contains URL addresses from links to be rendered in plain text.
|
||||
*
|
||||
* @var array $_link_list
|
||||
* @see self::_build_link_list()
|
||||
*/
|
||||
protected $_link_list = [];
|
||||
|
||||
/**
|
||||
* Links handling.
|
||||
* - 0 if links should be removed
|
||||
* - 1 if a table of link URLs should be listed after the text
|
||||
* - 2 if the link should be displayed to the original point in the text they appeared
|
||||
*
|
||||
* @var int $_links_mode
|
||||
*/
|
||||
protected $_links_mode = 1;
|
||||
|
||||
/**
|
||||
* 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 bool $from_file Indicates $source is a file to pull content from
|
||||
* @param bool|int $links_mode Links handling mode
|
||||
* @param int $width Maximum width of the formatted text, 0 for no limit
|
||||
*/
|
||||
function __construct($source = '', $from_file = false, $links_mode = self::LINKS_DEFAULT, $width = 75, $charset = 'UTF-8')
|
||||
{
|
||||
if (!empty($source)) {
|
||||
$this->set_html($source, $from_file);
|
||||
}
|
||||
|
||||
$this->set_base_url();
|
||||
$this->set_links_mode($links_mode);
|
||||
|
||||
$this->width = $width;
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the links behavior mode
|
||||
*
|
||||
* @param bool|int $mode
|
||||
*/
|
||||
private function set_links_mode($mode)
|
||||
{
|
||||
$allowed = [
|
||||
self::LINKS_NONE,
|
||||
self::LINKS_END,
|
||||
self::LINKS_INLINE
|
||||
];
|
||||
|
||||
if (!in_array((int) $mode, $allowed)) {
|
||||
$this->_links_mode = self::LINKS_DEFAULT;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_links_mode = (int) $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads source HTML into memory, either from $source string or a file.
|
||||
*
|
||||
* @param string $source HTML content
|
||||
* @param bool $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 = [];
|
||||
|
||||
$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 &$text Reference to HTML content string
|
||||
*/
|
||||
protected function _converter(&$text)
|
||||
{
|
||||
// Convert <BLOCKQUOTE> (before PRE!)
|
||||
$this->_convert_blockquotes($text);
|
||||
|
||||
// Convert <PRE>
|
||||
$this->_convert_pre($text);
|
||||
|
||||
// Remove body tag and anything before
|
||||
// We used to have '/^.*<body[^>]*>\n*/is' in $this->search, but this requires
|
||||
// high pcre.backtrack_limit setting when converting long HTML strings (#8137)
|
||||
if (($pos = stripos($text, '<body')) !== false) {
|
||||
$pos = strpos($text, '>', $pos);
|
||||
$text = substr($text, $pos + 1);
|
||||
$text = ltrim($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, [$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 "&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 e.g. 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 or simply the link 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 _handle_link($link, $display)
|
||||
{
|
||||
if (empty($link)) {
|
||||
return $display;
|
||||
}
|
||||
|
||||
// Ignored link types
|
||||
if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
|
||||
return $display;
|
||||
}
|
||||
|
||||
// skip links with href == content (#1490434)
|
||||
if ($link === $display) {
|
||||
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 (self::LINKS_NONE === $this->_links_mode) {
|
||||
// When not using link list use URL if there's no content (#5795)
|
||||
// The content here is HTML, convert it to text first
|
||||
$h2t = new rcube_html2text($display, false, false, 1024, $this->charset);
|
||||
$display = $h2t->get_text();
|
||||
|
||||
if (empty($display) && preg_match('!^([a-z][a-z0-9.+-]+://)!i', $link)) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
return $display;
|
||||
}
|
||||
|
||||
if (self::LINKS_INLINE === $this->_links_mode) {
|
||||
return $this->_build_link_inline($url, $display);
|
||||
}
|
||||
|
||||
return $this->_build_link_list($url, $display);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function called by _handle_link() on link replacement.
|
||||
*
|
||||
* Displays the link next to the original point in the text they
|
||||
* appeared.
|
||||
*
|
||||
* @param string $url URL of the link
|
||||
* @param string $display linktext
|
||||
*/
|
||||
protected function _build_link_inline($url, $display)
|
||||
{
|
||||
return $display . ' <' . $url . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function called by _handle_link() 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.
|
||||
*
|
||||
* @param string $url URL of the link
|
||||
* @param string $display Part of the text to associate number with
|
||||
*/
|
||||
protected function _build_link_list($url, $display)
|
||||
{
|
||||
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 &$text 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,
|
||||
[$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',
|
||||
[$this, 'pre_preg_callback'], $text, 1);
|
||||
|
||||
// free memory
|
||||
$this->pre_content = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for BLOCKQUOTE body conversion.
|
||||
*
|
||||
* @param string &$text HTML content
|
||||
*/
|
||||
protected function _convert_blockquotes(&$text)
|
||||
{
|
||||
$level = 0;
|
||||
$offset = 0;
|
||||
|
||||
while (($start = stripos($text, '<blockquote', $offset)) !== false) {
|
||||
$offset = $start + 12;
|
||||
|
||||
do {
|
||||
$end = stripos($text, '</blockquote>', $offset);
|
||||
$next = stripos($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);
|
||||
|
||||
// restore text width
|
||||
$this->width = $p_width;
|
||||
|
||||
// Add citation markers and create <pre> block
|
||||
$body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', [$this, 'blockquote_citation_callback'], trim($body));
|
||||
$body = '<pre>' . htmlspecialchars($body, ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, $this->charset) . '</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 = isset($line[0]) && $line[0] == '>' ? '' : ' ';
|
||||
|
||||
return $m[1] . '>' . $space . $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for preg_replace_callback use.
|
||||
*
|
||||
* @param array $matches PREG matches
|
||||
*
|
||||
* @return string Element content
|
||||
*/
|
||||
public function tags_preg_callback($matches)
|
||||
{
|
||||
switch (strtolower($matches[1])) {
|
||||
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->_handle_link($url, $matches[4]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for preg_replace_callback use in PRE content handler.
|
||||
*
|
||||
* @param array $matches PREG matches
|
||||
*
|
||||
* @return string PRE content
|
||||
*/
|
||||
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 containing HTML tags
|
||||
$chunks = preg_split('/(<[^>]*>)/', $str, -1, 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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
|
||||
{
|
||||
const TYPE_GIF = 1;
|
||||
const TYPE_JPG = 2;
|
||||
const TYPE_PNG = 3;
|
||||
const TYPE_TIF = 4;
|
||||
|
||||
/** @var array Image file type to extension map */
|
||||
public static $extensions = [
|
||||
self::TYPE_GIF => 'gif',
|
||||
self::TYPE_JPG => 'jpg',
|
||||
self::TYPE_PNG => 'png',
|
||||
self::TYPE_TIF => 'tif',
|
||||
];
|
||||
|
||||
/** @var string Image file location */
|
||||
private $image_file;
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $filename Image file name/path
|
||||
*/
|
||||
function __construct($filename)
|
||||
{
|
||||
$this->image_file = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image properties.
|
||||
*
|
||||
* @return array|null Hash array with image props like type, width, height
|
||||
*/
|
||||
public function props()
|
||||
{
|
||||
$gd_type = null;
|
||||
$channels = null;
|
||||
$width = null;
|
||||
$height = null;
|
||||
|
||||
// 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($gd_type, false);
|
||||
|
||||
if (isset($imsize['channels'])) {
|
||||
$channels = $imsize['channels'];
|
||||
}
|
||||
}
|
||||
|
||||
// use ImageMagick
|
||||
if (empty($type) && ($data = $this->identify())) {
|
||||
list($type, $width, $height) = $data;
|
||||
$channels = null;
|
||||
}
|
||||
|
||||
if (!empty($type)) {
|
||||
return [
|
||||
'type' => $type,
|
||||
'gd_type' => $gd_type,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'channels' => $channels,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool $browser_compat Convert to image type displayable by any browser
|
||||
*
|
||||
* @return string|false Output type on success, False on failure
|
||||
*/
|
||||
public function resize($size, $filename = null, $browser_compat = false)
|
||||
{
|
||||
$result = false;
|
||||
$rcube = rcube::get_instance();
|
||||
$convert = self::getCommand('im_convert_path');
|
||||
$props = $this->props();
|
||||
|
||||
if (empty($props)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$filename) {
|
||||
$filename = $this->image_file;
|
||||
}
|
||||
|
||||
// use Imagemagick
|
||||
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, ["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, ['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 {
|
||||
$valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif";
|
||||
|
||||
if (in_array($type, explode(',', $valid_types))) { // Valid type?
|
||||
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 += [
|
||||
'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);
|
||||
|
||||
try {
|
||||
// it throws exception on formats not supporting these features
|
||||
$image->setImageBackgroundColor('white');
|
||||
$image->setImageAlphaChannel(11);
|
||||
$image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// ignore errors
|
||||
}
|
||||
|
||||
$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'] && $props['width'] > 0 && $props['height'] > 0) {
|
||||
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);
|
||||
|
||||
if ($new_image === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 orientation of image if EXIF data exists and specifies orientation (GD strips the EXIF data)
|
||||
if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
|
||||
$exif = @exif_read_data($this->image_file);
|
||||
if ($exif && !empty($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 = self::getCommand('im_convert_path');
|
||||
|
||||
if (!$filename) {
|
||||
$filename = $this->image_file;
|
||||
|
||||
// modify extension
|
||||
if ($extension = self::$extensions[$type]) {
|
||||
$filename = preg_replace('/\.[^.]+$/', '', $filename) . '.' . $extension;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 -flatten -quality 75 {in} {type}:{out}', $p);
|
||||
|
||||
if ($result === '') {
|
||||
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 (!empty($result)) {
|
||||
@chmod($filename, 0600);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: print error to the log?
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if image format conversion is supported (for specified mimetype).
|
||||
*
|
||||
* @param string $mimetype Mimetype name
|
||||
*
|
||||
* @return bool 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) || self::getCommand('im_convert_path');
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageMagick based image properties read.
|
||||
*/
|
||||
private function identify()
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
// use ImageMagick in command line
|
||||
if ($cmd = self::getCommand('im_identify_path')) {
|
||||
$args = ['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 [
|
||||
strtolower($image->getImageFormat()),
|
||||
$image->getImageWidth(),
|
||||
$image->getImageHeight(),
|
||||
];
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have enough memory to load specified image
|
||||
*
|
||||
* @param array Hash array with image props like channels, width, height
|
||||
*
|
||||
* @return bool True if there's enough memory to process the image, False otherwise
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured command and make sure it is safe to use.
|
||||
* We cannot trust configuration, and escapeshellcmd() is useless.
|
||||
*
|
||||
* @param string $opt_name Configuration option name
|
||||
*
|
||||
* @return bool|string The command or False if not set or invalid
|
||||
*/
|
||||
private static function getCommand($opt_name)
|
||||
{
|
||||
static $error = [];
|
||||
|
||||
$cmd = rcube::get_instance()->config->get($opt_name);
|
||||
|
||||
if (empty($cmd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/^(convert|identify)(\.exe)?$/i', $cmd)) {
|
||||
return $cmd;
|
||||
}
|
||||
|
||||
// Executable must exist, also disallow network shares on Windows
|
||||
if ($cmd[0] != "\\" && file_exists($cmd)) {
|
||||
return $cmd;
|
||||
}
|
||||
|
||||
if (empty($error[$opt_name])) {
|
||||
rcube::raise_error("Invalid $opt_name: $cmd", true, false);
|
||||
$error[$opt_name] = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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
|
||||
*/
|
||||
class rcube_imap_search
|
||||
{
|
||||
/** @var array IMAP connection options */
|
||||
public $options = [];
|
||||
|
||||
/** @var rcube_imap_search_job[] Search jobs */
|
||||
protected $jobs = [];
|
||||
|
||||
/** @var int Time limit in seconds */
|
||||
protected $timelimit = 0;
|
||||
|
||||
/** @var array Search results */
|
||||
protected $results;
|
||||
|
||||
/** @var rcube_imap_generic IMAP connection object */
|
||||
protected $conn;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param array $options IMAP connection options
|
||||
* @param rcube_imap_generic $conn IMAP connection object
|
||||
*/
|
||||
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 bool $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 timelimit property
|
||||
*
|
||||
* @param int $seconds Limit in seconds
|
||||
*/
|
||||
public function set_timelimit($seconds)
|
||||
{
|
||||
$this->timelimit = $seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for previous (potentially incomplete) search results
|
||||
*
|
||||
* @param array $res Search result
|
||||
*/
|
||||
public function set_results($res)
|
||||
{
|
||||
$this->results = $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection to the IMAP server (used for single-thread mode)
|
||||
*
|
||||
* @return rcube_imap_generic IMAP connection object
|
||||
*/
|
||||
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 */
|
||||
{
|
||||
/** @var rcube_imap_search The job worker */
|
||||
public $worker;
|
||||
|
||||
/** @var string IMAP folder to search in */
|
||||
private $folder;
|
||||
|
||||
/** @var string Search criteria */
|
||||
private $search;
|
||||
|
||||
/** @var string Search charset */
|
||||
private $charset;
|
||||
|
||||
/** @var string Header field to sort by */
|
||||
private $sort_field;
|
||||
|
||||
/** @var bool True if threaded listing is active */
|
||||
private $threading;
|
||||
|
||||
/** @var rcube_result_index|rcube_result_thread Search result */
|
||||
private $result;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $folder IMAP folder to search in
|
||||
* @param string $str Search criteria
|
||||
* @param string $charset Search charset
|
||||
* @param string $sort_field Header field to sort by
|
||||
* @param bool $threading True if threaded listing is active
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the IMAP search
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$this->result = $this->search_index();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of rcube_imap::search_index()
|
||||
*
|
||||
* @return rcube_result_index|rcube_result_thread Search result
|
||||
*/
|
||||
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 (empty($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the saved search set as a array
|
||||
*
|
||||
* @return array Search set
|
||||
*/
|
||||
public function get_search_set()
|
||||
{
|
||||
return [
|
||||
$this->search,
|
||||
$this->result,
|
||||
$this->charset,
|
||||
$this->sort_field,
|
||||
$this->threading,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search result.
|
||||
*
|
||||
* @return rcube_result_index|rcube_result_thread Search result
|
||||
*/
|
||||
public function get_result()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Model class to access an LDAP directories
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage LDAP
|
||||
*/
|
||||
class rcube_ldap_generic extends Net_LDAP3
|
||||
{
|
||||
/** private properties */
|
||||
protected $cache = null;
|
||||
protected $attributes = ['dn'];
|
||||
protected $error;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param array $config Configuration
|
||||
*/
|
||||
function __construct($config = null)
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
$this->config_set('log_hook', [$this, 'log']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a connection to the LDAP server
|
||||
*/
|
||||
public function connect($host = null)
|
||||
{
|
||||
// Net_LDAP3 does not support IDNA yet
|
||||
// also parse_host() here is very Roundcube specific
|
||||
$host = rcube_utils::parse_host($host, $this->config['mail_domain']);
|
||||
$host = rcube_utils::idn_to_ascii($host);
|
||||
|
||||
return parent::connect($host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints debug/error info to the log
|
||||
*/
|
||||
public function log($level, $msg)
|
||||
{
|
||||
$msg = implode("\n", $msg);
|
||||
|
||||
switch ($level) {
|
||||
case LOG_DEBUG:
|
||||
case LOG_INFO:
|
||||
case LOG_NOTICE:
|
||||
if (!empty($this->config['debug'])) {
|
||||
rcube::write_log('ldap', $msg);
|
||||
}
|
||||
break;
|
||||
|
||||
case LOG_EMERG:
|
||||
case LOG_ALERT:
|
||||
case LOG_CRIT:
|
||||
rcube::raise_error($msg, true, true);
|
||||
break;
|
||||
|
||||
case LOG_ERR:
|
||||
case LOG_WARNING:
|
||||
$this->error = $msg;
|
||||
rcube::raise_error($msg, true, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last LDAP error occurred
|
||||
*
|
||||
* @return mixed Error message string or null if no error occurred
|
||||
*/
|
||||
function get_error()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function set_debug($dbg = true)
|
||||
{
|
||||
$this->config['debug'] = (bool) $dbg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function set_cache($cache_engine)
|
||||
{
|
||||
$this->config['cache'] = $cache_engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public static function scope2func($scope, &$ns_function = null)
|
||||
{
|
||||
return self::scope_to_function($scope, $ns_function);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function set_config($opt, $val = null)
|
||||
{
|
||||
$this->config_set($opt, $val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function add($dn, $entry)
|
||||
{
|
||||
return $this->add_entry($dn, $entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function delete($dn)
|
||||
{
|
||||
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->_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->_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->_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->_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 = ['dn'])
|
||||
{
|
||||
$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->_error("ldap_get_entries() failed with " . ldap_error($this->conn));
|
||||
return [];
|
||||
}
|
||||
|
||||
$count = $list['count'];
|
||||
unset($list['count']);
|
||||
|
||||
$this->_debug("S: $count record(s)");
|
||||
}
|
||||
else {
|
||||
$list = [];
|
||||
$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) {
|
||||
$result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
|
||||
if ($result === false) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
* @param bool $flat Convert one-element-array values into strings (not implemented)
|
||||
*
|
||||
* @return array Hash array with attributes as keys
|
||||
*/
|
||||
public static function normalize_entry($entry, $flat = false)
|
||||
{
|
||||
if (!isset($entry['count'])) {
|
||||
return $entry;
|
||||
}
|
||||
|
||||
$rec = [];
|
||||
|
||||
for ($i=0; $i < $entry['count']; $i++) {
|
||||
$attr = $entry[$i];
|
||||
if ($entry[$attr]['count'] == 1) {
|
||||
switch ($attr) {
|
||||
case 'objectclass':
|
||||
$rec[$attr] = [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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an LDAP filter string matching all words from the search string
|
||||
* in the given list of attributes.
|
||||
*
|
||||
* @param string $value Search value
|
||||
* @param mixed $attrs List of LDAP attributes to search
|
||||
* @param int $mode Matching mode:
|
||||
* 0 - partial (*abc*),
|
||||
* 1 - strict (=),
|
||||
* 2 - prefix (abc*)
|
||||
* @return string LDAP filter
|
||||
*/
|
||||
public static function fulltext_search_filter($value, $attributes, $mode = 1)
|
||||
{
|
||||
if (empty($attributes)) {
|
||||
$attributes = ['cn'];
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
$value = str_replace('*', '', $value);
|
||||
$words = $mode == 0 ? rcube_utils::tokenize_string($value, 1) : [$value];
|
||||
|
||||
// set wildcards
|
||||
$wp = $ws = '';
|
||||
if ($mode != 1) {
|
||||
$ws = '*';
|
||||
$wp = !$mode ? '*' : '';
|
||||
}
|
||||
|
||||
// search each word in all listed attributes
|
||||
foreach ($words as $word) {
|
||||
$parts = [];
|
||||
|
||||
foreach ($attributes as $attr) {
|
||||
$parts[] = "($attr=$wp" . self::quote_string($word) . "$ws)";
|
||||
}
|
||||
|
||||
$groups[] = '(|' . implode('', $parts) . ')';
|
||||
}
|
||||
|
||||
return count($groups) > 1 ? '(&' . implode('', $groups) . ')' : implode('', $groups);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,430 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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
|
||||
*/
|
||||
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 hidden recipients (Bcc)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $bcc;
|
||||
|
||||
/**
|
||||
* 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 body (RFC822.TEXT)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* IMAP part bodies
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $bodypart = [];
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/**
|
||||
* Message flags
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $flags = [];
|
||||
|
||||
/**
|
||||
* Extra flags (for the messages list)
|
||||
*
|
||||
* @var array
|
||||
* @deprecated Use $flags
|
||||
*/
|
||||
public $list_flags = [];
|
||||
|
||||
/**
|
||||
* Extra columns content (for the messages list)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $list_cols = [];
|
||||
|
||||
/**
|
||||
* Message structure
|
||||
*
|
||||
* @var rcube_message_part
|
||||
*/
|
||||
public $structure;
|
||||
|
||||
/**
|
||||
* Message thread depth
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $depth;
|
||||
|
||||
/**
|
||||
* Whether the message has references in the thread
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $has_children;
|
||||
|
||||
/**
|
||||
* Number of flagged children (in a thread)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $flagged_children;
|
||||
|
||||
/**
|
||||
* Number of unread children (in a thread)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $unread_children;
|
||||
|
||||
/**
|
||||
* UID of the message parent (in a thread)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $parent_uid;
|
||||
|
||||
/**
|
||||
* IMAP MODSEQ value
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $modseq;
|
||||
|
||||
/**
|
||||
* IMAP ENVELOPE
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $envelope;
|
||||
|
||||
/**
|
||||
* Header name to rcube_message_header object property map
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $obj_headers = [
|
||||
'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',
|
||||
'disposition-notification-to' => 'mdn_to',
|
||||
'x-confirm-reading-to' => 'mdn_to',
|
||||
'message-id' => 'messageID',
|
||||
'x-priority' => 'priority',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns header value
|
||||
*
|
||||
* @param string $name Header name
|
||||
* @param bool $decode Decode the header content
|
||||
*
|
||||
* @param string|null Header content
|
||||
*/
|
||||
public function get($name, $decode = true)
|
||||
{
|
||||
$name = strtolower($name);
|
||||
$value = null;
|
||||
|
||||
if (isset($this->obj_headers[$name]) && isset($this->{$this->obj_headers[$name]})) {
|
||||
$value = $this->{$this->obj_headers[$name]};
|
||||
}
|
||||
else if (isset($this->others[$name])) {
|
||||
$value = $this->others[$name];
|
||||
}
|
||||
|
||||
if ($decode && $value !== null) {
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $key => $val) {
|
||||
$val = 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
|
||||
*
|
||||
* @param string $name Header name
|
||||
* @param string $value Header content
|
||||
*/
|
||||
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 $arr Hash array with header values
|
||||
*
|
||||
* @return 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
|
||||
*/
|
||||
class rcube_message_header_sorter
|
||||
{
|
||||
/** @var array Message UIDs */
|
||||
private $uids = [];
|
||||
|
||||
|
||||
/**
|
||||
* 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, [$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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: |
|
||||
| Class representing a message part |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a message part
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Storage
|
||||
*/
|
||||
class rcube_message_part
|
||||
{
|
||||
/**
|
||||
* Part MIME identifier
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $mime_id = '';
|
||||
|
||||
/**
|
||||
* Content main type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $ctype_primary = 'text';
|
||||
|
||||
/**
|
||||
* Content subtype
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $ctype_secondary = 'plain';
|
||||
|
||||
/**
|
||||
* Full content type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $mimetype = 'text/plain';
|
||||
|
||||
/**
|
||||
* Real content type of a message/rfc822 part
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $real_mimetype = '';
|
||||
|
||||
/**
|
||||
* Part size in bytes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $size = 0;
|
||||
|
||||
/**
|
||||
* Part body
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* Part headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $headers = [];
|
||||
|
||||
/**
|
||||
* Sub-Parts
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $parts = [];
|
||||
|
||||
/**
|
||||
* Part Content-Id
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $content_id;
|
||||
|
||||
/**
|
||||
* Part Content-Location
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $content_location;
|
||||
|
||||
public $type;
|
||||
public $replaces = [];
|
||||
public $disposition = '';
|
||||
public $filename = '';
|
||||
public $encoding = '8bit';
|
||||
public $charset = '';
|
||||
public $d_parameters = [];
|
||||
public $ctype_parameters = [];
|
||||
|
||||
|
||||
/**
|
||||
* Clone handler.
|
||||
*/
|
||||
function __clone()
|
||||
{
|
||||
if (isset($this->parts)) {
|
||||
foreach ($this->parts as $idx => $part) {
|
||||
if (is_object($part)) {
|
||||
$this->parts[$idx] = clone $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,992 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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
|
||||
*/
|
||||
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 Character 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 rcube_mime_decode library
|
||||
*
|
||||
* @param string $raw_body The message source
|
||||
*
|
||||
* @return object rcube_message_part The message structure
|
||||
*/
|
||||
public static function parse_message($raw_body)
|
||||
{
|
||||
$conf = [
|
||||
'include_bodies' => true,
|
||||
'decode_bodies' => true,
|
||||
'decode_headers' => false,
|
||||
'default_charset' => self::get_charset(),
|
||||
];
|
||||
|
||||
$mime = new rcube_mime_decode($conf);
|
||||
|
||||
return $mime->decode($raw_body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split an address list into a structured array list
|
||||
*
|
||||
* @param string|array $input Input string (or list of strings)
|
||||
* @param int $max List only this number of addresses
|
||||
* @param bool $decode Decode address strings
|
||||
* @param string $fallback Fallback charset if none specified
|
||||
* @param bool $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 common case when the same header is used many times in a mail message
|
||||
if (is_array($input)) {
|
||||
$input = implode(', ', $input);
|
||||
}
|
||||
|
||||
$a = self::parse_address_list((string) $input, $decode, $fallback);
|
||||
$out = [];
|
||||
$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']);
|
||||
$string = '';
|
||||
|
||||
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] = ['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 = $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 = [];
|
||||
$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 (!empty($matches[$idx+1]) && ($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') {
|
||||
$rest = '';
|
||||
// base64 must be decoded a segment at a time.
|
||||
// However, there are broken implementations that continue
|
||||
// in the following word, we'll handle that (#6048)
|
||||
for ($i=0; $i<$count; $i++) {
|
||||
$chunk = $rest . $tmp[$i];
|
||||
$length = strlen($chunk);
|
||||
if ($length % 4) {
|
||||
$length = floor($length / 4) * 4;
|
||||
$rest = substr($chunk, $length);
|
||||
$chunk = substr($chunk, 0, $length);
|
||||
}
|
||||
|
||||
$text .= base64_decode($chunk);
|
||||
}
|
||||
}
|
||||
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 = [];
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
public static function parse_headers($headers)
|
||||
{
|
||||
$result = [];
|
||||
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
|
||||
$lines = explode("\n", $headers);
|
||||
$count = count($lines);
|
||||
|
||||
for ($i=0; $i<$count; $i++) {
|
||||
if ($p = strpos($lines[$i], ': ')) {
|
||||
$field = strtolower(substr($lines[$i], 0, $p));
|
||||
$value = trim(substr($lines[$i], $p+1));
|
||||
if (!empty($value)) {
|
||||
$result[$field] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* E-mail address list parser
|
||||
*/
|
||||
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);
|
||||
|
||||
// simplified regexp, supporting quoted local part
|
||||
$email_rx = '([^\s:]+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($str as $key => $val) {
|
||||
$name = '';
|
||||
$address = '';
|
||||
$val = trim($val);
|
||||
|
||||
// First token might be a group name, ignore it
|
||||
$tokens = self::explode_header_string(' ', $val);
|
||||
if (isset($tokens[0]) && $tokens[0][strlen($tokens[0])-1] == ':') {
|
||||
$val = substr($val, strlen($tokens[0]));
|
||||
}
|
||||
|
||||
if (preg_match('/(.*)<('.$email_rx.')$/', $val, $m)) {
|
||||
// Note: There are cases like "Test<test@domain.tld" with no closing bracket,
|
||||
// therefor we do not include it in the regexp above, but we have to
|
||||
// remove it later, because $email_rx will catch it (#8164)
|
||||
$address = rtrim($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;
|
||||
}
|
||||
|
||||
// unquote and/or decode name
|
||||
if ($name) {
|
||||
// An unquoted name ending with colon is a address group name, ignore it
|
||||
if ($name[strlen($name)-1] == ':') {
|
||||
$name = '';
|
||||
}
|
||||
|
||||
if (strlen($name) > 1 && $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 (strlen($name) > 1 && $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] = ['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 = [];
|
||||
$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 $mark Mark each flowed line with specified character
|
||||
* @param bool $delsp Remove the trailing space of each flowed line
|
||||
*
|
||||
* @return string Interpreted text with unwrapped lines and stuffed space removed
|
||||
*/
|
||||
public static function unfold_flowed($text, $mark = null, $delsp = false)
|
||||
{
|
||||
$text = preg_split('/\r?\n/', $text);
|
||||
$last = -1;
|
||||
$q_level = 0;
|
||||
$marks = [];
|
||||
|
||||
foreach ($text as $idx => $line) {
|
||||
if ($q = strspn($line, '>')) {
|
||||
// remove quote chars
|
||||
$line = substr($line, $q);
|
||||
// remove (optional) space-staffing
|
||||
if (isset($line[0]) && $line[0] === ' ') {
|
||||
$line = substr($line, 1);
|
||||
}
|
||||
|
||||
// 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])
|
||||
) {
|
||||
if ($delsp) {
|
||||
$text[$last] = substr($text[$last], 0, -1);
|
||||
}
|
||||
$text[$last] .= $line;
|
||||
unset($text[$idx]);
|
||||
|
||||
if ($mark) {
|
||||
$marks[$last] = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$last = $idx;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($line == '-- ') {
|
||||
$last = $idx;
|
||||
}
|
||||
else {
|
||||
// remove space-stuffing
|
||||
if (isset($line[0]) && $line[0] === ' ') {
|
||||
$line = substr($line, 1);
|
||||
}
|
||||
|
||||
$last_len = isset($text[$last]) ? strlen($text[$last]) : 0;
|
||||
|
||||
if (
|
||||
$last_len && $line && !$q_level && $text[$last] != '-- '
|
||||
&& isset($text[$last][$last_len-1]) && $text[$last][$last_len-1] == ' '
|
||||
) {
|
||||
if ($delsp) {
|
||||
$text[$last] = substr($text[$last], 0, -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 $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 ($level = strspn($line, '>')) {
|
||||
// remove quote chars
|
||||
$line = substr($line, $level);
|
||||
// remove (optional) space-staffing and spaces before the line end
|
||||
$line = rtrim($line, ' ');
|
||||
if (isset($line[0]) && $line[0] === ' ') {
|
||||
$line = substr($line, 1);
|
||||
}
|
||||
|
||||
$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) {
|
||||
$charset = rcube_charset::parse_charset($charset);
|
||||
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 = [];
|
||||
|
||||
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) {
|
||||
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 bool $is_stream Set to True if $path contains file contents
|
||||
* @param bool $skip_suffix Set to True if the config/mimetypes.php map 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)
|
||||
{
|
||||
$mime_type = null;
|
||||
$config = rcube::get_instance()->config;
|
||||
|
||||
// Detect mimetype using filename extension
|
||||
if (!$skip_suffix) {
|
||||
$mime_type = self::file_ext_type($name);
|
||||
}
|
||||
|
||||
// try fileinfo extension if available
|
||||
if (!$mime_type && function_exists('finfo_open')) {
|
||||
$mime_magic = $config->get('mime_magic');
|
||||
// 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) {
|
||||
$func = $is_stream ? 'finfo_buffer' : 'finfo_file';
|
||||
$mime_type = $func($finfo, $path, FILEINFO_MIME_TYPE);
|
||||
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;
|
||||
}
|
||||
|
||||
return $mime_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* File type detection based on file name only.
|
||||
*
|
||||
* @param string $filename Path to the file or file contents
|
||||
*
|
||||
* @return string|null Mimetype label
|
||||
*/
|
||||
public static function file_ext_type($filename)
|
||||
{
|
||||
static $mime_ext = [];
|
||||
|
||||
if (empty($mime_ext)) {
|
||||
foreach (rcube::get_instance()->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 (!empty($mime_ext) && $filename) {
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if ($ext && !empty($mime_ext[$ext])) {
|
||||
return $mime_ext[$ext];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function get_mime_extensions($mimetype = null)
|
||||
{
|
||||
static $mime_types, $mime_extensions;
|
||||
|
||||
// return cached data
|
||||
if (is_array($mime_types)) {
|
||||
return $mimetype ? (isset($mime_types[$mimetype]) ? $mime_types[$mimetype] : []) : $mime_extensions;
|
||||
}
|
||||
|
||||
// load mapping file
|
||||
$file_paths = [];
|
||||
|
||||
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';
|
||||
$file_paths[] = '/usr/local/etc/apache24/mime.types';
|
||||
}
|
||||
|
||||
$mime_types = [];
|
||||
$mime_extensions = [];
|
||||
$lines = [];
|
||||
$regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
|
||||
|
||||
foreach ($file_paths as $fp) {
|
||||
if (@is_readable($fp)) {
|
||||
$lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($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 = [
|
||||
'image/gif' => ['gif'],
|
||||
'image/png' => ['png'],
|
||||
'image/x-png' => ['png'],
|
||||
'image/jpeg' => ['jpg', 'jpeg', 'jpe'],
|
||||
'image/jpg' => ['jpg', 'jpeg', 'jpe'],
|
||||
'image/pjpeg' => ['jpg', 'jpeg', 'jpe'],
|
||||
'image/tiff' => ['tif'],
|
||||
'image/bmp' => ['bmp'],
|
||||
'image/x-ms-bmp' => ['bmp'],
|
||||
'message/rfc822' => ['eml'],
|
||||
'text/x-mail' => ['eml'],
|
||||
];
|
||||
|
||||
foreach ($aliases as $mime => $exts) {
|
||||
if (isset($mime_types[$mime])) {
|
||||
$mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
|
||||
}
|
||||
else {
|
||||
$mime_types[$mime] = $exts;
|
||||
}
|
||||
|
||||
foreach ($exts as $ext) {
|
||||
if (!isset($mime_extensions[$ext])) {
|
||||
$mime_extensions[$ext] = $mime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($mimetype) {
|
||||
return !empty($mime_types[$mimetype]) ? $mime_types[$mimetype] : [];
|
||||
}
|
||||
|
||||
return $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 (isset($part[0]) && $part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
|
||||
$parts[$idx] = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
return implode('@', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix mimetype name.
|
||||
*
|
||||
* @param string $type Mimetype
|
||||
*
|
||||
* @return string Mimetype
|
||||
*/
|
||||
public static function fix_mimetype($type)
|
||||
{
|
||||
$type = strtolower(trim($type));
|
||||
$aliases = [
|
||||
'image/x-ms-bmp' => 'image/bmp', // #4771
|
||||
'pdf' => 'application/pdf', // #6816
|
||||
];
|
||||
|
||||
if (!empty($aliases[$type])) {
|
||||
return $aliases[$type];
|
||||
}
|
||||
|
||||
// Some versions of Outlook create garbage Content-Type:
|
||||
// application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608
|
||||
if (preg_match('/^application\/pdf.+/', $type)) {
|
||||
return 'application/pdf';
|
||||
}
|
||||
|
||||
// treat image/pjpeg (image/pjpg, image/jpg) as image/jpeg (#4196)
|
||||
if (preg_match('/^image\/p?jpe?g$/', $type)) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 derived from Mail_mimeDecode |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
| Author: Richard Heyes <richard@phpguru.org> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for parsing MIME messages
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Storage
|
||||
*/
|
||||
class rcube_mime_decode
|
||||
{
|
||||
/**
|
||||
* Class configuration parameters.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $params = [
|
||||
'include_bodies' => true,
|
||||
'decode_bodies' => true,
|
||||
'decode_headers' => true,
|
||||
'crlf' => "\r\n",
|
||||
'default_charset' => RCUBE_CHARSET,
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Sets up the object, initialize the variables, and splits and
|
||||
* stores the header and body of the input.
|
||||
*
|
||||
* @param array $params An array of various parameters that determine
|
||||
* various things:
|
||||
* include_bodies - Whether to include the body in the returned
|
||||
* object.
|
||||
* decode_bodies - Whether to decode the bodies
|
||||
* of the parts. (Transfer encoding)
|
||||
* decode_headers - Whether to decode headers
|
||||
* crlf - CRLF type to use (CRLF/LF/CR)
|
||||
*/
|
||||
public function __construct($params = [])
|
||||
{
|
||||
if (!empty($params)) {
|
||||
$this->params = array_merge($this->params, (array) $params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the decoding process.
|
||||
*
|
||||
* @param string $input The input to decode
|
||||
* @param bool $convert Convert result to rcube_message_part structure
|
||||
*
|
||||
* @return object|bool Decoded results or False on failure
|
||||
*/
|
||||
public function decode($input, $convert = true)
|
||||
{
|
||||
list($header, $body) = $this->splitBodyHeader($input);
|
||||
|
||||
$struct = $this->do_decode($header, $body);
|
||||
|
||||
if ($struct && $convert) {
|
||||
$struct = $this->structure_part($struct);
|
||||
}
|
||||
|
||||
if ($struct) {
|
||||
$struct->size = strlen($input);
|
||||
}
|
||||
|
||||
return $struct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the decoding. Decodes the body string passed to it
|
||||
* If it finds certain content-types it will call itself in a
|
||||
* recursive fashion
|
||||
*
|
||||
* @param string $headers Header section
|
||||
* @param string $body Body section
|
||||
* @param string $default_ctype Default content type
|
||||
*
|
||||
* @return object|bool Decoded results or False on error
|
||||
*/
|
||||
protected function do_decode($headers, $body, $default_ctype = 'text/plain')
|
||||
{
|
||||
$return = new rcube_message_part;
|
||||
$headers = $this->parseHeaders($headers);
|
||||
|
||||
foreach ($headers as $value) {
|
||||
$header_name = strtolower($value['name']);
|
||||
|
||||
if (isset($return->headers[$header_name]) && !is_array($return->headers[$header_name])) {
|
||||
$return->headers[$header_name] = [$return->headers[$header_name]];
|
||||
$return->headers[$header_name][] = $value['value'];
|
||||
}
|
||||
else if (isset($return->headers[$header_name])) {
|
||||
$return->headers[$header_name][] = $value['value'];
|
||||
}
|
||||
else {
|
||||
$return->headers[$header_name] = $value['value'];
|
||||
}
|
||||
|
||||
switch ($header_name) {
|
||||
case 'content-type':
|
||||
$content_type = $this->parseHeaderValue($value['value']);
|
||||
|
||||
if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
|
||||
$return->ctype_primary = $regs[1];
|
||||
$return->ctype_secondary = $regs[2];
|
||||
}
|
||||
|
||||
if (!empty($content_type['other'])) {
|
||||
$return->ctype_parameters = array_merge((array) $return->ctype_parameters, (array) $content_type['other']);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'content-disposition';
|
||||
$content_disposition = $this->parseHeaderValue($value['value']);
|
||||
$return->disposition = $content_disposition['value'];
|
||||
|
||||
if (!empty($content_disposition['other'])) {
|
||||
$return->d_parameters = array_merge((array) $return->d_parameters, (array) $content_disposition['other']);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'content-transfer-encoding':
|
||||
$content_transfer_encoding = $this->parseHeaderValue($value['value']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($content_type)) {
|
||||
$ctype = strtolower($content_type['value']);
|
||||
|
||||
switch ($ctype) {
|
||||
case 'text/plain':
|
||||
$encoding = $content_transfer_encoding['value'] ?? '7bit';
|
||||
|
||||
if ($this->params['include_bodies']) {
|
||||
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $encoding) : $body;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'text/html':
|
||||
$encoding = $content_transfer_encoding['value'] ?? '7bit';
|
||||
|
||||
if ($this->params['include_bodies']) {
|
||||
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $encoding) : $body;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'multipart/digest':
|
||||
case 'multipart/alternative':
|
||||
case 'multipart/related':
|
||||
case 'multipart/mixed':
|
||||
case 'multipart/signed':
|
||||
case 'multipart/encrypted':
|
||||
if (!isset($content_type['other']['boundary'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$default_ctype = $ctype === 'multipart/digest' ? 'message/rfc822' : 'text/plain';
|
||||
$parts = $this->boundarySplit($body, $content_type['other']['boundary']);
|
||||
|
||||
for ($i = 0; $i < count($parts); $i++) {
|
||||
list($part_header, $part_body) = $this->splitBodyHeader($parts[$i]);
|
||||
$return->parts[] = $this->do_decode($part_header, $part_body, $default_ctype);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'message/rfc822':
|
||||
$obj = new rcube_mime_decode($this->params);
|
||||
$return->parts[] = $obj->decode($body, false);
|
||||
unset($obj);
|
||||
|
||||
if ($this->params['include_bodies']) {
|
||||
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body) : $body;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($this->params['include_bodies']) {
|
||||
$encoding = !empty($content_transfer_encoding['value']) ? $content_transfer_encoding['value'] : '7bit';
|
||||
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $encoding) : $body;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$ctype = explode('/', $default_ctype);
|
||||
$return->ctype_primary = $ctype[0];
|
||||
$return->ctype_secondary = $ctype[1];
|
||||
|
||||
if ($this->params['include_bodies']) {
|
||||
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body) : $body;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string containing a header and body
|
||||
* section, this function will split them (at the first
|
||||
* blank line) and return them.
|
||||
*
|
||||
* @param string $input Input to split apart
|
||||
*
|
||||
* @return array Contains header and body section
|
||||
*/
|
||||
protected function splitBodyHeader($input)
|
||||
{
|
||||
$pos = strpos($input, $this->params['crlf'] . $this->params['crlf']);
|
||||
if ($pos === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$crlf_len = strlen($this->params['crlf']);
|
||||
$header = substr($input, 0, $pos);
|
||||
$body = substr($input, $pos + 2 * $crlf_len);
|
||||
|
||||
if (substr_compare($body, $this->params['crlf'], -$crlf_len) === 0) {
|
||||
$body = substr($body, 0, -$crlf_len);
|
||||
}
|
||||
|
||||
return [$header, $body];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse headers given in $input and return as assoc array.
|
||||
*
|
||||
* @param string $input Headers to parse
|
||||
*
|
||||
* @return array Contains parsed headers
|
||||
*/
|
||||
protected function parseHeaders($input)
|
||||
{
|
||||
$return = [];
|
||||
|
||||
if ($input !== '') {
|
||||
// Unfold the input
|
||||
$input = preg_replace('/' . $this->params['crlf'] . "(\t| )/", ' ', $input);
|
||||
$headers = explode($this->params['crlf'], trim($input));
|
||||
|
||||
foreach ($headers as $value) {
|
||||
$hdr_name = substr($value, 0, $pos = strpos($value, ':'));
|
||||
$hdr_value = substr($value, $pos + 1);
|
||||
|
||||
if (isset($hdr_value[0]) && $hdr_value[0] == ' ') {
|
||||
$hdr_value = substr($hdr_value, 1);
|
||||
}
|
||||
|
||||
$return[] = [
|
||||
'name' => $hdr_name,
|
||||
'value' => $this->params['decode_headers'] ? $this->decodeHeader($hdr_value) : $hdr_value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to parse a header value, extract first part, and any secondary
|
||||
* parts (after ;) This function is not as robust as it could be.
|
||||
* Eg. header comments in the wrong place will probably break it.
|
||||
*
|
||||
* @param string $input Header value to parse
|
||||
*
|
||||
* @return array Contains parsed result
|
||||
*/
|
||||
protected function parseHeaderValue($input)
|
||||
{
|
||||
$parts = preg_split('/;\s*/', $input);
|
||||
$return = [];
|
||||
|
||||
if (!empty($parts)) {
|
||||
$return['value'] = trim($parts[0]);
|
||||
|
||||
for ($n = 1; $n < count($parts); $n++) {
|
||||
if (preg_match('/^([[:alnum:]]+)="?([^"]*)"?+/', $parts[$n], $matches)) {
|
||||
$return['other'][strtolower($matches[1])] = $matches[2];
|
||||
}
|
||||
// Support RFC2231 encoding
|
||||
else if (preg_match('/^([[:alnum:]]+)\*([0-9]*)\*?="*([^"]+)"*/', $parts[$n], $matches)) {
|
||||
$key = strtolower($matches[1]);
|
||||
$val = $matches[3];
|
||||
|
||||
if (preg_match("/^(([^']*)'[^']*')/", $val, $m)) {
|
||||
$val = rawurldecode(substr($val, strlen($m[0])));
|
||||
}
|
||||
|
||||
if (isset($return['other'][$key])) {
|
||||
$return['other'][$key] .= $val;
|
||||
}
|
||||
else {
|
||||
$return['other'][$key] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$return['value'] = trim($input);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function splits the input based on the given boundary
|
||||
*
|
||||
* @param string $input Input to parse
|
||||
* @param string $boundary Boundary
|
||||
*
|
||||
* @return array Contains array of resulting mime parts
|
||||
*/
|
||||
protected function boundarySplit($input, $boundary)
|
||||
{
|
||||
$tmp = explode('--' . $boundary, $input);
|
||||
$parts = [];
|
||||
|
||||
for ($i = 1; $i < count($tmp)-1; $i++) {
|
||||
$parts[] = $tmp[$i];
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a header, this function will decode it according to RFC2047.
|
||||
* Probably not *exactly* conformant, but it does pass all the given
|
||||
* examples (in RFC2047).
|
||||
*
|
||||
* @param string $input Input header value to decode
|
||||
*
|
||||
* @return string Decoded header value
|
||||
*/
|
||||
protected function decodeHeader($input)
|
||||
{
|
||||
return rcube_mime::decode_mime_string($input, $this->params['default_charset']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive method to convert a rcube_mime_decode structure
|
||||
* into a rcube_message_part object.
|
||||
*
|
||||
* @param object $part A message part struct
|
||||
* @param int $count Part count
|
||||
* @param string $parent Parent MIME ID
|
||||
*
|
||||
* @return object rcube_message_part
|
||||
* @see self::decode()
|
||||
*/
|
||||
protected function structure_part($part, $count = 0, $parent = '')
|
||||
{
|
||||
$struct = new rcube_message_part;
|
||||
$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 (!empty($part->headers['content-transfer-encoding'])) {
|
||||
$struct->encoding = $part->headers['content-transfer-encoding'];
|
||||
}
|
||||
|
||||
if (!empty($part->ctype_parameters['charset'])) {
|
||||
$struct->charset = $part->ctype_parameters['charset'];
|
||||
}
|
||||
|
||||
// determine filename
|
||||
if (!empty($part->d_parameters['filename'])) {
|
||||
$filename = $part->d_parameters['filename'];
|
||||
}
|
||||
else if (!empty($part->ctype_parameters['name'])) {
|
||||
$filename = $part->ctype_parameters['name'];
|
||||
}
|
||||
|
||||
if (!empty($filename)) {
|
||||
if (empty($this->params['decode_headers'])) {
|
||||
$filename = $this->decodeHeader($filename);
|
||||
}
|
||||
|
||||
$struct->filename = $filename;
|
||||
}
|
||||
|
||||
$struct->body = $part->body;
|
||||
$struct->size = is_string($part->body) ? strlen($part->body) : 0;
|
||||
$struct->disposition = $part->disposition;
|
||||
|
||||
$count = 0;
|
||||
foreach ((array) $part->parts as $child_part) {
|
||||
$struct->parts[] = $this->structure_part($child_part, ++$count, $struct->mime_id);
|
||||
}
|
||||
|
||||
return $struct;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 = [];
|
||||
protected $skins = [];
|
||||
|
||||
|
||||
/**
|
||||
* 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 some members
|
||||
switch ($var) {
|
||||
case 'env': return $this->env;
|
||||
case 'skins': return $this->skins;
|
||||
case 'charset': return $this->charset;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all stored env variables and commands
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->env = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool $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 array|string $p Either a string with the action or url parameters as key-value pairs
|
||||
* @param int $delay Delay in seconds
|
||||
*/
|
||||
abstract function redirect($p = [], $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");
|
||||
|
||||
// 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()) {
|
||||
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/privacy headers
|
||||
*
|
||||
* @param bool $privacy Enable privacy headers
|
||||
*/
|
||||
public function common_headers($privacy = true)
|
||||
{
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
|
||||
// Unlock IE compatibility mode
|
||||
if ($this->browser->ie) {
|
||||
$headers['X-UA-Compatible'] = 'IE=edge';
|
||||
}
|
||||
|
||||
if ($privacy) {
|
||||
// Request browser to disable DNS prefetching (CVE-2010-0464)
|
||||
$headers['X-DNS-Prefetch-Control'] = 'off';
|
||||
|
||||
// Request browser disable Referer (sic) header
|
||||
$headers['Referrer-Policy'] = 'same-origin';
|
||||
}
|
||||
|
||||
// send CSRF and clickjacking protection headers
|
||||
if ($xframe = $this->app->config->get('x_frame_options', 'sameorigin')) {
|
||||
$headers['X-Frame-Options'] = $xframe;
|
||||
}
|
||||
|
||||
$plugin = $this->app->plugins->exec_hook('common_headers', ['headers' => $headers, 'privacy' => $privacy]);
|
||||
|
||||
foreach ($plugin['headers'] as $header => $value) {
|
||||
header("$header: $value");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send headers related to file downloads
|
||||
*
|
||||
* @param string $filename File name
|
||||
* @param array $params Optional parameters:
|
||||
* type - File content type (default: 'application/octet-stream')
|
||||
* disposition - Download type: 'inline' or 'attachment' (default)
|
||||
* length - Content length
|
||||
* charset - File name character set
|
||||
* type_charset - Content character set
|
||||
* time_limit - Script execution limit (default: 3600)
|
||||
*/
|
||||
public function download_headers($filename, $params = [])
|
||||
{
|
||||
if (empty($params['disposition'])) {
|
||||
$params['disposition'] = 'attachment';
|
||||
}
|
||||
|
||||
if ($params['disposition'] == 'inline' && stripos($params['type'], 'text') === 0) {
|
||||
$params['type'] .= '; charset=' . ($params['type_charset'] ?: $this->charset);
|
||||
}
|
||||
|
||||
header("Content-Type: " . (!empty($params['type']) ? $params['type'] : "application/octet-stream"));
|
||||
|
||||
if ($params['disposition'] == 'attachment' && $this->browser->ie) {
|
||||
header("Content-Type: application/force-download");
|
||||
}
|
||||
|
||||
$disposition = "Content-Disposition: " . $params['disposition'];
|
||||
|
||||
// For non-ascii characters we'll use RFC2231 syntax
|
||||
if (!preg_match('/[^a-zA-Z0-9_.:,?;@+ -]/', $filename)) {
|
||||
$disposition .= sprintf("; filename=\"%s\"", $filename);
|
||||
}
|
||||
else {
|
||||
$disposition .= sprintf("; filename*=%s''%s",
|
||||
!empty($params['charset']) ? $params['charset'] : $this->charset,
|
||||
rawurlencode($filename)
|
||||
);
|
||||
}
|
||||
|
||||
header($disposition);
|
||||
|
||||
if (isset($params['length'])) {
|
||||
header("Content-Length: " . $params['length']);
|
||||
}
|
||||
|
||||
// don't kill the connection if download takes more than 30 sec.
|
||||
if (!array_key_exists('time_limit', $params)) {
|
||||
$params['time_limit'] = 3600;
|
||||
}
|
||||
|
||||
if (is_numeric($params['time_limit'])) {
|
||||
@set_time_limit($params['time_limit']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
fwrite(STDERR, "Error $code: $message\n");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an edit field for inclusion on a form
|
||||
*
|
||||
* @param string $name Field name
|
||||
* @param string $value Field value
|
||||
* @param array $attrib HTML element attributes for the field
|
||||
* @param string $type HTML element type (default 'text')
|
||||
*
|
||||
* @return string HTML field definition
|
||||
*/
|
||||
public static function get_edit_field($name, $value, $attrib = [], $type = 'text')
|
||||
{
|
||||
static $colcounts = [];
|
||||
|
||||
$fname = '_' . $name;
|
||||
$attrib['name'] = $fname . (!empty($attrib['array']) ? '[]' : '');
|
||||
$attrib['class'] = trim((!empty($attrib['class']) ? $attrib['class'] : '') . ' ff_' . $name);
|
||||
|
||||
if ($type == 'checkbox') {
|
||||
$attrib['value'] = '1';
|
||||
$input = new html_checkbox($attrib);
|
||||
}
|
||||
else if ($type == 'textarea') {
|
||||
if (!empty($attrib['size'])) {
|
||||
$attrib['cols'] = $attrib['size'];
|
||||
}
|
||||
$input = new html_textarea($attrib);
|
||||
}
|
||||
else if ($type == 'select') {
|
||||
$input = new html_select($attrib);
|
||||
if (empty($attrib['skip-empty'])) {
|
||||
$input->add('---', '');
|
||||
}
|
||||
if (!empty($attrib['options'])) {
|
||||
$input->add(array_values($attrib['options']), array_keys($attrib['options']));
|
||||
}
|
||||
}
|
||||
else if ($type == 'password' || (isset($attrib['type']) && $attrib['type'] == 'password')) {
|
||||
$input = new html_passwordfield($attrib);
|
||||
}
|
||||
else {
|
||||
if (!isset($attrib['type']) || ($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);
|
||||
if (!empty($attrib['array'])) {
|
||||
if (!isset($colcounts[$name])) {
|
||||
$colcounts[$name] = 0;
|
||||
}
|
||||
$idx = intval($colcounts[$name]++);
|
||||
$value = $postvalue[$idx] ?? null;
|
||||
}
|
||||
else {
|
||||
$value = $postvalue;
|
||||
}
|
||||
}
|
||||
|
||||
return $input->show($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a variable into a javascript object notation
|
||||
*
|
||||
* @param mixed $input Input value
|
||||
* @param bool $pretty Enable JSON formatting
|
||||
* @param bool $inline Enable inline mode (generates output safe for use inside HTML)
|
||||
*
|
||||
* @return string Serialized JSON string
|
||||
*/
|
||||
public static function json_serialize($input, $pretty = false, $inline = true)
|
||||
{
|
||||
$options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_IGNORE;
|
||||
|
||||
// JSON_HEX_TAG is needed for inlining JSON inside of the <script> tag
|
||||
// if input contains a html tag it will cause issues (#6207)
|
||||
if ($inline) {
|
||||
$options |= JSON_HEX_TAG;
|
||||
}
|
||||
|
||||
if ($pretty) {
|
||||
$options |= JSON_PRETTY_PRINT;
|
||||
}
|
||||
|
||||
return json_encode($input, $options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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;
|
||||
|
||||
/** @var string Plugin directory location */
|
||||
protected $home;
|
||||
|
||||
/** @var string Base URL to the plugin directory */
|
||||
protected $urlbase;
|
||||
|
||||
/** @var string Plugin task name (if registered) */
|
||||
private $mytask;
|
||||
|
||||
/** @var array List of plugin configuration files already loaded */
|
||||
private $loaded_config = [];
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 bool 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 bool 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 bool 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 = slashify($this->home) . $fname;
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) {
|
||||
rcube::raise_error([
|
||||
'code' => 527, '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)
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
$texts = $rcube->read_localization(realpath(slashify($this->home) . $dir));
|
||||
|
||||
// prepend domain to text keys and add to the application texts repository
|
||||
if (!empty($texts)) {
|
||||
$domain = $this->ID;
|
||||
$add = [];
|
||||
|
||||
foreach ($texts as $key => $value) {
|
||||
$add[$domain.'.'.$key] = $value;
|
||||
}
|
||||
|
||||
$rcube->load_language($_SESSION['language'], $add);
|
||||
|
||||
// add labels to client
|
||||
if ($add2client && method_exists($rcube->output, 'add_label')) {
|
||||
if (is_array($add2client)) {
|
||||
$js_labels = array_map([$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(...$args)
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
if (method_exists($rcube->output, 'add_label')) {
|
||||
if (count($args) == 1 && is_array($args[0])) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
$args = array_map([$this, 'label_map_callback'], $args);
|
||||
$rcube->output->add_label($args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for rcube::gettext() adding the plugin ID as domain
|
||||
*
|
||||
* @param string|array $p Named parameters array or label name
|
||||
*
|
||||
* @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 javascript 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_template::button()
|
||||
*/
|
||||
public function add_button($p, $container)
|
||||
{
|
||||
if ($this->api->output->type == 'html') {
|
||||
// fix relative paths
|
||||
foreach (['imagepas', 'imageact', 'imagesel'] as $key) {
|
||||
if (!empty($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)
|
||||
{
|
||||
// pattern "skins/[a-z0-9-_]+/plugins/$this->ID/" used to identify plugin resources loaded from the core skin folder
|
||||
if ($fn[0] != '/' && !preg_match("#^(https?://|skins/[a-z0-9-_]+/plugins/$this->ID/)#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.
|
||||
*
|
||||
* @param string $extra_dir Additional directory to search in (optional)
|
||||
* @param mixed $skin_name Specific skin name(s) to look for, string or array (optional)
|
||||
* @return string Skin path relative to plugins directory
|
||||
*/
|
||||
public function local_skin_path($extra_dir = null, $skin_name = null)
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
$skins = array_keys((array)$rcube->output->skins);
|
||||
$skin_path = '';
|
||||
|
||||
if (empty($skins)) {
|
||||
$skins = (array) $rcube->config->get('skin');
|
||||
}
|
||||
|
||||
$dirs = ['skins'];
|
||||
if (!empty($extra_dir)) {
|
||||
array_unshift($dirs, $extra_dir);
|
||||
}
|
||||
|
||||
if (!empty($skin_name)) {
|
||||
$skins = (array) $skin_name;
|
||||
}
|
||||
|
||||
foreach ($skins as $skin) {
|
||||
foreach ($dirs as $dir) {
|
||||
// skins folder in the plugins dir
|
||||
$skin_path = $dir . '/' . $skin;
|
||||
|
||||
if (!is_dir(realpath(slashify($this->home) . $skin_path))) {
|
||||
// plugins folder in the skins dir
|
||||
$skin_path .= '/plugins/' . $this->ID;
|
||||
if (is_dir(realpath(slashify(RCUBE_INSTALL_PATH) . $skin_path))) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
else {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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 loaded 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;
|
||||
|
||||
/** @var string */
|
||||
public $dir;
|
||||
/** @var string */
|
||||
public $url = 'plugins/';
|
||||
/** @var string */
|
||||
public $task = '';
|
||||
/** @var bool */
|
||||
public $initialized = false;
|
||||
|
||||
public $output;
|
||||
public $handlers = [];
|
||||
public $allowed_prefs = [];
|
||||
public $allowed_session_prefs = [];
|
||||
public $active_plugins = [];
|
||||
|
||||
protected $plugins = [];
|
||||
protected $plugins_initialized = [];
|
||||
protected $tasks = [];
|
||||
protected $actions = [];
|
||||
protected $actionmap = [];
|
||||
protected $objectsmap = [];
|
||||
protected $template_contents = [];
|
||||
protected $exec_stack = [];
|
||||
protected $deprecated_hooks = [];
|
||||
|
||||
|
||||
/**
|
||||
* 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 rcube $app Instance of the rcube base class
|
||||
* @param string $task 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', [$this, 'template_container_hook']);
|
||||
// maybe also register a shutdown function which triggers
|
||||
// shutdown functions of all plugin objects
|
||||
|
||||
foreach ($this->plugins as $plugin) {
|
||||
// ... task, request type and framed mode
|
||||
if (empty($this->plugins_initialized[$plugin->ID]) && !$this->filter($plugin)) {
|
||||
$plugin->init();
|
||||
$this->plugins_initialized[$plugin->ID] = $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
// we have finished initializing all plugins
|
||||
$this->initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $plugins_enabled List of configured plugins to load
|
||||
* @param array $plugins_required List of plugins required by the application
|
||||
*/
|
||||
public function load_plugins($plugins_enabled, $plugins_required = [])
|
||||
{
|
||||
foreach ($plugins_enabled as $plugin_name) {
|
||||
$this->load_plugin($plugin_name);
|
||||
}
|
||||
|
||||
// check existence of all required core plugins
|
||||
foreach ($plugins_required 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([
|
||||
'code' => 520, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Required plugin $plugin_name was not loaded"
|
||||
],
|
||||
true, true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the specified plugin
|
||||
*
|
||||
* @param string $plugin_name Plugin name
|
||||
* @param bool $force Force loading of the plugin even if it doesn't match the filter
|
||||
* @param bool $require Require loading of the plugin, error if it doesn't exist
|
||||
*
|
||||
* @return bool True on success, false if not loaded or failure
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
// Validate the plugin name to prevent from path traversal
|
||||
if (preg_match('/[^a-zA-Z0-9_-]/', $plugin_name)) {
|
||||
rcube::raise_error([
|
||||
'code' => 520, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Invalid plugin name: $plugin_name"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// plugin already loaded?
|
||||
if (!isset($this->plugins[$plugin_name])) {
|
||||
$fn = "$plugins_dir/$plugin_name/$plugin_name.php";
|
||||
|
||||
if (!is_readable($fn)) {
|
||||
if ($require) {
|
||||
rcube::raise_error([
|
||||
'code' => 520, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Failed to load plugin file $fn"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!class_exists($plugin_name, false)) {
|
||||
include $fn;
|
||||
}
|
||||
|
||||
// instantiate class if exists
|
||||
if (!class_exists($plugin_name, false)) {
|
||||
rcube::raise_error([
|
||||
'code' => 520, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "No plugin class $plugin_name found in $fn"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$plugin = new $plugin_name($this);
|
||||
$this->active_plugins[] = $plugin_name;
|
||||
|
||||
// check inheritance...
|
||||
if (is_subclass_of($plugin, 'rcube_plugin')) {
|
||||
// call onload method on plugin if it exists.
|
||||
// this is useful if you want to be called early in the boot process
|
||||
if (method_exists($plugin, 'onload')) {
|
||||
$plugin->onload();
|
||||
}
|
||||
|
||||
if (!empty($plugin->allowed_prefs)) {
|
||||
$this->allowed_prefs = array_merge($this->allowed_prefs, $plugin->allowed_prefs);
|
||||
}
|
||||
|
||||
$this->plugins[$plugin_name] = $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->plugins[$plugin_name])) {
|
||||
$plugin = $this->plugins[$plugin_name];
|
||||
|
||||
// init a plugin only if $force is set or if we're called after initialization
|
||||
if (
|
||||
($force || $this->initialized)
|
||||
&& empty($this->plugins_initialized[$plugin_name])
|
||||
&& ($force || !$this->filter($plugin))
|
||||
) {
|
||||
$plugin->init();
|
||||
$this->plugins_initialized[$plugin_name] = $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should prevent this plugin from initializing
|
||||
*
|
||||
* @param rcube_plugin $plugin Plugin object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function filter($plugin)
|
||||
{
|
||||
return ($plugin->noajax && !(is_object($this->output) && $this->output->type == 'html'))
|
||||
|| ($plugin->task && !preg_match('/^('.$plugin->task.')$/i', $this->task))
|
||||
|| ($plugin->noframe && !empty($_REQUEST['_framed']));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param string $plugin_name 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 = [
|
||||
'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',
|
||||
'GPL-2.0' => 'http://www.gnu.org/licenses/gpl-2.0.html',
|
||||
'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html',
|
||||
'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html',
|
||||
'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html',
|
||||
'AGPL-3.0' => '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',
|
||||
'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) . "/$plugin_name/$plugin_name.php";
|
||||
$info = false;
|
||||
|
||||
// Validate the plugin name to prevent from path traversal
|
||||
if (preg_match('/[^a-zA-Z0-9_-]/', $plugin_name)) {
|
||||
rcube::raise_error([
|
||||
'code' => 520, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Invalid plugin name: $plugin_name"
|
||||
],
|
||||
true, false
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!class_exists($plugin_name, false)) {
|
||||
if (is_readable($fn)) {
|
||||
include($fn);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists($plugin_name)) {
|
||||
$info = $plugin_name::info();
|
||||
}
|
||||
|
||||
// fall back to composer.json file
|
||||
if (empty($info)) {
|
||||
$info = [];
|
||||
$composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
|
||||
|
||||
if (is_readable($composer) && ($json = json_decode(file_get_contents($composer), true))) {
|
||||
// Build list of plugins required
|
||||
$require = [];
|
||||
if (!empty($json['require'])) {
|
||||
foreach (array_keys((array) $json['require']) as $dname) {
|
||||
if (!preg_match('|^([^/]+)/([a-zA-Z0-9_-]+)$|', $dname, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vendor = $m[1];
|
||||
$name = $m[2];
|
||||
|
||||
if ($name != 'plugin-installer' && $vendor != 'pear' && $vendor != 'pear-pear') {
|
||||
$dpath = unslashify($dir->path) . "/$name/$name.php";
|
||||
if (is_readable($dpath)) {
|
||||
$require[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($json['name']) && is_string($json['name']) && strpos($json['name'], '/') !== false) {
|
||||
list($info['vendor'], $info['name']) = explode('/', $json['name'], 2);
|
||||
}
|
||||
|
||||
$info['version'] = isset($json['version']) ? $json['version'] : null;
|
||||
$info['license'] = isset($json['license']) ? $json['license'] : null;
|
||||
$info['require'] = $require;
|
||||
|
||||
if (!empty($json['homepage'])) {
|
||||
$info['uri'] = $json['homepage'];
|
||||
}
|
||||
}
|
||||
|
||||
// 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 && !empty($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 (!empty($json['name']) && $composer_lock && !empty($composer_lock['installed'])
|
||||
&& !empty($composer_lock['installed'][$json['name']])
|
||||
) {
|
||||
$lock = $composer_lock['installed'][$json['name']];
|
||||
$info['version'] = $lock['version'];
|
||||
$info['uri'] = !empty($lock['homepage']) ? $lock['homepage'] : $lock['source']['url'];
|
||||
$info['src_uri'] = !empty($lock['dist']['url']) ? $lock['dist']['url'] : $lock['source']['url'];
|
||||
}
|
||||
}
|
||||
|
||||
// fall back to package.xml file
|
||||
if (empty($info)) {
|
||||
$package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
|
||||
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 = [
|
||||
'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['require'][] = $dn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At least provide the name
|
||||
if (!$info && class_exists($plugin_name)) {
|
||||
$info = ['name' => $plugin_name, 'version' => '--'];
|
||||
}
|
||||
else if (!empty($info['license'])) {
|
||||
// Convert license identifier to something shorter
|
||||
if (preg_match('/^([ALGP]+)[-v]([0-9.]+)(\+|-or-later)?/', $info['license'], $matches)) {
|
||||
$info['license'] = $matches[1] . '-' . sprintf('%.1f', $matches[2])
|
||||
. (!empty($matches[3]) ? '+' : '');
|
||||
}
|
||||
|
||||
if (empty($info['license_uri']) && !empty($license_uris[$info['license']])) {
|
||||
$info['license_uri'] = $license_uris[$info['license']];
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a plugin object to register a callback for a certain hook
|
||||
*
|
||||
* @param string $hook Hook name
|
||||
* @param callback $callback A callback function
|
||||
*/
|
||||
public function register_hook($hook, $callback)
|
||||
{
|
||||
if (is_callable($callback)) {
|
||||
if (isset($this->deprecated_hooks[$hook])) {
|
||||
rcube::raise_error([
|
||||
'code' => 522, '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([
|
||||
'code' => 521, '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 callback $callback A callback function
|
||||
*/
|
||||
public function unregister_hook($hook, $callback)
|
||||
{
|
||||
if (empty($this->handlers[$hook])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callback_id = array_search($callback, (array) $this->handlers[$hook]);
|
||||
if ($callback_id !== false) {
|
||||
// array_splice() removes the element and re-indexes keys
|
||||
// that is required by the 'for' loop in exec_hook() below
|
||||
array_splice($this->handlers[$hook], $callback_id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
if (!is_array($args)) {
|
||||
$args = ['arg' => $args];
|
||||
}
|
||||
|
||||
// TODO: avoid recursion by checking in_array($hook, $this->exec_stack) ?
|
||||
|
||||
$args += ['abort' => false];
|
||||
array_push($this->exec_stack, $hook);
|
||||
|
||||
// Use for loop here, so handlers added in the hook will be executed too
|
||||
if (!empty($this->handlers[$hook])) {
|
||||
for ($i = 0; $i < count($this->handlers[$hook]); $i++) {
|
||||
$ret = call_user_func($this->handlers[$hook][$i], $args);
|
||||
if ($ret && is_array($ret)) {
|
||||
$args = $ret + $args;
|
||||
}
|
||||
|
||||
if (!empty($args['break'])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 callback $callback A callback function
|
||||
* @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([
|
||||
'code' => 523, '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([
|
||||
'code' => 524, '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 callback $callback A callback function
|
||||
*/
|
||||
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([
|
||||
'code' => 525, '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([
|
||||
'code' => 526, '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([
|
||||
'code' => 526, 'file' => __FILE__, 'line' => __LINE__,
|
||||
'message' => "Cannot register task $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 bool True if registered, otherwise false
|
||||
*/
|
||||
public function is_plugin_task($task)
|
||||
{
|
||||
return !empty($this->tasks[$task]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a plugin hook is currently processing.
|
||||
* Mainly used to prevent loops and recursion.
|
||||
*
|
||||
* @param string $hook Hook to check (optional)
|
||||
*
|
||||
* @return bool True if any/the given hook is currently processed, otherwise false
|
||||
*/
|
||||
public function is_processing($hook = null)
|
||||
{
|
||||
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->include_script($src, 'head_bottom', false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) {
|
||||
$rcube = rcube::get_instance();
|
||||
$devel_mode = $rcube->config->get('devel_mode');
|
||||
$assets_dir = $rcube->config->get('assets_dir');
|
||||
$path = unslashify($assets_dir ?: RCUBE_INSTALL_PATH);
|
||||
$dir = $path . (strpos($fn, "plugins/") === false ? '/plugins' : '');
|
||||
|
||||
// Prefer .less files in devel_mode (assume less.js is loaded)
|
||||
if ($devel_mode) {
|
||||
$less = preg_replace('/\.css$/i', '.less', $fn);
|
||||
if ($less != $fn && is_file("$dir/$less")) {
|
||||
$fn = $less;
|
||||
}
|
||||
}
|
||||
else if (!preg_match('/\.min\.css$/', $fn)) {
|
||||
$min = preg_replace('/\.css$/i', '.min.css', $fn);
|
||||
if (is_file("$dir/$min")) {
|
||||
$fn = $min;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_file("$dir/$fn")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$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)
|
||||
{
|
||||
if (!isset($this->template_contents[$container])) {
|
||||
$this->template_contents[$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|null Plugin instance
|
||||
*/
|
||||
public function get_plugin($name)
|
||||
{
|
||||
return !empty($this->plugins[$name]) ? $this->plugins[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for template_container hooks
|
||||
*
|
||||
* @param array $attrib Container attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function template_container_hook($attrib)
|
||||
{
|
||||
$container = $attrib['name'];
|
||||
$content = $attrib['content'] ?? '';
|
||||
|
||||
if (isset($this->template_contents[$container])) {
|
||||
$content .= $this->template_contents[$container];
|
||||
}
|
||||
|
||||
return ['content' => $content];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the given file name link into the plugins directory
|
||||
*
|
||||
* @param string $fn Filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function resource_url($fn)
|
||||
{
|
||||
// pattern "skins/" used to identify plugin resources loaded from the core skin folder
|
||||
if ($fn[0] != '/' && !preg_match('#^(https?://|skins/)#i', $fn)) {
|
||||
return $this->url . $fn;
|
||||
}
|
||||
else {
|
||||
return $fn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 = [];
|
||||
protected $params = [];
|
||||
protected $order = 'ASC';
|
||||
|
||||
const SEPARATOR_ELEMENT = ' ';
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor.
|
||||
*/
|
||||
public function __construct($mailbox = null, $data = null, $order = null)
|
||||
{
|
||||
$this->mailbox = $mailbox;
|
||||
$this->order = $order == 'DESC' ? 'DESC' : 'ASC';
|
||||
$this->init($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes object with SORT command response
|
||||
*
|
||||
* @param string $data IMAP response string
|
||||
*/
|
||||
public function init($data = null)
|
||||
{
|
||||
$this->meta = [];
|
||||
|
||||
$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, ['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_first($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
&& empty($this->meta['max']) && empty($this->meta['min']) && empty($this->meta['count']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of elements in the result
|
||||
*
|
||||
* @return int Number of elements
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
if (isset($this->meta['count'])) {
|
||||
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|null Maximal message identifier
|
||||
*/
|
||||
public function max()
|
||||
{
|
||||
if ($this->is_empty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->meta['max'])) {
|
||||
$this->meta['max'] = null;
|
||||
$all = $this->get();
|
||||
if (!empty($all)) {
|
||||
$this->meta['max'] = (int) max($all);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->meta['max'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns minimal message identifier in the result
|
||||
*
|
||||
* @return int|null Minimal message identifier
|
||||
*/
|
||||
public function min()
|
||||
{
|
||||
if ($this->is_empty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->meta['min'])) {
|
||||
$this->meta['min'] = null;
|
||||
$all = $this->get();
|
||||
if (!empty($all)) {
|
||||
$this->meta['min'] = (int) min($all);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->meta['min'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Slices data set.
|
||||
*
|
||||
* @param int $offset Offset (as for PHP's array_slice())
|
||||
* @param int $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 = [];
|
||||
$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 = [])
|
||||
{
|
||||
$data = $this->get();
|
||||
$data = array_intersect($data, $ids);
|
||||
|
||||
$this->meta = [];
|
||||
$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;
|
||||
}
|
||||
|
||||
$data = $this->get();
|
||||
$data = array_reverse($data);
|
||||
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
|
||||
|
||||
$this->meta['pos'] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('|', ['^', preg_quote(self::SEPARATOR_ELEMENT, '/')]);
|
||||
$end = implode('|', ['$', preg_quote(self::SEPARATOR_ELEMENT, '/')]);
|
||||
|
||||
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
|
||||
$get_index ? PREG_OFFSET_CAPTURE : 0)
|
||||
) {
|
||||
if ($get_index) {
|
||||
$idx = 0;
|
||||
if (!empty($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 [];
|
||||
}
|
||||
|
||||
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|null Element value
|
||||
*/
|
||||
public function get_element($index)
|
||||
{
|
||||
if (empty($this->raw_data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$count = $this->count();
|
||||
|
||||
// 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, 0, $pos)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Finally use less effective method
|
||||
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
|
||||
|
||||
return (int) $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] ?? null;
|
||||
}
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 = [];
|
||||
public $incomplete = false;
|
||||
public $folder;
|
||||
|
||||
protected $meta = [];
|
||||
protected $index = [];
|
||||
protected $folders = [];
|
||||
protected $sdata = [];
|
||||
protected $order = 'ASC';
|
||||
protected $sorting;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor.
|
||||
*
|
||||
* @param array $folders List of IMAP folders
|
||||
*/
|
||||
public function __construct($folders = [])
|
||||
{
|
||||
$this->folders = $folders;
|
||||
$this->meta = ['count' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes object with SORT command response
|
||||
*
|
||||
* @param rcube_result_index|rcube_result_thread Search result
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param rcube_result_index|rcube_result_thread Search result
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param rcube_message_header[] $headers Messages in the index
|
||||
* @param string $sort_field Header field to sort by
|
||||
* @param string $sort_order Sort order
|
||||
*/
|
||||
public function set_message_index($headers, $sort_field, $sort_order)
|
||||
{
|
||||
$this->sorting = $sort_field;
|
||||
$this->order = $sort_order;
|
||||
$this->index = [];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
$this->index[] = $header->uid . '-' . $header->folder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_reverse($this->index);
|
||||
|
||||
// revert order in all sub-sets
|
||||
foreach ($this->sets as $set) {
|
||||
if ($this->order != $set->get_parameters('ORDER')) {
|
||||
$set->revert();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
$idx = array_search($msgid, $this->index);
|
||||
|
||||
if ($get_index) {
|
||||
return $idx;
|
||||
}
|
||||
|
||||
return $idx !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [], $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 int $offset Offset (as for PHP's array_slice())
|
||||
* @param int $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 = [])
|
||||
{
|
||||
// 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 in compressed form
|
||||
*
|
||||
* @return string List of message IDs in compressed form
|
||||
*/
|
||||
public function get_compressed()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result element at specified index
|
||||
*
|
||||
* @param int|string $idx 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] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [
|
||||
'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|rcube_result_index|rcube_result_thread A result set or 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialization __sleep handler
|
||||
*
|
||||
* @return array Names of all object properties that should be serialized
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
$this->sdata = ['incomplete' => [], 'error' => []];
|
||||
|
||||
foreach ($this->sets as $set) {
|
||||
if ($set->incomplete) {
|
||||
$this->sdata['incomplete'][] = $set->get_parameters('MAILBOX');
|
||||
}
|
||||
else if ($set->is_error()) {
|
||||
$this->sdata['error'][] = $set->get_parameters('MAILBOX');
|
||||
}
|
||||
}
|
||||
|
||||
return ['sdata', 'index', 'folders', 'sorting', 'order'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialization __wakeup handler
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->meta = ['count' => count($this->index)];
|
||||
$this->incomplete = count($this->sdata['incomplete']) > 0;
|
||||
|
||||
// restore result sets from saved index
|
||||
$data = [];
|
||||
foreach ($this->index as $item) {
|
||||
list($uid, $folder) = explode('-', $item, 2);
|
||||
$data[$folder] = ($data[$folder] ?? '') . ' ' . $uid;
|
||||
}
|
||||
|
||||
foreach ($this->folders as $folder) {
|
||||
if (in_array($folder, $this->sdata['error'])) {
|
||||
$data_str = null;
|
||||
}
|
||||
else {
|
||||
$data_str = '* SORT' . ($data[$folder] ?? '');
|
||||
}
|
||||
|
||||
$set = new rcube_result_index($folder, $data_str, strtoupper($this->order));
|
||||
|
||||
if (in_array($folder, $this->sdata['incomplete'])) {
|
||||
$set->incomplete = true;
|
||||
}
|
||||
|
||||
$this->sets[] = $set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Class representing an address directory result set |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Roundcube result set class
|
||||
*
|
||||
* Representing an address directory result set.
|
||||
* Implements Iterator and can thus be used in foreach() loops.
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Addressbook
|
||||
*/
|
||||
class rcube_result_set implements Iterator, ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var int The number of total records. Note that when only a subset of records is requested,
|
||||
* this number may be higher than the number of data records in this result set.
|
||||
*/
|
||||
public $count = 0;
|
||||
|
||||
/**
|
||||
* @var int When a subset of the total records is requested, $first gives the index into the total record
|
||||
* set from that the data records in this result set start. This is normally a multiple of the
|
||||
* user-configured page size.
|
||||
*/
|
||||
public $first = 0;
|
||||
|
||||
/**
|
||||
* @var bool True if the results are from an addressbook that does not support listing all records but
|
||||
* requires the search function to be used.
|
||||
*/
|
||||
public $searchonly = false;
|
||||
|
||||
/**
|
||||
* @var array The data records of the result set. May be a subset of the total records, e.g. for one page.
|
||||
*/
|
||||
public $records = [];
|
||||
|
||||
private $current = 0;
|
||||
|
||||
function __construct($count = 0, $first = 0)
|
||||
{
|
||||
$this->count = (int) $count;
|
||||
$this->first = (int) $first;
|
||||
}
|
||||
|
||||
public function add($rec)
|
||||
{
|
||||
$this->records[] = $rec;
|
||||
}
|
||||
|
||||
public function iterate()
|
||||
{
|
||||
$current = $this->current();
|
||||
|
||||
$this->current++;
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
public function first()
|
||||
{
|
||||
$this->current = 0;
|
||||
return $this->current();
|
||||
}
|
||||
|
||||
public function seek($i): void
|
||||
{
|
||||
$this->current = $i;
|
||||
}
|
||||
|
||||
/*** Implement PHP ArrayAccess interface ***/
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$offset = count($this->records);
|
||||
$this->records[] = $value;
|
||||
}
|
||||
else {
|
||||
$this->records[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->records[$offset]);
|
||||
}
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->records[$offset];
|
||||
}
|
||||
|
||||
/*** PHP 5 Iterator interface ***/
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->current = 0;
|
||||
}
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function current()
|
||||
{
|
||||
return $this->records[$this->current] ?? null;
|
||||
}
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function key()
|
||||
{
|
||||
return $this->current;
|
||||
}
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function next()
|
||||
{
|
||||
return $this->iterate();
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->records[$this->current]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 = [];
|
||||
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 = [];
|
||||
|
||||
$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_first($data);
|
||||
$data = trim($data);
|
||||
$data = preg_replace('/[\r\n]/', '', $data);
|
||||
$data = preg_replace('/\s+/', ' ', $data);
|
||||
|
||||
$this->raw_data = empty($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of elements (threads) in the result
|
||||
*
|
||||
* @return int Number of elements
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
if (isset($this->meta['count'])) {
|
||||
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 (isset($this->meta['messages'])) {
|
||||
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|null Maximum message identifier
|
||||
*/
|
||||
public function max()
|
||||
{
|
||||
if ($this->is_empty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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|null Minimum message identifier
|
||||
*/
|
||||
public function min()
|
||||
{
|
||||
if ($this->is_empty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->meta['min'])) {
|
||||
$this->meta['min'] = (int) @min($this->get());
|
||||
}
|
||||
|
||||
return $this->meta['min'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Slices data set.
|
||||
*
|
||||
* @param int $offset Offset (as for PHP's array_slice())
|
||||
* @param int $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 = [];
|
||||
$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 = ['count' => 0];
|
||||
|
||||
while ($start < $datalen
|
||||
&& (($pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) !== false
|
||||
|| ($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;
|
||||
}
|
||||
|
||||
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
|
||||
$data = array_reverse($data);
|
||||
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
|
||||
|
||||
$this->meta['pos'] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool True on success, False if message ID doesn't exist
|
||||
*/
|
||||
public function exists($msgid, $get_index = false)
|
||||
{
|
||||
$msgid = (int) $msgid;
|
||||
$begin = implode('|', [
|
||||
'^',
|
||||
preg_quote(self::SEPARATOR_ELEMENT, '/'),
|
||||
preg_quote(self::SEPARATOR_LEVEL, '/'),
|
||||
]);
|
||||
$end = implode('|', [
|
||||
'$',
|
||||
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 : 0)
|
||||
) {
|
||||
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 [];
|
||||
}
|
||||
|
||||
$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] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [
|
||||
'MAILBOX' => $this->mailbox,
|
||||
'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->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 (
|
||||
($start < $datalen && ($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 (
|
||||
($start < $datalen && ($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 = [];
|
||||
$start = 0;
|
||||
|
||||
while ($start < $datalen
|
||||
&& (($pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) !== false
|
||||
|| ($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 = [];
|
||||
$children = [];
|
||||
|
||||
$this->build_thread_data($data, $depth, $children);
|
||||
|
||||
return [$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 = [];
|
||||
|
||||
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 structure as a string,
|
||||
// arrays handling is much more expensive
|
||||
// For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))((11)(12))
|
||||
// -- 2
|
||||
// -- 3
|
||||
// \-- 6
|
||||
// |-- 4
|
||||
// | \-- 23
|
||||
// |
|
||||
// \-- 44
|
||||
// \-- 7
|
||||
// \-- 96
|
||||
// -- 11
|
||||
// \-- 12
|
||||
//
|
||||
// The output will be: 2 3~1:6~2:4~3:23~2:44~3:7~4:96 11~1:12
|
||||
// Note: The "11" thread has no root, we use the first message as root
|
||||
|
||||
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;
|
||||
if (isset($this->meta['messages'])) {
|
||||
$this->meta['messages']++;
|
||||
}
|
||||
else {
|
||||
$this->meta['messages'] = 1;
|
||||
}
|
||||
$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--;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle threads with missing parent by using first message as root
|
||||
if (substr_compare($str, '((', $start, 2) === 0) {
|
||||
// Extract the current thread, e.g. "((1)(2))"
|
||||
$thread = substr($str, $start, $off - $start);
|
||||
// Length of the first token, e.g. "(1)"
|
||||
$len = strspn($thread, '(0123456789', 1) + 1;
|
||||
// Extract the token and modify it to look like a thread root
|
||||
$token = substr($thread, 1, $len);
|
||||
// Warning: The order is important
|
||||
$token = str_replace('(', '', $token);
|
||||
$token = str_replace(' ', ' (', $token);
|
||||
$token = str_replace(')', ' ', $token);
|
||||
$thread = substr_replace($thread, $token, 1, $len);
|
||||
// Parse the thread
|
||||
$thread = $this->parse_thread($thread, 0, 0, $depth);
|
||||
}
|
||||
else {
|
||||
$thread = $this->parse_thread($str, $start + 1, $off - 1, $depth);
|
||||
}
|
||||
|
||||
if ($thread) {
|
||||
if (!$depth) {
|
||||
if ($node) {
|
||||
$node .= self::SEPARATOR_ELEMENT;
|
||||
}
|
||||
}
|
||||
$node .= $thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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> |
|
||||
| Author: Cor Bosman <cor@roundcu.be> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract class to provide database supported session storage
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
abstract class rcube_session
|
||||
{
|
||||
protected $config;
|
||||
protected $key;
|
||||
protected $ip;
|
||||
protected $cookie;
|
||||
protected $changed;
|
||||
protected $start;
|
||||
protected $vars;
|
||||
protected $now;
|
||||
protected $lifetime;
|
||||
protected $reloaded = false;
|
||||
protected $appends = [];
|
||||
protected $unsets = [];
|
||||
protected $gc_enabled = 0;
|
||||
protected $gc_handlers = [];
|
||||
protected $cookiename = 'roundcube_sessauth';
|
||||
protected $ip_check = false;
|
||||
protected $logging = false;
|
||||
protected $ignore_write = false;
|
||||
|
||||
|
||||
/**
|
||||
* Blocks session data from being written to database.
|
||||
* Can be used if write-race conditions are to be expected
|
||||
* @var boolean
|
||||
*/
|
||||
public $nowrite = false;
|
||||
|
||||
/**
|
||||
* Factory, returns driver-specific instance of the class
|
||||
*
|
||||
* @param rcube_config $config
|
||||
*
|
||||
* @return rcube_session Session object
|
||||
*/
|
||||
public static function factory($config)
|
||||
{
|
||||
// get session storage driver
|
||||
$storage = $config->get('session_storage', 'db');
|
||||
|
||||
// class name for this storage
|
||||
$class = "rcube_session_{$storage}";
|
||||
|
||||
// try to instantiate class
|
||||
if (class_exists($class)) {
|
||||
return new $class($config);
|
||||
}
|
||||
|
||||
// no storage found, raise error
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'session',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Failed to find session driver. Check session_storage config option"
|
||||
],
|
||||
true, true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_config $config
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
// set ip check
|
||||
$this->set_ip_check($this->config->get('ip_check'));
|
||||
|
||||
// set cookie name
|
||||
if ($name = $this->config->get('session_auth_name')) {
|
||||
$this->set_cookiename($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register session handler
|
||||
*/
|
||||
public function register_session_handler()
|
||||
{
|
||||
if (session_id()) {
|
||||
// Session already exists, skip
|
||||
return;
|
||||
}
|
||||
|
||||
ini_set('session.serialize_handler', 'php');
|
||||
|
||||
// set custom functions for PHP session management
|
||||
session_set_save_handler(
|
||||
[$this, 'open'],
|
||||
[$this, 'close'],
|
||||
[$this, 'read'],
|
||||
[$this, 'sess_write'],
|
||||
[$this, 'destroy'],
|
||||
[$this, 'gc']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for session_start()
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
$this->start = microtime(true);
|
||||
$this->ip = rcube_utils::remote_addr();
|
||||
$this->logging = $this->config->get('session_debug', false);
|
||||
|
||||
$lifetime = $this->config->get('session_lifetime', 1) * 60;
|
||||
$this->set_lifetime($lifetime);
|
||||
|
||||
session_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract methods should be implemented by driver classes
|
||||
*/
|
||||
abstract function open($save_path, $session_name);
|
||||
abstract function close();
|
||||
abstract function destroy($key);
|
||||
abstract function read($key);
|
||||
abstract function write($key, $vars);
|
||||
abstract function update($key, $newvars, $oldvars);
|
||||
|
||||
/**
|
||||
* Session write handler. This calls the implementation methods for write/update after some initial checks.
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $vars Serialized data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function sess_write($key, $vars)
|
||||
{
|
||||
if ($this->nowrite) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check cache
|
||||
$oldvars = $this->get_cache($key);
|
||||
|
||||
// if there are cached vars, update store, else insert new data
|
||||
if ($oldvars) {
|
||||
$newvars = $this->_fixvars($vars, $oldvars);
|
||||
return $this->update($key, $newvars, $oldvars);
|
||||
}
|
||||
else {
|
||||
return $this->write($key, $vars);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for session_write_close()
|
||||
*/
|
||||
public function write_close()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new (separate) session
|
||||
*
|
||||
* @param array $data Session data
|
||||
*
|
||||
* @return string Session identifier (on success)
|
||||
*/
|
||||
public function create($data)
|
||||
{
|
||||
$length = strlen(session_id());
|
||||
$key = rcube_utils::random_bytes($length);
|
||||
|
||||
// create new session
|
||||
if ($this->write($key, $this->serialize($data))) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge vars with old vars and apply unsets
|
||||
*/
|
||||
protected function _fixvars($vars, $oldvars)
|
||||
{
|
||||
$newvars = '';
|
||||
|
||||
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 = [];
|
||||
|
||||
return $newvars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute registered garbage collector routines
|
||||
*
|
||||
* @param int $maxlifetime Maximum session lifetime
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function gc($maxlifetime)
|
||||
{
|
||||
// move gc execution to the script shutdown function
|
||||
// see rcube::shutdown() and rcube_session::write_close()
|
||||
$this->gc_enabled = $maxlifetime;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
foreach ($this->gc_handlers as $fct) {
|
||||
call_user_func($fct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and set new session id
|
||||
*
|
||||
* @param bool $destroy If enabled the current session will be destroyed
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function regenerate_id($destroy = true)
|
||||
{
|
||||
$old_id = session_id();
|
||||
|
||||
// Since PHP 7.0 session_regenerate_id() will cause the old
|
||||
// session data update, we don't need this
|
||||
$this->ignore_write = true;
|
||||
session_regenerate_id($destroy);
|
||||
$this->ignore_write = false;
|
||||
|
||||
$this->vars = null;
|
||||
$this->key = session_id();
|
||||
|
||||
$this->log("Session regenerate: $old_id -> {$this->key}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* See if we have vars of this key already cached, and if so, return them.
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return string Serialized data string
|
||||
*/
|
||||
protected function get_cache($key)
|
||||
{
|
||||
// no session data in cache (read() returns false)
|
||||
if (!$this->key) {
|
||||
$cache = null;
|
||||
}
|
||||
// use internal data for fast requests (up to 0.5 sec.)
|
||||
else if ($key == $this->key && (!$this->vars || microtime(true) - $this->start < 0.5)) {
|
||||
$cache = $this->vars;
|
||||
}
|
||||
else { // else read data again
|
||||
$cache = $this->read($key);
|
||||
}
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the given value to the certain node in the session data array
|
||||
*
|
||||
* Warning: Do not use if you already modified $_SESSION in the same request (#1490608)
|
||||
*
|
||||
* @param string $path Path denoting the session variable where to append the value
|
||||
* @param string $key Key name under which to append the new value (use null for appending to an indexed list)
|
||||
* @param mixed $value 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 (array_key_exists($path, $this->unsets)) {
|
||||
unset($this->unsets[$path]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset a session variable
|
||||
*
|
||||
* @param string $var Variable name (can be a path denoting a certain node
|
||||
* in the session array, e.g. compose.attachments.5)
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
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->log("Session destroy: " . session_id());
|
||||
|
||||
$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 = [];
|
||||
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) {
|
||||
$data = $this->read($this->key);
|
||||
}
|
||||
|
||||
if (!empty($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']
|
||||
*/
|
||||
protected function &get_node($path, &$data_arr)
|
||||
{
|
||||
$node = &$data_arr;
|
||||
|
||||
if (!empty($path)) {
|
||||
foreach ((array) $path as $key) {
|
||||
if (!isset($node[$key])) {
|
||||
$node[$key] = [];
|
||||
}
|
||||
$node = &$node[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize session data
|
||||
*/
|
||||
protected 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
|
||||
*
|
||||
* @param string $str Serialized data string
|
||||
*
|
||||
* @return array Unserialized data
|
||||
*/
|
||||
public static 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
|
||||
*
|
||||
* @param int $lifetime Session lifetime (in seconds)
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return string Client IP address
|
||||
*/
|
||||
public function get_ip()
|
||||
{
|
||||
return $this->ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for cookie encryption secret
|
||||
*
|
||||
* @param string $secret Authentication secret string
|
||||
*/
|
||||
public function set_secret($secret = null)
|
||||
{
|
||||
// generate random hash and store in session
|
||||
if (!$secret) {
|
||||
if (!empty($_SESSION['auth_secret'])) {
|
||||
$secret = $_SESSION['auth_secret'];
|
||||
}
|
||||
else {
|
||||
$secret = rcube_utils::random_bytes(strlen($this->key));
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['auth_secret'] = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable IP check
|
||||
*
|
||||
* @param bool $check IP address checking state
|
||||
*/
|
||||
public function set_ip_check($check)
|
||||
{
|
||||
$this->ip_check = $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the cookie name used for session cookie
|
||||
*
|
||||
* @param string $name Authentication cookie name
|
||||
*/
|
||||
public function set_cookiename($name)
|
||||
{
|
||||
if ($name) {
|
||||
$this->cookiename = $name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check session authentication cookie
|
||||
*
|
||||
* @return bool True if valid, False if not
|
||||
*/
|
||||
public function check_auth()
|
||||
{
|
||||
$this->cookie = isset($_COOKIE[$this->cookiename]) ? $_COOKIE[$this->cookiename] : null;
|
||||
|
||||
$result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
|
||||
$prev = null;
|
||||
|
||||
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
|
||||
*/
|
||||
public 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 for specified time slot.
|
||||
*
|
||||
* @param int $timeslot Time slot to use
|
||||
*
|
||||
* @return string Cookie value
|
||||
*/
|
||||
protected function _mkcookie($timeslot)
|
||||
{
|
||||
// make sure the secret key exists
|
||||
$this->set_secret();
|
||||
|
||||
// no need to hash this, it's just a random string
|
||||
return $_SESSION['auth_secret'] . '-' . $timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes debug information to the log
|
||||
*
|
||||
* @param string Log line
|
||||
*/
|
||||
function log($line)
|
||||
{
|
||||
if ($this->logging) {
|
||||
rcube::write_log('session', $line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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: |
|
||||
| Provide SMTP functionality using socket connections |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to provide SMTP functionality using PEAR Net_SMTP
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Mail
|
||||
*/
|
||||
class rcube_smtp
|
||||
{
|
||||
private $conn;
|
||||
private $response;
|
||||
private $error;
|
||||
private $anonymize_log = 0;
|
||||
|
||||
// define headers delimiter
|
||||
const SMTP_MIME_CRLF = "\r\n";
|
||||
|
||||
const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
|
||||
|
||||
|
||||
/**
|
||||
* SMTP Connection and authentication
|
||||
*
|
||||
* @param string $host Server host
|
||||
* @param string $port Server port
|
||||
* @param string $user User name
|
||||
* @param string $pass Password
|
||||
*
|
||||
* @return bool True on success, or False on error
|
||||
*/
|
||||
public function connect($host = null, $port = null, $user = null, $pass = null)
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
// disconnect/destroy $this->conn
|
||||
$this->disconnect();
|
||||
|
||||
// reset error/response var
|
||||
$this->error = $this->response = null;
|
||||
|
||||
if (!$host) {
|
||||
$host = $rcube->config->get('smtp_host', 'localhost:587');
|
||||
if (is_array($host)) {
|
||||
if (array_key_exists($_SESSION['storage_host'], $host)) {
|
||||
$host = $host[$_SESSION['storage_host']];
|
||||
}
|
||||
else {
|
||||
$this->response[] = "Connection failed: No SMTP server found for IMAP host " . $_SESSION['storage_host'];
|
||||
$this->error = ['label' => 'smtpconnerror', 'vars' => ['code' => '500']];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!empty($port) && !empty($host) && !preg_match('/:\d+$/', $host)) {
|
||||
$host = "{$host}:{$port}";
|
||||
}
|
||||
|
||||
$host = rcube_utils::parse_host($host);
|
||||
|
||||
// let plugins alter smtp connection config
|
||||
$CONFIG = $rcube->plugins->exec_hook('smtp_connect', [
|
||||
'smtp_host' => $host,
|
||||
'smtp_user' => $user !== null ? $user : $rcube->config->get('smtp_user', '%u'),
|
||||
'smtp_pass' => $pass !== null ? $pass : $rcube->config->get('smtp_pass', '%p'),
|
||||
'smtp_auth_cid' => $rcube->config->get('smtp_auth_cid'),
|
||||
'smtp_auth_pw' => $rcube->config->get('smtp_auth_pw'),
|
||||
'smtp_auth_type' => $rcube->config->get('smtp_auth_type'),
|
||||
'smtp_helo_host' => $rcube->config->get('smtp_helo_host'),
|
||||
'smtp_timeout' => $rcube->config->get('smtp_timeout'),
|
||||
'smtp_conn_options' => $rcube->config->get('smtp_conn_options'),
|
||||
'smtp_auth_callbacks' => [],
|
||||
'gssapi_context' => null,
|
||||
'gssapi_cn' => null,
|
||||
]);
|
||||
|
||||
$smtp_host = $CONFIG['smtp_host'] ?: 'localhost';
|
||||
|
||||
list($smtp_host, $scheme, $smtp_port) = rcube_utils::parse_host_uri($smtp_host, 587, 465);
|
||||
|
||||
$use_tls = $scheme === 'tls';
|
||||
|
||||
// re-add the ssl:// prefix
|
||||
if ($scheme === 'ssl') {
|
||||
$smtp_host = "ssl://{$smtp_host}";
|
||||
}
|
||||
|
||||
// Handle per-host socket options
|
||||
rcube_utils::parse_socket_options($CONFIG['smtp_conn_options'], $smtp_host);
|
||||
|
||||
// Use valid EHLO/HELO host (#6408)
|
||||
$helo_host = $CONFIG['smtp_helo_host'] ?: rcube_utils::server_name();
|
||||
$helo_host = rcube_utils::idn_to_ascii($helo_host);
|
||||
if (!preg_match('/^[a-zA-Z0-9.:-]+$/', $helo_host)) {
|
||||
$helo_host = 'localhost';
|
||||
}
|
||||
|
||||
// IDNA Support
|
||||
$smtp_host = rcube_utils::idn_to_ascii($smtp_host);
|
||||
|
||||
$this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host, false, 0, $CONFIG['smtp_conn_options'],
|
||||
$CONFIG['gssapi_context'], $CONFIG['gssapi_cn']);
|
||||
|
||||
if ($rcube->config->get('smtp_debug')) {
|
||||
$this->conn->setDebug(true, [$this, 'debug_handler']);
|
||||
$this->anonymize_log = 0;
|
||||
|
||||
$_host = ($use_tls ? 'tls://' : '') . $smtp_host . ':' . $smtp_port;
|
||||
$this->debug_handler($this->conn, "Connecting to $_host...");
|
||||
}
|
||||
|
||||
// register authentication methods
|
||||
if (!empty($CONFIG['smtp_auth_callbacks']) && method_exists($this->conn, 'setAuthMethod')) {
|
||||
foreach ($CONFIG['smtp_auth_callbacks'] as $callback) {
|
||||
$this->conn->setAuthMethod($callback['name'], $callback['function'],
|
||||
$callback['prepend'] ?? true);
|
||||
}
|
||||
}
|
||||
|
||||
// try to connect to server and exit on failure
|
||||
$result = $this->conn->connect($CONFIG['smtp_timeout']);
|
||||
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$this->_conn_error('smtpconnerror', "Connection failed", [], $result);
|
||||
$this->conn = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// workaround for timeout bug in Net_SMTP 1.5.[0-1] (#1487843)
|
||||
if (method_exists($this->conn, 'setTimeout')
|
||||
&& ($timeout = ini_get('default_socket_timeout'))
|
||||
) {
|
||||
$this->conn->setTimeout($timeout);
|
||||
}
|
||||
|
||||
// XCLIENT extension
|
||||
$result = $this->_process_xclient($use_tls, $helo_host);
|
||||
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$this->_conn_error('smtpconnerror', "XCLIENT failed", [], $result);
|
||||
$this->disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($use_tls) {
|
||||
$result = $this->conn->starttls();
|
||||
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$this->_conn_error('smtpconnerror', "STARTTLS failed", [], $result);
|
||||
$this->disconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($CONFIG['smtp_user'] == '%u') {
|
||||
$smtp_user = (string) $rcube->get_user_name();
|
||||
} else {
|
||||
$smtp_user = $CONFIG['smtp_user'];
|
||||
}
|
||||
|
||||
if ($CONFIG['smtp_pass'] == '%p') {
|
||||
$smtp_pass = (string) $rcube->get_user_password();
|
||||
} else {
|
||||
$smtp_pass = $CONFIG['smtp_pass'];
|
||||
}
|
||||
|
||||
$smtp_auth_type = $CONFIG['smtp_auth_type'] ?: null;
|
||||
$smtp_authz = null;
|
||||
|
||||
if (!empty($CONFIG['smtp_auth_cid'])) {
|
||||
$smtp_authz = $smtp_user;
|
||||
$smtp_user = $CONFIG['smtp_auth_cid'];
|
||||
$smtp_pass = $CONFIG['smtp_auth_pw'];
|
||||
}
|
||||
|
||||
// attempt to authenticate to the SMTP server
|
||||
if (($smtp_user && $smtp_pass) || ($smtp_auth_type == 'GSSAPI')) {
|
||||
// IDNA Support
|
||||
if (strpos($smtp_user, '@')) {
|
||||
$smtp_user = rcube_utils::idn_to_ascii($smtp_user);
|
||||
}
|
||||
|
||||
$result = $this->conn->auth($smtp_user, $smtp_pass, $smtp_auth_type, false, $smtp_authz);
|
||||
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$this->_conn_error('smtpautherror', "Authentication failure", [], $result);
|
||||
$this->disconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for sending mail
|
||||
*
|
||||
* @param string Sender e-Mail address
|
||||
*
|
||||
* @param mixed Either a comma-separated list of recipients
|
||||
* (RFC822 compliant), or an array of recipients,
|
||||
* each RFC822 valid. This may contain recipients not
|
||||
* specified in the headers, for Bcc:, resending
|
||||
* messages, etc.
|
||||
* @param mixed The message headers to send with the mail
|
||||
* Either as an associative array or a finally
|
||||
* formatted string
|
||||
* @param mixed The full text of the message body, including any Mime parts
|
||||
* or file handle
|
||||
* @param array Delivery options (e.g. DSN request)
|
||||
*
|
||||
* @return bool True on success, or False on error
|
||||
*/
|
||||
public function send_mail($from, $recipients, $headers, $body, $opts = [])
|
||||
{
|
||||
if (!is_object($this->conn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare message headers as string
|
||||
$text_headers = null;
|
||||
if (is_array($headers)) {
|
||||
if (!($headerElements = $this->_prepare_headers($headers))) {
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
list($from, $text_headers) = $headerElements;
|
||||
}
|
||||
else if (is_string($headers)) {
|
||||
$text_headers = $headers;
|
||||
}
|
||||
|
||||
// exit if no from address is given
|
||||
if (!isset($from)) {
|
||||
$this->reset();
|
||||
$this->response[] = "No From address has been provided";
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare list of recipients
|
||||
$recipients = $this->_parse_rfc822($recipients);
|
||||
if (is_a($recipients, 'PEAR_Error')) {
|
||||
$this->error = ['label' => 'smtprecipientserror'];
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
$exts = $this->conn->getServiceExtensions();
|
||||
$from_params = null;
|
||||
$recipient_params = null;
|
||||
|
||||
// RFC3461: Delivery Status Notification
|
||||
if (!empty($opts['dsn'])) {
|
||||
if (isset($exts['DSN'])) {
|
||||
$from_params = 'RET=HDRS';
|
||||
$recipient_params = 'NOTIFY=SUCCESS,FAILURE';
|
||||
}
|
||||
}
|
||||
|
||||
// RFC6531: request SMTPUTF8 if needed
|
||||
if (preg_match('/[^\x00-\x7F]/', $from . implode('', $recipients))) {
|
||||
if (isset($exts['SMTPUTF8'])) {
|
||||
$from_params = ltrim($from_params . ' SMTPUTF8');
|
||||
}
|
||||
else {
|
||||
$this->_conn_error('smtputf8error', "SMTP server does not support unicode in email addresses");
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// RFC2298.3: remove envelope sender address
|
||||
if (empty($opts['mdn_use_from'])
|
||||
&& preg_match('/Content-Type: multipart\/report/', $text_headers)
|
||||
&& preg_match('/report-type=disposition-notification/', $text_headers)
|
||||
) {
|
||||
$from = '';
|
||||
}
|
||||
|
||||
// set From: address
|
||||
$result = $this->conn->mailFrom($from, $from_params);
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$this->_conn_error('smtpfromerror', "Failed to set sender '$from'", ['from' => $from]);
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// set mail recipients
|
||||
foreach ($recipients as $recipient) {
|
||||
$result = $this->conn->rcptTo($recipient, $recipient_params);
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$this->_conn_error('smtptoerror', "Failed to add recipient '$recipient'", ['to' => $recipient]);
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_resource($body)) {
|
||||
if ($text_headers) {
|
||||
$text_headers = preg_replace('/[\r\n]+$/', '', $text_headers);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($text_headers) {
|
||||
$body = $text_headers . "\r\n" . $body;
|
||||
}
|
||||
|
||||
$text_headers = null;
|
||||
}
|
||||
|
||||
// Send the message's headers and the body as SMTP data.
|
||||
$result = $this->conn->data($body, $text_headers);
|
||||
if (is_a($result, 'PEAR_Error')) {
|
||||
$err = $this->conn->getResponse();
|
||||
$err_label = 'smtperror';
|
||||
$err_vars = [];
|
||||
|
||||
if (!in_array($err[0], [354, 250, 221])) {
|
||||
$msg = sprintf('[%d] %s', $err[0], $err[1]);
|
||||
}
|
||||
else {
|
||||
$msg = $result->getMessage();
|
||||
|
||||
if (strpos($msg, 'size exceeds')) {
|
||||
$err_label = 'smtpsizeerror';
|
||||
$exts = $this->conn->getServiceExtensions();
|
||||
|
||||
if (!empty($exts['SIZE'])) {
|
||||
$limit = $exts['SIZE'];
|
||||
$msg .= " (Limit: $limit)";
|
||||
if (class_exists('rcmail_action')) {
|
||||
$limit = rcmail_action::show_bytes($limit);
|
||||
}
|
||||
|
||||
$err_vars['limit'] = $limit;
|
||||
$err_label = 'smtpsizeerror';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$err_vars['msg'] = $msg;
|
||||
|
||||
$this->error = ['label' => $err_label, 'vars' => $err_vars];
|
||||
$this->response[] = "Failed to send data. " . $msg;
|
||||
$this->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->response[] = implode(': ', $this->conn->getResponse());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the global SMTP connection
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
if (is_object($this->conn)) {
|
||||
$this->conn->rset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the global SMTP connection
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if (is_object($this->conn)) {
|
||||
$this->conn->disconnect();
|
||||
$this->conn = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is our own debug handler for the SMTP connection
|
||||
*/
|
||||
public function debug_handler($smtp, $message)
|
||||
{
|
||||
// catch AUTH commands and set anonymization flag for subsequent sends
|
||||
if (preg_match('/^Send: AUTH ([A-Z]+)/', $message, $m)) {
|
||||
$this->anonymize_log = $m[1] == 'LOGIN' ? 2 : 1;
|
||||
}
|
||||
// anonymize this log entry
|
||||
else if ($this->anonymize_log > 0 && strpos($message, 'Send:') === 0 && --$this->anonymize_log == 0) {
|
||||
$message = sprintf('Send: ****** [%d]', strlen($message) - 8);
|
||||
}
|
||||
|
||||
if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
|
||||
$diff = $len - self::DEBUG_LINE_LENGTH;
|
||||
$message = substr($message, 0, self::DEBUG_LINE_LENGTH)
|
||||
. "... [truncated $diff bytes]";
|
||||
}
|
||||
|
||||
rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message
|
||||
*/
|
||||
public function get_error()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server response messages array
|
||||
*/
|
||||
public function get_response()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an array of mail headers and return a string containing
|
||||
* text usable in sending a message.
|
||||
*
|
||||
* @param array $headers The array of headers to prepare, in an associative
|
||||
* array, where the array key is the header name (ie,
|
||||
* 'Subject'), and the array value is the header
|
||||
* value (ie, 'test'). The header produced from those
|
||||
* values would be 'Subject: test'.
|
||||
*
|
||||
* @return mixed Returns false if it encounters a bad address,
|
||||
* otherwise returns an array containing two
|
||||
* elements: Any From: address found in the headers,
|
||||
* and the plain text version of the headers.
|
||||
*/
|
||||
private function _prepare_headers($headers)
|
||||
{
|
||||
$lines = [];
|
||||
$from = null;
|
||||
|
||||
foreach ($headers as $key => $value) {
|
||||
if (strcasecmp($key, 'From') === 0) {
|
||||
$addresses = $this->_parse_rfc822($value);
|
||||
|
||||
if (is_array($addresses)) {
|
||||
$from = $addresses[0];
|
||||
}
|
||||
|
||||
// Reject envelope From: addresses with spaces.
|
||||
if (strpos($from, ' ') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lines[] = $key . ': ' . $value;
|
||||
}
|
||||
else if (strcasecmp($key, 'Received') === 0) {
|
||||
$received = [];
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $line) {
|
||||
$received[] = $key . ': ' . $line;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$received[] = $key . ': ' . $value;
|
||||
}
|
||||
|
||||
// Put Received: headers at the top. Spam detectors often
|
||||
// flag messages with Received: headers after the Subject:
|
||||
// as spam.
|
||||
$lines = array_merge($received, $lines);
|
||||
}
|
||||
else {
|
||||
// If $value is an array (i.e., a list of addresses), convert
|
||||
// it to a comma-delimited string of its elements (addresses).
|
||||
if (is_array($value)) {
|
||||
$value = implode(', ', $value);
|
||||
}
|
||||
|
||||
$lines[] = $key . ': ' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
return [$from, implode(self::SMTP_MIME_CRLF, $lines) . self::SMTP_MIME_CRLF];
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a set of recipients and parse them, returning an array of
|
||||
* bare addresses (forward paths) that can be passed to sendmail
|
||||
* or an smtp server with the rcpt to: command.
|
||||
*
|
||||
* @param mixed Either a comma-separated list of recipients
|
||||
* (RFC822 compliant), or an array of recipients,
|
||||
* each RFC822 valid.
|
||||
*
|
||||
* @return array An array of forward paths (bare addresses).
|
||||
*/
|
||||
private function _parse_rfc822($recipients)
|
||||
{
|
||||
// if we're passed an array, assume addresses are valid and implode them before parsing.
|
||||
if (is_array($recipients)) {
|
||||
$recipients = implode(', ', $recipients);
|
||||
}
|
||||
|
||||
$addresses = [];
|
||||
$recipients = preg_replace('/[\s\t]*\r?\n/', '', $recipients);
|
||||
$recipients = rcube_utils::explode_quoted_string(',', $recipients);
|
||||
|
||||
reset($recipients);
|
||||
foreach ($recipients as $recipient) {
|
||||
$a = rcube_utils::explode_quoted_string(' ', $recipient);
|
||||
foreach ($a as $word) {
|
||||
$word = trim($word);
|
||||
$len = strlen($word);
|
||||
|
||||
if ($len && strpos($word, "@") > 0 && $word[$len-1] != '"') {
|
||||
$word = preg_replace('/^<|>$/', '', $word);
|
||||
if (!in_array($word, $addresses)) {
|
||||
array_push($addresses, $word);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send XCLIENT command if configured and supported
|
||||
*/
|
||||
private function _process_xclient($use_tls, $helo_host)
|
||||
{
|
||||
$rcube = rcube::get_instance();
|
||||
|
||||
if (!is_object($this->conn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exts = $this->conn->getServiceExtensions();
|
||||
|
||||
if (!isset($exts['XCLIENT'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$opts = explode(' ', $exts['XCLIENT']);
|
||||
$cmd = '';
|
||||
|
||||
if ($rcube->config->get('smtp_xclient_login') && in_array_nocase('login', $opts)) {
|
||||
$cmd .= " LOGIN=" . $rcube->get_user_name();
|
||||
}
|
||||
|
||||
if ($rcube->config->get('smtp_xclient_addr') && in_array_nocase('addr', $opts)) {
|
||||
$ip = rcube_utils::remote_addr();
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$r = $ip;
|
||||
}
|
||||
elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$r = "IPV6:{$ip}";
|
||||
}
|
||||
else {
|
||||
$r = "[UNAVAILABLE]";
|
||||
}
|
||||
|
||||
$cmd .= " ADDR={$r}";
|
||||
}
|
||||
|
||||
if ($cmd) {
|
||||
$result = $this->conn->command("XCLIENT" . $cmd, [220]);
|
||||
|
||||
if ($result !== true) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (!$use_tls) {
|
||||
return $this->conn->helo($helo_host);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection error
|
||||
*/
|
||||
private function _conn_error($label, $message, $vars = [], $result = null)
|
||||
{
|
||||
$err = $this->conn->getResponse();
|
||||
|
||||
$vars['code'] = $result ? $result->getCode() : $err[0];
|
||||
$vars['msg'] = $result ? $result->getMessage() : $err[1];
|
||||
|
||||
$this->error = ['label' => $label, 'vars' => $vars];
|
||||
$this->response[] = "{$message}: {$err[1]} (Code: {$err[0]})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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: |
|
||||
| Spellchecking using different backends |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <machniak@kolabsys.com> |
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper class for spellchecking with GoogieSpell and PSpell support.
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_spellchecker
|
||||
{
|
||||
private $matches = [];
|
||||
private $options = [];
|
||||
private $content;
|
||||
private $engine;
|
||||
private $backend;
|
||||
private $lang;
|
||||
private $rc;
|
||||
private $error;
|
||||
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 ?: 'en';
|
||||
|
||||
$this->options = [
|
||||
'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'),
|
||||
];
|
||||
|
||||
$class = 'rcube_spellchecker_' . $this->engine;
|
||||
|
||||
if (class_exists($class)) {
|
||||
$this->backend = new $class($this, $this->lang, $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) && empty($configured[0])) {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
$langs = [];
|
||||
if (!empty($configured)) {
|
||||
$langs = (array) $configured;
|
||||
}
|
||||
else if ($this->backend) {
|
||||
$langs = $this->backend->languages();
|
||||
}
|
||||
|
||||
// load index
|
||||
$rcube_languages = [];
|
||||
$rcube_language_aliases = [];
|
||||
|
||||
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
|
||||
|
||||
// add correct labels
|
||||
$languages = [];
|
||||
foreach ($langs as $lang) {
|
||||
$langc = strtolower(substr($lang, 0, 2));
|
||||
$alias = !empty($rcube_language_aliases[$langc]) ? $rcube_language_aliases[$langc] : null;
|
||||
if (!$alias) {
|
||||
$alias = $langc.'_'.strtoupper($langc);
|
||||
}
|
||||
if (!empty($rcube_languages[$lang])) {
|
||||
$languages[$lang] = $rcube_languages[$lang];
|
||||
}
|
||||
else if (!empty($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 misspelling 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;
|
||||
}
|
||||
|
||||
// ignore links (#8527)
|
||||
$callback = function ($matches) {
|
||||
// replace the link with a dummy string that has the same length
|
||||
// we can't just remove the link
|
||||
return str_repeat(' ', strlen($matches[0]));
|
||||
};
|
||||
|
||||
$this->content = preg_replace_callback('~(^|\s)(www.\S+|[a-z]+://\S+)~', $callback, $this->content);
|
||||
|
||||
if ($this->backend) {
|
||||
$this->matches = $this->backend->check($this->content);
|
||||
}
|
||||
|
||||
return $this->found() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of misspellings found (after check)
|
||||
*
|
||||
* @return int Number of misspellings
|
||||
*/
|
||||
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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (is_array($item[4])) {
|
||||
$suggestions = $item[4];
|
||||
}
|
||||
else if (empty($item[4])) {
|
||||
$suggestions = [];
|
||||
}
|
||||
else {
|
||||
$suggestions = explode("\t", $item[4]);
|
||||
}
|
||||
|
||||
$result[$word] = $suggestions;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error message
|
||||
*
|
||||
* @return string Error message
|
||||
*/
|
||||
function error()
|
||||
{
|
||||
return $this->error ?: ($this->backend ? $this->backend->error() : false);
|
||||
}
|
||||
|
||||
private function html2text($text)
|
||||
{
|
||||
$h2t = new rcube_html2text($text, false, false, 0);
|
||||
return $h2t->get_text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the specified word is an exception according to the 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 (!empty($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()
|
||||
{
|
||||
$userid = null;
|
||||
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
|
||||
$userid = $this->rc->get_user_id();
|
||||
}
|
||||
|
||||
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_save', [
|
||||
'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', 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', 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', 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;
|
||||
}
|
||||
|
||||
$userid = null;
|
||||
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
|
||||
$userid = $this->rc->get_user_id();
|
||||
}
|
||||
|
||||
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_get', [
|
||||
'userid' => $userid, 'language' => $this->lang, 'dictionary' => []]);
|
||||
|
||||
if (empty($plugin['abort'])) {
|
||||
$dict = [];
|
||||
$sql_result = $this->rc->db->query(
|
||||
"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 = [];
|
||||
}
|
||||
|
||||
return $this->dict;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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/Domain name spoofing detection |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <machniak@kolabsys.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper class for spoofing detection.
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_spoofchecker
|
||||
{
|
||||
/** @var array In-memory cache of checked domains */
|
||||
protected static $results = [];
|
||||
|
||||
/**
|
||||
* Detects (potential) spoofing in an e-mail address or a domain.
|
||||
*
|
||||
* @param string $domain Email address or domain (UTF8 not punycode)
|
||||
*
|
||||
* @return bool True if spoofed/suspicious, False otherwise
|
||||
*/
|
||||
public static function check($domain)
|
||||
{
|
||||
if (($pos = strrpos($domain, '@')) !== false) {
|
||||
$domain = substr($domain, $pos + 1);
|
||||
}
|
||||
|
||||
if (isset(self::$results[$domain])) {
|
||||
return self::$results[$domain];
|
||||
}
|
||||
|
||||
// Spoofchecker is part of ext-intl (requires ICU >= 4.2)
|
||||
try {
|
||||
$checker = new Spoofchecker();
|
||||
|
||||
// Note: The constant (and method?) added in PHP 7.3.0
|
||||
if (defined('Spoofchecker::HIGHLY_RESTRICTIVE')) {
|
||||
$checker->setRestrictionLevel(Spoofchecker::HIGHLY_RESTRICTIVE);
|
||||
}
|
||||
else {
|
||||
$checker->setChecks(Spoofchecker::SINGLE_SCRIPT | Spoofchecker::INVISIBLE);
|
||||
}
|
||||
|
||||
$result = $checker->isSuspicious($domain);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
rcube::raise_error($e, true);
|
||||
$result = false;
|
||||
}
|
||||
|
||||
// TODO: Use areConfusable() to detect ascii-spoofing of some domains, e.g. paypa1.com?
|
||||
// TODO: Domains with non-printable characters should be considered spoofed
|
||||
|
||||
return self::$results[$domain] = $result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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: |
|
||||
| Handle string replacements based on preg_replace_callback |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper class for string replacements based on preg_replace_callback
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_string_replacer
|
||||
{
|
||||
public $pattern;
|
||||
public $mailto_pattern;
|
||||
public $link_pattern;
|
||||
public $linkref_index;
|
||||
public $linkref_pattern;
|
||||
|
||||
protected $values = [];
|
||||
protected $options = [];
|
||||
protected $linkrefs = [];
|
||||
protected $urls = [];
|
||||
protected $noword = '[^\w@.#-]';
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param array $options Configuration options
|
||||
*/
|
||||
function __construct($options = [])
|
||||
{
|
||||
// Create hard-to-guess replacement string
|
||||
$uniq_ident = sprintf('%010d%010d', mt_rand(), mt_rand());
|
||||
$this->pattern = '/##' . $uniq_ident . '##(\d+)##/';
|
||||
|
||||
// Simplified domain expression for UTF8 characters handling
|
||||
// Support unicode/punycode in top-level domain part
|
||||
$utf_domain = '[^?&@"\'\\/()<>\s\r\t\n]+\\.?([^\\x00-\\x2f\\x3b-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-zA-Z0-9]{2,})';
|
||||
$url1 = '.:;,';
|
||||
$url2 = 'a-zA-Z0-9%=#$@+?|!&\\/_~\\[\\]\\(\\){}\*\x80-\xFE-';
|
||||
|
||||
// Supported link prefixes
|
||||
$link_prefix = "([\w]+:\/\/|{$this->noword}[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)";
|
||||
|
||||
$this->options = $options;
|
||||
$this->linkref_index = '/\[([^<>\]#]+)\](:?\s*' . substr($this->pattern, 1, -1) . ')/';
|
||||
$this->linkref_pattern = '/\[([^<>\]#]+)\]/';
|
||||
$this->link_pattern = "/$link_prefix($utf_domain([$url1]*[$url2]+)*)/";
|
||||
$this->mailto_pattern = "/("
|
||||
. "[-\w!\#\$%&*+~\/^`|{}=]+(?:\.[-\w!\#\$%&*+~\/^`|{}=]+)*" // local-part
|
||||
. "@$utf_domain" // domain-part
|
||||
. "(\?[$url1$url2]+)?" // e.g. ?subject=test...
|
||||
. ")/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a string to the internal list
|
||||
*
|
||||
* @param string $str String value
|
||||
*
|
||||
* @return int Index of value for retrieval
|
||||
*/
|
||||
public function add($str)
|
||||
{
|
||||
$i = count($this->values);
|
||||
$this->values[$i] = $str;
|
||||
return $i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build replacement string
|
||||
*
|
||||
* @param string|int $i Replacement index
|
||||
*
|
||||
* @return string Replacement string
|
||||
*/
|
||||
public function get_replacement($i)
|
||||
{
|
||||
return str_replace('(\d+)', $i, substr($this->pattern, 1, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used to build HTML links around URL strings
|
||||
*
|
||||
* @param array $matches Matches result from preg_replace_callback
|
||||
*
|
||||
* @return string Return valid link for recognized schemes, otherwise
|
||||
* return the unmodified URL.
|
||||
*/
|
||||
protected function link_callback($matches)
|
||||
{
|
||||
$i = -1;
|
||||
$scheme = strtolower($matches[1]);
|
||||
$url_prefix = '';
|
||||
$prefix = '';
|
||||
|
||||
if (preg_match('!^(http|ftp|file)s?://!i', $scheme)) {
|
||||
$url = $matches[1] . $matches[2];
|
||||
}
|
||||
else if (preg_match("/^({$this->noword}*)(www\.)$/i", $matches[1], $m)) {
|
||||
$url = $m[2] . $matches[2];
|
||||
$url_prefix = 'http://';
|
||||
$prefix = $m[1];
|
||||
}
|
||||
|
||||
if (!empty($url)) {
|
||||
$suffix = $this->parse_url_brackets($url);
|
||||
$attrib = isset($this->options['link_attribs']) ? (array) $this->options['link_attribs'] : [];
|
||||
$attrib['href'] = $url_prefix . $url;
|
||||
|
||||
$i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix);
|
||||
$this->urls[$i] = $attrib['href'];
|
||||
}
|
||||
|
||||
return $i >= 0 ? $prefix . $this->get_replacement($i) : $matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to add an entry to the link index
|
||||
*
|
||||
* @param array $matches Matches result from preg_replace_callback with PREG_OFFSET_CAPTURE
|
||||
*
|
||||
* @return string Replacement string
|
||||
*/
|
||||
protected function linkref_addindex($matches)
|
||||
{
|
||||
$key = $matches[1][0];
|
||||
|
||||
if (!isset($this->linkrefs[$key])) {
|
||||
$this->linkrefs[$key] = [];
|
||||
}
|
||||
|
||||
// Store the reference and its occurrence position
|
||||
$this->linkrefs[$key][] = [
|
||||
$this->urls[$matches[3][0]] ?? null,
|
||||
$matches[0][1]
|
||||
];
|
||||
|
||||
return $this->get_replacement($this->add('[' . $key . ']')) . $matches[2][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to replace link references with real links
|
||||
*
|
||||
* @param array $matches Matches result from preg_replace_callback with PREG_OFFSET_CAPTURE
|
||||
*
|
||||
* @return string Replacement string
|
||||
*/
|
||||
protected function linkref_callback($matches)
|
||||
{
|
||||
$i = 0;
|
||||
$key = $matches[1][0];
|
||||
|
||||
if (!empty($this->linkrefs[$key])) {
|
||||
$attrib = isset($this->options['link_attribs']) ? (array) $this->options['link_attribs'] : [];
|
||||
|
||||
foreach ($this->linkrefs[$key] as $linkref) {
|
||||
$attrib['href'] = $linkref[0];
|
||||
if ($linkref[1] >= $matches[1][1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$i = $this->add(html::a($attrib, rcube::Q($matches[1][0])));
|
||||
}
|
||||
|
||||
return $i > 0 ? '[' . $this->get_replacement($i) . ']' : $matches[0][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used to build mailto: links around e-mail strings
|
||||
*
|
||||
* @param array $matches Matches result from preg_replace_callback
|
||||
*
|
||||
* @return string Replacement string
|
||||
*/
|
||||
protected function mailto_callback($matches)
|
||||
{
|
||||
$href = $matches[1];
|
||||
$suffix = $this->parse_url_brackets($href);
|
||||
$i = $this->add(html::a('mailto:' . $href, rcube::Q($href)) . $suffix);
|
||||
|
||||
return $i >= 0 ? $this->get_replacement($i) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the index from the preg_replace matches array
|
||||
* and return the substitution value.
|
||||
*
|
||||
* @param array $matches Matches result from preg_replace_callback
|
||||
*
|
||||
* @return string Value at index $matches[1]
|
||||
*/
|
||||
protected function replace_callback($matches)
|
||||
{
|
||||
return $this->values[$matches[1]] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all defined (link|mailto) patterns with replacement string
|
||||
*
|
||||
* @param string $str Text
|
||||
*
|
||||
* @return string Text
|
||||
*/
|
||||
public function replace($str)
|
||||
{
|
||||
if (!is_string($str)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// search for patterns like links and e-mail addresses
|
||||
$str = preg_replace_callback($this->link_pattern, [$this, 'link_callback'], $str);
|
||||
$str = preg_replace_callback($this->mailto_pattern, [$this, 'mailto_callback'], $str);
|
||||
|
||||
// resolve link references
|
||||
/*
|
||||
This code requires PHP 7.4 and could be used instead of the two if() statements below,
|
||||
when we get there.
|
||||
|
||||
$str = preg_replace_callback($this->linkref_index,
|
||||
[$this, 'linkref_addindex'], $str, -1, $count, PREG_OFFSET_CAPTURE
|
||||
);
|
||||
$str = preg_replace_callback($this->linkref_pattern,
|
||||
[$this, 'linkref_callback'], $str, -1, $count, PREG_OFFSET_CAPTURE
|
||||
);
|
||||
*/
|
||||
if (preg_match_all($this->linkref_index, $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
|
||||
$diff = 0;
|
||||
foreach ($matches as $m) {
|
||||
$replace = $this->linkref_addindex($m);
|
||||
$str = substr_replace($str, $replace, $m[0][1] + $diff, strlen($m[0][0]));
|
||||
$diff += strlen($replace) - strlen($m[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match_all($this->linkref_pattern, $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
|
||||
$diff = 0;
|
||||
foreach ($matches as $m) {
|
||||
$replace = $this->linkref_callback($m);
|
||||
$str = substr_replace($str, $replace, $m[0][1] + $diff, strlen($m[0][0]));
|
||||
$diff += strlen($replace) - strlen($m[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace substituted strings with original values
|
||||
*
|
||||
* @param string $str Text
|
||||
*
|
||||
* @return string Text
|
||||
*/
|
||||
public function resolve($str)
|
||||
{
|
||||
return preg_replace_callback($this->pattern, [$this, 'replace_callback'], $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes bracket characters in URL handling
|
||||
*
|
||||
* @param string &$url URL
|
||||
*
|
||||
* @return string Suffix (the rest of the URL input)
|
||||
*/
|
||||
protected static function parse_url_brackets(&$url)
|
||||
{
|
||||
// #1487672: special handling of square brackets,
|
||||
// URL regexp allows [] characters in URL, for example:
|
||||
// "http://example.com/?a[b]=c". However we need to handle
|
||||
// properly situation when a bracket is placed at the end
|
||||
// of the link e.g. "[http://example.com]"
|
||||
// Yes, this is not perfect handles correctly only paired characters
|
||||
// but it should work for common cases
|
||||
|
||||
$suffix = '';
|
||||
|
||||
if (preg_match('/(\\[|\\])/', $url)) {
|
||||
$in = false;
|
||||
for ($i=0, $len=strlen($url); $i<$len; $i++) {
|
||||
if ($url[$i] == '[') {
|
||||
if ($in) {
|
||||
break;
|
||||
}
|
||||
$in = true;
|
||||
}
|
||||
else if ($url[$i] == ']') {
|
||||
if (!$in) {
|
||||
break;
|
||||
}
|
||||
$in = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($i < $len) {
|
||||
$suffix = substr($url, $i);
|
||||
$url = substr($url, 0, $i);
|
||||
}
|
||||
}
|
||||
|
||||
// Do the same for parentheses
|
||||
if (preg_match('/(\\(|\\))/', $url)) {
|
||||
$in = false;
|
||||
for ($i=0, $len=strlen($url); $i<$len; $i++) {
|
||||
if ($url[$i] == '(') {
|
||||
if ($in) {
|
||||
break;
|
||||
}
|
||||
$in = true;
|
||||
}
|
||||
else if ($url[$i] == ')') {
|
||||
if (!$in) {
|
||||
break;
|
||||
}
|
||||
$in = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($i < $len) {
|
||||
$suffix = substr($url, $i);
|
||||
$url = substr($url, 0, $i);
|
||||
}
|
||||
}
|
||||
|
||||
return $suffix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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: |
|
||||
| Converts plain text to HTML |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts plain text to HTML
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_text2html
|
||||
{
|
||||
/** @var string Contains the HTML content after conversion */
|
||||
protected $html;
|
||||
|
||||
/** @var string Contains the plain text */
|
||||
protected $text;
|
||||
|
||||
/** @var array Configuration */
|
||||
protected $config = [
|
||||
// non-breaking space
|
||||
'space' => "\xC2\xA0",
|
||||
// enables format=flowed parser
|
||||
'flowed' => false,
|
||||
// enables delsp=yes parser
|
||||
'delsp' => false,
|
||||
// enables wrapping for non-flowed text
|
||||
'wrap' => true,
|
||||
// line-break tag
|
||||
'break' => "<br>\n",
|
||||
// prefix and suffix (wrapper element)
|
||||
'begin' => '<div class="pre">',
|
||||
'end' => '</div>',
|
||||
// enables links replacement
|
||||
'links' => true,
|
||||
// string replacer class
|
||||
'replacer' => 'rcube_string_replacer',
|
||||
// prefix and suffix of unwrappable line
|
||||
'nobr_start' => '<span style="white-space:nowrap">',
|
||||
'nobr_end' => '</span>',
|
||||
];
|
||||
|
||||
/** @var bool Internal state */
|
||||
protected $converted = false;
|
||||
|
||||
/** @var bool Internal no-wrap mode state */
|
||||
protected $nowrap = false;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* If the plain text source string (or file) is supplied, the class
|
||||
* will instantiate with that source propagated, all that has
|
||||
* to be done it to call get_html().
|
||||
*
|
||||
* @param string $source Plain text
|
||||
* @param bool $from_file Indicates $source is a file to pull content from
|
||||
* @param array $config Class configuration
|
||||
*/
|
||||
function __construct($source = '', $from_file = false, $config = [])
|
||||
{
|
||||
if (!empty($source)) {
|
||||
$this->set_text($source, $from_file);
|
||||
}
|
||||
|
||||
if (!empty($config) && is_array($config)) {
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads source text into memory, either from $source string or a file.
|
||||
*
|
||||
* @param string $source Plain text
|
||||
* @param bool $from_file Indicates $source is a file to pull content from
|
||||
*/
|
||||
function set_text($source, $from_file = false)
|
||||
{
|
||||
if ($from_file && file_exists($source)) {
|
||||
$this->text = file_get_contents($source);
|
||||
}
|
||||
else {
|
||||
$this->text = $source;
|
||||
}
|
||||
|
||||
$this->converted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML content.
|
||||
*
|
||||
* @return string HTML content
|
||||
*/
|
||||
function get_html()
|
||||
{
|
||||
if (!$this->converted) {
|
||||
$this->convert();
|
||||
}
|
||||
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the HTML.
|
||||
*/
|
||||
function print_html()
|
||||
{
|
||||
print $this->get_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Workhorse function that does actual conversion (calls converter() method).
|
||||
*/
|
||||
protected function convert()
|
||||
{
|
||||
// Convert TXT to HTML
|
||||
$this->html = $this->converter($this->text);
|
||||
$this->converted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workhorse function that does actual conversion.
|
||||
*
|
||||
* @param string $text Plain text
|
||||
*
|
||||
* @return string HTML content
|
||||
*/
|
||||
protected function converter($text)
|
||||
{
|
||||
// make links and email-addresses clickable
|
||||
$attribs = ['link_attribs' => ['rel' => 'noreferrer', 'target' => '_blank']];
|
||||
$replacer = new $this->config['replacer']($attribs);
|
||||
|
||||
if ($this->config['flowed']) {
|
||||
$delsp = $this->config['delsp'];
|
||||
$text = rcube_mime::unfold_flowed($text, null, $delsp);
|
||||
}
|
||||
|
||||
// search for patterns like links and e-mail addresses and replace with tokens
|
||||
if ($this->config['links']) {
|
||||
$text = $replacer->replace($text);
|
||||
}
|
||||
|
||||
// split body into single lines
|
||||
$text = preg_split('/\r?\n/', $text);
|
||||
$quote_level = 0;
|
||||
$last = null;
|
||||
$length = 0;
|
||||
|
||||
// wrap quoted lines with <blockquote>
|
||||
for ($n = 0, $cnt = count($text); $n < $cnt; $n++) {
|
||||
$first = $text[$n][0] ?? '';
|
||||
|
||||
if ($first == '>' && preg_match('/^(>+ {0,1})+/', $text[$n], $regs)) {
|
||||
$q = substr_count($regs[0], '>');
|
||||
$text[$n] = substr($text[$n], strlen($regs[0]));
|
||||
$text[$n] = $this->convert_line($text[$n]);
|
||||
$_length = strlen(str_replace(' ', '', $text[$n]));
|
||||
|
||||
if ($q > $quote_level) {
|
||||
if ($last !== null) {
|
||||
$text[$last] .= (!$length ? "\n" : '')
|
||||
. $replacer->get_replacement($replacer->add(
|
||||
str_repeat('<blockquote>', $q - $quote_level)))
|
||||
. $text[$n];
|
||||
|
||||
unset($text[$n]);
|
||||
}
|
||||
else {
|
||||
$text[$n] = $replacer->get_replacement($replacer->add(
|
||||
str_repeat('<blockquote>', $q - $quote_level))) . $text[$n];
|
||||
|
||||
$last = $n;
|
||||
}
|
||||
}
|
||||
else if ($q < $quote_level) {
|
||||
$text[$last] .= (!$length ? "\n" : '')
|
||||
. $replacer->get_replacement($replacer->add(
|
||||
str_repeat('</blockquote>', $quote_level - $q)))
|
||||
. $text[$n];
|
||||
|
||||
unset($text[$n]);
|
||||
}
|
||||
else {
|
||||
$last = $n;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$text[$n] = $this->convert_line($text[$n]);
|
||||
$q = 0;
|
||||
$_length = strlen(str_replace(' ', '', $text[$n]));
|
||||
|
||||
if ($quote_level > 0) {
|
||||
$text[$last] .= (!$length ? "\n" : '')
|
||||
. $replacer->get_replacement($replacer->add(
|
||||
str_repeat('</blockquote>', $quote_level)))
|
||||
. $text[$n];
|
||||
|
||||
unset($text[$n]);
|
||||
}
|
||||
else {
|
||||
$last = $n;
|
||||
}
|
||||
}
|
||||
|
||||
$quote_level = $q;
|
||||
$length = $_length;
|
||||
}
|
||||
|
||||
if ($quote_level > 0) {
|
||||
$text[$last] .= $replacer->get_replacement($replacer->add(
|
||||
str_repeat('</blockquote>', $quote_level)));
|
||||
}
|
||||
|
||||
$text = implode("\n", $text);
|
||||
|
||||
// colorize signature (up to <sig_max_lines> lines)
|
||||
$len = strlen($text);
|
||||
$sig_sep = "--" . $this->config['space'] . "\n";
|
||||
$sig_max_lines = rcube::get_instance()->config->get('sig_max_lines', 15);
|
||||
|
||||
while (($sp = strrpos($text, $sig_sep, !empty($sp) ? -$len+$sp-1 : 0)) !== false) {
|
||||
if ($sp == 0 || $text[$sp-1] == "\n") {
|
||||
// do not touch blocks with more that X lines
|
||||
if (substr_count($text, "\n", $sp) < $sig_max_lines) {
|
||||
$text = substr($text, 0, max(0, $sp))
|
||||
.'<span class="sig">'.substr($text, $sp).'</span>';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// insert url/mailto links and citation tags
|
||||
$text = $replacer->resolve($text);
|
||||
|
||||
// replace line breaks
|
||||
$text = str_replace("\n", $this->config['break'], $text);
|
||||
|
||||
return $this->config['begin'] . $text . $this->config['end'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts spaces in line of text
|
||||
*
|
||||
* @param string $text Plain text
|
||||
*
|
||||
* @return string Converted text
|
||||
*/
|
||||
protected function convert_line($text)
|
||||
{
|
||||
static $table;
|
||||
|
||||
if (empty($table)) {
|
||||
$table = get_html_translation_table(HTML_SPECIALCHARS);
|
||||
unset($table['?']);
|
||||
|
||||
// replace some whitespace characters
|
||||
$table["\r"] = '';
|
||||
$table["\t"] = ' ';
|
||||
}
|
||||
|
||||
// empty line?
|
||||
if ($text === '') {
|
||||
return $text;
|
||||
}
|
||||
|
||||
// skip signature separator
|
||||
if ($text == '-- ') {
|
||||
return '--' . $this->config['space'];
|
||||
}
|
||||
|
||||
if ($this->nowrap) {
|
||||
if (!in_array($text[0], [' ', '-', '+', '@'])) {
|
||||
$this->nowrap = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Detect start of a unified diff
|
||||
// TODO: Support normal diffs
|
||||
// TODO: Support diff header and comment
|
||||
if (
|
||||
($text[0] === '-' && preg_match('/^--- \S+/', $text))
|
||||
|| ($text[0] === '+' && preg_match('/^\+\+\+ \S+/', $text))
|
||||
|| ($text[0] === '@' && preg_match('/^@@ [0-9 ,+-]+ @@/', $text))
|
||||
) {
|
||||
$this->nowrap = true;
|
||||
}
|
||||
}
|
||||
|
||||
// replace HTML special and whitespace characters
|
||||
$text = strtr($text, $table);
|
||||
|
||||
$nbsp = $this->config['space'];
|
||||
$wrappable = !$this->nowrap && ($this->config['flowed'] || $this->config['wrap']);
|
||||
|
||||
// make the line wrappable
|
||||
if ($wrappable) {
|
||||
$pos = 0;
|
||||
$diff = 0;
|
||||
$last = -2;
|
||||
$len = strlen($nbsp);
|
||||
$copy = $text;
|
||||
|
||||
while (($pos = strpos($text, ' ', $pos)) !== false) {
|
||||
if (($pos == 0 || $text[$pos-1] == ' ') && $pos - 1 != $last) {
|
||||
$last = $pos;
|
||||
$copy = substr_replace($copy, $nbsp, $pos + $diff, 1);
|
||||
$diff += $len - 1;
|
||||
}
|
||||
$pos++;
|
||||
}
|
||||
|
||||
$text = $copy;
|
||||
}
|
||||
// make the whole line non-breakable if needed
|
||||
else if ($text !== '' && preg_match('/[^a-zA-Z0-9_]/', $text)) {
|
||||
// use non-breakable spaces to correctly display
|
||||
// trailing/leading spaces and multi-space inside
|
||||
$text = str_replace(' ', $nbsp, $text);
|
||||
// wrap in nobr element, so it's not wrapped on e.g. - or /
|
||||
$text = $this->config['nobr_start'] . $text . $this->config['nobr_end'];
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,797 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Storage
|
||||
*/
|
||||
class rcube_tnef_decoder
|
||||
{
|
||||
const SIGNATURE = 0x223e9f78;
|
||||
const LVL_MESSAGE = 0x01;
|
||||
const LVL_ATTACHMENT = 0x02;
|
||||
|
||||
const AFROM = 0x08000;
|
||||
const ASUBJECT = 0x18004;
|
||||
const AMESSAGEID = 0x18009;
|
||||
const AFILENAME = 0x18010;
|
||||
const APARENTID = 0x1800a;
|
||||
const ACONVERSATIONID = 0x1800b;
|
||||
const ABODY = 0x2800c;
|
||||
const ADATESENT = 0x38005;
|
||||
const ADATERECEIVED = 0x38006;
|
||||
const ADATEMODIFIED = 0x38020;
|
||||
const APRIORITY = 0x4800d;
|
||||
const AOWNER = 0x60000;
|
||||
const ASENTFOR = 0x60001;
|
||||
const ASTATUS = 0x68007;
|
||||
const ATTACHDATA = 0x6800f;
|
||||
const ATTACHMETAFILE = 0x68011;
|
||||
const ATTACHCREATEDATE = 0x38012;
|
||||
const ARENDDATA = 0x69002;
|
||||
const AMAPIPROPS = 0x69003;
|
||||
const ARECIPIENTTABLE = 0x69004;
|
||||
const AMAPIATTRS = 0x69005;
|
||||
const AOEMCODEPAGE = 0x69007;
|
||||
const AORIGINALMCLASS = 0x70006;
|
||||
const AMCLASS = 0x78008;
|
||||
const AVERSION = 0x89006;
|
||||
|
||||
const MAPI_TYPE_UNSET = 0x0000;
|
||||
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_BODY = 0x1000;
|
||||
const MAPI_RTF_COMPRESSED = 0x1009;
|
||||
const MAPI_BODY_HTML = 0x1013;
|
||||
const MAPI_NATIVE_BODY = 0x1016;
|
||||
|
||||
const MAPI_DISPLAY_NAME = 0x3001;
|
||||
const MAPI_ADDRTYPE = 0x3002;
|
||||
const MAPI_EMAIL_ADDRESS = 0x3003;
|
||||
const MAPI_COMMENT = 0x3004;
|
||||
const MAPI_DEPTH = 0x3005;
|
||||
const MAPI_PROVIDER_DISPLAY = 0x3006;
|
||||
const MAPI_CREATION_TIME = 0x3007;
|
||||
const MAPI_LAST_MODIFICATION_TIME = 0x3008;
|
||||
const MAPI_RESOURCE_FLAGS = 0x3009;
|
||||
const MAPI_PROVIDER_DLL_NAME = 0x300A;
|
||||
const MAPI_SEARCH_KEY = 0x300B;
|
||||
const MAPI_ATTACHMENT_X400_PARAMETERS = 0x3700;
|
||||
const MAPI_ATTACH_DATA = 0x3701;
|
||||
const MAPI_ATTACH_ENCODING = 0x3702;
|
||||
const MAPI_ATTACH_EXTENSION = 0x3703;
|
||||
const MAPI_ATTACH_FILENAME = 0x3704;
|
||||
const MAPI_ATTACH_METHOD = 0x3705;
|
||||
const MAPI_ATTACH_LONG_FILENAME = 0x3707;
|
||||
const MAPI_ATTACH_PATHNAME = 0x3708;
|
||||
const MAPI_ATTACH_RENDERING = 0x3709;
|
||||
const MAPI_ATTACH_TAG = 0x370A;
|
||||
const MAPI_RENDERING_POSITION = 0x370B;
|
||||
const MAPI_ATTACH_TRANSPORT_NAME = 0x370C;
|
||||
const MAPI_ATTACH_LONG_PATHNAME = 0x370D;
|
||||
const MAPI_ATTACH_MIME_TAG = 0x370E;
|
||||
const MAPI_ATTACH_ADDITIONAL_INFO = 0x370F;
|
||||
const MAPI_ATTACH_MIME_SEQUENCE = 0x3710;
|
||||
const MAPI_ATTACH_CONTENT_ID = 0x3712;
|
||||
const MAPI_ATTACH_CONTENT_LOCATION = 0x3713;
|
||||
const MAPI_ATTACH_FLAGS = 0x3714;
|
||||
|
||||
const MAPI_NAMED_TYPE_ID = 0x0000;
|
||||
const MAPI_NAMED_TYPE_STRING = 0x0001;
|
||||
const MAPI_NAMED_TYPE_NONE = 0xff;
|
||||
const MAPI_MV_FLAG = 0x1000;
|
||||
|
||||
const RTF_UNCOMPRESSED = 0x414c454d;
|
||||
const RTF_COMPRESSED = 0x75465a4c;
|
||||
|
||||
protected $codepage;
|
||||
|
||||
|
||||
/**
|
||||
* Decompress the data.
|
||||
*
|
||||
* @param string $data The data to decompress.
|
||||
* @param bool $as_html Return message body as HTML
|
||||
*
|
||||
* @return array The decompressed data.
|
||||
*/
|
||||
public function decompress($data, $as_html = false)
|
||||
{
|
||||
$attachments = [];
|
||||
$message = [];
|
||||
|
||||
if ($this->_geti($data, 32) == self::SIGNATURE) {
|
||||
$this->_geti($data, 16);
|
||||
|
||||
// Version
|
||||
$this->_geti($data, 8); // lvl_message
|
||||
$this->_geti($data, 32); // idTnefVersion
|
||||
$this->_getx($data, $this->_geti($data, 32));
|
||||
$this->_geti($data, 16); // checksum
|
||||
|
||||
while (strlen($data) > 0) {
|
||||
switch ($this->_geti($data, 8)) {
|
||||
case self::LVL_MESSAGE:
|
||||
$this->_decodeMessage($data, $message);
|
||||
break;
|
||||
|
||||
case self::LVL_ATTACHMENT:
|
||||
$this->_decodeAttachment($data, $attachments);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the message body as HTML
|
||||
if ($message && $as_html) {
|
||||
// HTML body
|
||||
if (!empty($message['size']) && $message['subtype'] == 'html') {
|
||||
$message = $message['stream'];
|
||||
}
|
||||
// RTF body (converted to HTML)
|
||||
// Note: RTF can contain encapsulated HTML content
|
||||
else if (!empty($message['size']) && $message['subtype'] == 'rtf'
|
||||
&& function_exists('iconv')
|
||||
&& class_exists('RtfHtmlPhp\Document')
|
||||
) {
|
||||
try {
|
||||
$document = new RtfHtmlPhp\Document($message['stream']);
|
||||
$formatter = new RtfHtmlPhp\Html\HtmlFormatter(RCUBE_CHARSET);
|
||||
$message = $formatter->format($document);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// ignore the body
|
||||
rcube::raise_error([
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
'message' => "Failed to extract RTF/HTML content from TNEF attachment"
|
||||
], true, false
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$message = null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'message' => $message,
|
||||
'attachments' => array_reverse($attachments),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop specified number of bytes from the buffer.
|
||||
*
|
||||
* @param string &$data The data string.
|
||||
* @param int $bytes How many bytes to retrieve.
|
||||
*
|
||||
* @return string Extracted data
|
||||
*/
|
||||
protected function _getx(&$data, $bytes)
|
||||
{
|
||||
$value = null;
|
||||
|
||||
if (strlen($data) >= $bytes) {
|
||||
$value = substr($data, 0, $bytes);
|
||||
$data = substr($data, $bytes);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop specified number of bits from the buffer
|
||||
*
|
||||
* @param string &$data The data string.
|
||||
* @param int $bits How many bits to retrieve.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
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($data, $bytes);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a single attribute
|
||||
*
|
||||
* @param string &$data The data string.
|
||||
*
|
||||
* @return string Extracted data
|
||||
*/
|
||||
protected function _decodeAttribute(&$data)
|
||||
{
|
||||
// Data.
|
||||
$value = $this->_getx($data, $this->_geti($data, 32));
|
||||
|
||||
// Checksum.
|
||||
$this->_geti($data, 16);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param string $data The data string.
|
||||
* @param array &result TODO
|
||||
*/
|
||||
protected function _extractMapiAttributes($data, &$result)
|
||||
{
|
||||
// Number of attributes.
|
||||
$number = $this->_geti($data, 32);
|
||||
|
||||
while ((strlen($data) > 0) && $number--) {
|
||||
$have_mval = false;
|
||||
$num_mval = 1;
|
||||
$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:
|
||||
$attr_name = $this->_geti($data, 32);
|
||||
break;
|
||||
|
||||
case self::MAPI_NAMED_TYPE_STRING:
|
||||
$attr_name = 0x9999;
|
||||
$idlen = $this->_geti($data, 32);
|
||||
$name = $this->_getx($data, $idlen + ((4 - ($idlen % 4)) % 4));
|
||||
// $name = $this->convertString(substr($name, 0, $idlen));
|
||||
break;
|
||||
|
||||
case self::MAPI_NAMED_TYPE_NONE:
|
||||
default:
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($have_mval) {
|
||||
$num_mval = $this->_geti($data, 32);
|
||||
}
|
||||
|
||||
switch ($attr_type) {
|
||||
case self::MAPI_NULL:
|
||||
case self::MAPI_TYPE_UNSET:
|
||||
break;
|
||||
|
||||
case self::MAPI_SHORT:
|
||||
$value = $this->_geti($data, 16);
|
||||
$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);
|
||||
|
||||
// Read and truncate to length.
|
||||
$value = $this->_getx($data, $datalen);
|
||||
}
|
||||
|
||||
if ($attr_type == self::MAPI_UNICODE_STRING) {
|
||||
$value = $this->convertString($value);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Store any interesting attributes.
|
||||
switch ($attr_name) {
|
||||
case self::MAPI_RTF_COMPRESSED:
|
||||
$result['type'] = 'application';
|
||||
$result['subtype'] = 'rtf';
|
||||
$result['name'] = (!empty($result['name']) ? $result['name'] : 'Untitled') . '.rtf';
|
||||
$result['stream'] = $this->_decodeRTF($value);
|
||||
$result['size'] = strlen($result['stream']);
|
||||
break;
|
||||
|
||||
case self::MAPI_BODY:
|
||||
case self::MAPI_BODY_HTML:
|
||||
$result['type'] = 'text';
|
||||
$result['subtype'] = $attr_name == self::MAPI_BODY ? 'plain' : 'html';
|
||||
$result['name'] = (!empty($result['name']) ? $result['name'] : 'Untitled')
|
||||
. ($attr_name == self::MAPI_BODY ? '.txt' : '.html');
|
||||
$result['stream'] = $value;
|
||||
$result['size'] = strlen($value);
|
||||
break;
|
||||
|
||||
case self::MAPI_ATTACH_LONG_FILENAME:
|
||||
// Used in preference to AFILENAME value.
|
||||
$result['name'] = trim(preg_replace('/.*[\/](.*)$/', '\1', $value));
|
||||
break;
|
||||
|
||||
case self::MAPI_ATTACH_MIME_TAG:
|
||||
// Is this ever set, and what is format?
|
||||
$value = explode('/', trim($value));
|
||||
$result['type'] = $value[0];
|
||||
$result['subtype'] = $value[1];
|
||||
break;
|
||||
|
||||
case self::MAPI_ATTACH_CONTENT_ID:
|
||||
$result['content-id'] = $value;
|
||||
break;
|
||||
|
||||
case self::MAPI_ATTACH_DATA:
|
||||
$this->_getx($value, 16);
|
||||
$att = new rcube_tnef_decoder;
|
||||
$res = $att->decompress($value);
|
||||
$result = array_merge($result, $res['message']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes TNEF message attributes
|
||||
*
|
||||
* @param string &$data The data string.
|
||||
* @param array &$message Message data
|
||||
*/
|
||||
protected function _decodeMessage(&$data, &$message)
|
||||
{
|
||||
$attribute = $this->_geti($data, 32);
|
||||
$value = $this->_decodeAttribute($data);
|
||||
|
||||
switch ($attribute) {
|
||||
case self::AOEMCODEPAGE:
|
||||
// Find codepage of the message
|
||||
$value = unpack('V', $value);
|
||||
$this->codepage = $value[1];
|
||||
break;
|
||||
|
||||
case self::AMCLASS:
|
||||
$value = trim(str_replace('Microsoft Mail v3.0 ', '', $value));
|
||||
// Normal message will be that with prefix 'IPM.Microsoft Mail.
|
||||
break;
|
||||
|
||||
case self::ASUBJECT:
|
||||
$message['name'] = $value;
|
||||
break;
|
||||
|
||||
case self::AMAPIPROPS:
|
||||
$this->_extractMapiAttributes($value, $message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes TNEF attachment attributes
|
||||
*
|
||||
* @param string &$data The data string.
|
||||
* @param array &$attachment Attachments data
|
||||
*/
|
||||
protected function _decodeAttachment(&$data, &$attachment)
|
||||
{
|
||||
$attribute = $this->_geti($data, 32);
|
||||
$size = $this->_geti($data, 32);
|
||||
$value = $this->_getx($data, $size);
|
||||
|
||||
$this->_geti($data, 16); // checksum
|
||||
|
||||
switch ($attribute) {
|
||||
case self::ARENDDATA:
|
||||
// Add a new default data block to hold details of this
|
||||
// attachment. Reverse order is easier to handle later!
|
||||
array_unshift($attachment, [
|
||||
'type' => 'application',
|
||||
'subtype' => 'octet-stream',
|
||||
'name' => 'unknown',
|
||||
'stream' => ''
|
||||
]);
|
||||
|
||||
break;
|
||||
|
||||
case self::AFILENAME:
|
||||
$value = $this->convertString($value, true);
|
||||
// Strip path
|
||||
$attachment[0]['name'] = trim(preg_replace('/.*[\/](.*)$/', '\1', $value));
|
||||
break;
|
||||
|
||||
case self::ATTACHDATA:
|
||||
// The attachment itself
|
||||
$attachment[0]['size'] = $size;
|
||||
$attachment[0]['stream'] = $value;
|
||||
break;
|
||||
|
||||
case self::AMAPIATTRS:
|
||||
$this->_extractMapiAttributes($value, $attachment[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string value to system charset according to defined codepage
|
||||
*/
|
||||
protected function convertString($str, $use_codepage = false)
|
||||
{
|
||||
if ($use_codepage && $this->codepage
|
||||
&& ($charset = rcube_charset::$windows_codepages[$this->codepage])
|
||||
) {
|
||||
$str = rcube_charset::convert($str, $charset, RCUBE_CHARSET);
|
||||
}
|
||||
else if (($pos = strpos($str, "\0")) !== false && $pos != strlen($str)-1) {
|
||||
$str = rcube_charset::convert($str, 'UTF-16LE', RCUBE_CHARSET);
|
||||
}
|
||||
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes TNEF RTF
|
||||
*/
|
||||
protected function _decodeRTF($data)
|
||||
{
|
||||
$c_size = $this->_geti($data, 32);
|
||||
$size = $this->_geti($data, 32);
|
||||
$magic = $this->_geti($data, 32);
|
||||
$crc = $this->_geti($data, 32);
|
||||
|
||||
if ($magic == self::RTF_COMPRESSED) {
|
||||
$data = $this->_decompressRTF($data, $size);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress compressed RTF. Logic taken from Horde.
|
||||
*/
|
||||
protected function _decompressRTF($data, $size)
|
||||
{
|
||||
$in = $out = $flags = $flag_count = 0;
|
||||
$uncomp = '';
|
||||
$preload = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
|
||||
$length_preload = strlen($preload);
|
||||
|
||||
for ($cnt = 0; $cnt < $length_preload; $cnt++) {
|
||||
$uncomp .= $preload[$cnt];
|
||||
++$out;
|
||||
}
|
||||
|
||||
while ($out < ($size + $length_preload)) {
|
||||
if (($flag_count++ % 8) == 0) {
|
||||
$flags = ord($data[$in++]);
|
||||
}
|
||||
else {
|
||||
$flags = $flags >> 1;
|
||||
}
|
||||
|
||||
if (($flags & 1) != 0) {
|
||||
$offset = ord($data[$in++]);
|
||||
$length = ord($data[$in++]);
|
||||
$offset = ($offset << 4) | ($length >> 4);
|
||||
$length = ($length & 0xF) + 2;
|
||||
$offset = ((int)($out / 4096)) * 4096 + $offset;
|
||||
|
||||
if ($offset >= $out) {
|
||||
$offset -= 4096;
|
||||
}
|
||||
|
||||
$end = $offset + $length;
|
||||
|
||||
while ($offset < $end) {
|
||||
$uncomp.= $uncomp[$offset++];
|
||||
++$out;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$uncomp .= $data[$in++];
|
||||
++$out;
|
||||
}
|
||||
}
|
||||
|
||||
return substr($uncomp, $length_preload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RTF data and return the best plaintext representation we can.
|
||||
* Adapted from: http://webcheatsheet.com/php/reading_the_clean_text_from_rtf.php
|
||||
*
|
||||
* @param string $text The RTF (uncompressed) text.
|
||||
*
|
||||
* @return string The plain text.
|
||||
*/
|
||||
public static function rtf2text($text)
|
||||
{
|
||||
$document = '';
|
||||
$stack = [];
|
||||
$j = -1;
|
||||
|
||||
// Read the data character-by- character…
|
||||
for ($i = 0, $len = strlen($text); $i < $len; $i++) {
|
||||
$c = $text[$i];
|
||||
switch ($c) {
|
||||
case "\\":
|
||||
// Key Word
|
||||
$nextChar = $text[$i + 1];
|
||||
// If it is another backslash or nonbreaking space or hyphen,
|
||||
// then the character is plain text and add it to the output stream.
|
||||
if ($nextChar == "\\" && self::_rtfIsPlain($stack[$j])) {
|
||||
$document .= "\\";
|
||||
}
|
||||
elseif ($nextChar == '~' && self::_rtfIsPlain($stack[$j])) {
|
||||
$document .= ' ';
|
||||
}
|
||||
elseif ($nextChar == '_' && self::_rtfIsPlain($stack[$j])) {
|
||||
$document .= '-';
|
||||
}
|
||||
elseif ($nextChar == '*') {
|
||||
// Add to the stack.
|
||||
$stack[$j]['*'] = true;
|
||||
}
|
||||
elseif ($nextChar == "'") {
|
||||
// If it is a single quote, read next two characters that
|
||||
// are the hexadecimal notation of a character we should add
|
||||
// to the output stream.
|
||||
$hex = substr($text, $i + 2, 2);
|
||||
|
||||
if (self::_rtfIsPlain($stack[$j])) {
|
||||
$document .= html_entity_decode('&#' . hexdec($hex) .';');
|
||||
}
|
||||
|
||||
//Shift the pointer.
|
||||
$i += 2;
|
||||
}
|
||||
elseif ($nextChar >= 'a' && $nextChar <= 'z' || $nextChar >= 'A' && $nextChar <= 'Z') {
|
||||
// Since, we’ve found the alphabetic character, the next
|
||||
// characters are control words and, possibly, some digit
|
||||
// parameter.
|
||||
$word = '';
|
||||
$param = null;
|
||||
|
||||
// Start reading characters after the backslash.
|
||||
for ($k = $i + 1, $m = 0; $k < strlen($text); $k++, $m++) {
|
||||
$nextChar = $text[$k];
|
||||
// If the current character is a letter and there were
|
||||
// no digits before it, then we’re still reading the
|
||||
// control word. If there were digits, we should stop
|
||||
// since we reach the end of the control word.
|
||||
if ($nextChar >= 'a' && $nextChar <= 'z'
|
||||
|| $nextChar >= 'A' && $nextChar <= 'Z') {
|
||||
if (!empty($param)) {
|
||||
break;
|
||||
}
|
||||
$word .= $nextChar;
|
||||
}
|
||||
elseif ($nextChar >= '0' && $nextChar <= '9') {
|
||||
// If it is a digit, store the parameter.
|
||||
$param .= $nextChar;
|
||||
}
|
||||
elseif ($nextChar == '-') {
|
||||
// Since minus sign may occur only before a digit
|
||||
// parameter, check whether $param is empty.
|
||||
// Otherwise, we reach the end of the control word.
|
||||
if (!empty($param)) {
|
||||
break;
|
||||
}
|
||||
$param .= $nextChar;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Shift the pointer on the number of read characters.
|
||||
$i += $m - 1;
|
||||
|
||||
// Start analyzing.We are interested mostly in control words
|
||||
$toText = '';
|
||||
|
||||
switch (strtolower($word)) {
|
||||
// If the control word is "u", then its parameter is
|
||||
// the decimal notation of the Unicode character that
|
||||
// should be added to the output stream. We need to
|
||||
// check whether the stack contains \ucN control word.
|
||||
// If it does, we should remove the N characters from
|
||||
// the output stream.
|
||||
case 'u':
|
||||
$toText .= html_entity_decode('&#x' . dechex($param) .';');
|
||||
$ucDelta = @$stack[$j]['uc'];
|
||||
if ($ucDelta > 0) {
|
||||
$i += $ucDelta;
|
||||
}
|
||||
break;
|
||||
case 'par':
|
||||
case 'page':
|
||||
case 'column':
|
||||
case 'line':
|
||||
case 'lbr':
|
||||
$toText .= "\n";
|
||||
break;
|
||||
case 'emspace':
|
||||
case 'enspace':
|
||||
case 'qmspace':
|
||||
$toText .= ' ';
|
||||
break;
|
||||
case 'tab':
|
||||
$toText .= "\t";
|
||||
break;
|
||||
case 'chdate':
|
||||
$toText .= date('m.d.Y');
|
||||
break;
|
||||
case 'chdpl':
|
||||
$toText .= date('l, j F Y');
|
||||
break;
|
||||
case 'chdpa':
|
||||
$toText .= date('D, j M Y');
|
||||
break;
|
||||
case 'chtime':
|
||||
$toText .= date('H:i:s');
|
||||
break;
|
||||
case 'emdash':
|
||||
$toText .= html_entity_decode('—');
|
||||
break;
|
||||
case 'endash':
|
||||
$toText .= html_entity_decode('–');
|
||||
break;
|
||||
case 'bullet':
|
||||
$toText .= html_entity_decode('•');
|
||||
break;
|
||||
case 'lquote':
|
||||
$toText .= html_entity_decode('‘');
|
||||
break;
|
||||
case 'rquote':
|
||||
$toText .= html_entity_decode('’');
|
||||
break;
|
||||
case 'ldblquote':
|
||||
$toText .= html_entity_decode('«');
|
||||
break;
|
||||
case 'rdblquote':
|
||||
$toText .= html_entity_decode('»');
|
||||
break;
|
||||
default:
|
||||
$stack[$j][strtolower($word)] = empty($param) ? true : $param;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add data to the output stream if required.
|
||||
if (self::_rtfIsPlain($stack[$j])) {
|
||||
$document .= $toText;
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
break;
|
||||
|
||||
case '{':
|
||||
// New subgroup starts, add new stack element and write the data
|
||||
// from previous stack element to it.
|
||||
if (!empty($stack[$j])) {
|
||||
array_push($stack, $stack[$j++]);
|
||||
}
|
||||
else {
|
||||
$j++;
|
||||
}
|
||||
break;
|
||||
|
||||
case '}':
|
||||
array_pop($stack);
|
||||
$j--;
|
||||
break;
|
||||
|
||||
case '\0':
|
||||
case '\r':
|
||||
case '\f':
|
||||
case '\n':
|
||||
// Junk
|
||||
break;
|
||||
|
||||
default:
|
||||
// Add other data to the output stream if required.
|
||||
if (!empty($stack[$j]) && self::_rtfIsPlain($stack[$j])) {
|
||||
$document .= $c;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an RTF element is plain text
|
||||
*/
|
||||
protected static function _rtfIsPlain($s)
|
||||
{
|
||||
$notPlain = ['*', 'fonttbl', 'colortbl', 'datastore', 'themedata', 'stylesheet'];
|
||||
|
||||
for ($i = 0; $i < count($notPlain); $i++) {
|
||||
if (!empty($s[$notPlain[$i]])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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> |
|
||||
| Author: Cor Bosman <cor@roundcu.be> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to provide database session storage
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_session_db extends rcube_session
|
||||
{
|
||||
/** @var rcube_db Database handler */
|
||||
private $db;
|
||||
|
||||
/** @var string Session table name (quoted) */
|
||||
private $table_name;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_config $config Configuration
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
// get db instance
|
||||
$this->db = rcube::get_instance()->get_dbh();
|
||||
|
||||
// session table name
|
||||
$this->table_name = $this->db->table_name('session', true);
|
||||
|
||||
// register sessions handler
|
||||
$this->register_session_handler();
|
||||
|
||||
// register db gc handler
|
||||
$this->register_gc_handler([$this, 'gc_db']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the session
|
||||
*
|
||||
* @param string $save_path Session save path
|
||||
* @param string $session_name Session name
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function open($save_path, $session_name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the session
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function destroy($key)
|
||||
{
|
||||
if ($key) {
|
||||
$this->db->query("DELETE FROM {$this->table_name} WHERE `sess_id` = ?", $key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read session data from database
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return string Session vars (serialized string)
|
||||
*/
|
||||
public function read($key)
|
||||
{
|
||||
if ($this->lifetime) {
|
||||
$expire_time = $this->db->now(-$this->lifetime);
|
||||
$expire_check = "CASE WHEN `changed` < $expire_time THEN 1 ELSE 0 END AS expired";
|
||||
}
|
||||
|
||||
$sql_result = $this->db->query(
|
||||
"SELECT `vars`, `ip`, `changed`, " . $this->db->now() . " AS ts"
|
||||
. (isset($expire_check) ? ", $expire_check" : '')
|
||||
. " FROM {$this->table_name} WHERE `sess_id` = ?", $key
|
||||
);
|
||||
|
||||
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
|
||||
// Remove expired sessions (we use gc, but it may not be precise enough or disabled)
|
||||
if (!empty($sql_arr['expired'])) {
|
||||
$this->destroy($key);
|
||||
return '';
|
||||
}
|
||||
|
||||
$time_diff = time() - strtotime($sql_arr['ts']);
|
||||
|
||||
$this->changed = strtotime($sql_arr['changed']) + $time_diff; // local (PHP) time
|
||||
$this->ip = $sql_arr['ip'];
|
||||
$this->vars = base64_decode($sql_arr['vars']);
|
||||
$this->key = $key;
|
||||
|
||||
$this->db->reset();
|
||||
|
||||
return !empty($this->vars) ? (string) $this->vars : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new data into db session store
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $vars Serialized data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function write($key, $vars)
|
||||
{
|
||||
if ($this->ignore_write) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$now = $this->db->now();
|
||||
|
||||
$this->db->query("INSERT INTO {$this->table_name}"
|
||||
. " (`sess_id`, `vars`, `ip`, `changed`)"
|
||||
. " VALUES (?, ?, ?, $now)",
|
||||
$key, base64_encode($vars), (string)$this->ip
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session data
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $newvars New session data string
|
||||
* @param string $oldvars Old session data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function update($key, $newvars, $oldvars)
|
||||
{
|
||||
$now = $this->db->now();
|
||||
$ts = microtime(true);
|
||||
|
||||
// if new and old data are not the same, update data
|
||||
// else update expire timestamp only when certain conditions are met
|
||||
if ($newvars !== $oldvars) {
|
||||
$this->db->query("UPDATE {$this->table_name} "
|
||||
. "SET `changed` = $now, `vars` = ? WHERE `sess_id` = ?",
|
||||
base64_encode($newvars), $key);
|
||||
}
|
||||
else if ($ts - $this->changed > $this->lifetime / 2) {
|
||||
$this->db->query("UPDATE {$this->table_name} SET `changed` = $now"
|
||||
. " WHERE `sess_id` = ?", $key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up db sessions.
|
||||
*/
|
||||
public function gc_db()
|
||||
{
|
||||
// just clean all old sessions when this GC is called
|
||||
$this->db->query("DELETE FROM " . $this->db->table_name('session')
|
||||
. " WHERE `changed` < " . $this->db->now(-$this->gc_enabled));
|
||||
|
||||
$this->log("Session GC (DB): remove records < "
|
||||
. date('Y-m-d H:i:s', time() - $this->gc_enabled)
|
||||
. '; rows = ' . intval($this->db->affected_rows()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 memcache supported session management |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
| Author: Cor Bosman <cor@roundcu.bet> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to provide memcache session storage
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_session_memcache extends rcube_session
|
||||
{
|
||||
/** @var Memcache The memcache driver */
|
||||
private $memcache;
|
||||
|
||||
/** @var bool Debug state */
|
||||
private $debug;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_config $config Configuration
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
$this->memcache = rcube::get_instance()->get_memcache();
|
||||
$this->debug = $config->get('memcache_debug');
|
||||
|
||||
if (!$this->memcache) {
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'memcache',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Failed to connect to memcached. Please check configuration"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
// register sessions handler
|
||||
$this->register_session_handler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the session
|
||||
*
|
||||
* @param string $save_path Session save path
|
||||
* @param string $session_name Session name
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function open($save_path, $session_name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the session
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function destroy($key)
|
||||
{
|
||||
if ($key) {
|
||||
// #1488592: use 2nd argument
|
||||
$result = $this->memcache->delete($key, 0);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read session data from memcache
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return string Serialized data string
|
||||
*/
|
||||
public function 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;
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $value);
|
||||
}
|
||||
|
||||
return $this->vars ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to memcache storage
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $vars Session data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function write($key, $vars)
|
||||
{
|
||||
if ($this->ignore_write) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $vars]);
|
||||
$result = $this->memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->lifetime + 60);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update memcache session data
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $newvars New session data string
|
||||
* @param string $oldvars Old session data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function update($key, $newvars, $oldvars)
|
||||
{
|
||||
$ts = microtime(true);
|
||||
|
||||
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
|
||||
$data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $newvars]);
|
||||
$result = $this->memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->lifetime + 60);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write memcache debug info to the log
|
||||
*
|
||||
* @param string $type Operation type
|
||||
* @param string $key Session identifier
|
||||
* @param string $data Data to log
|
||||
* @param bool $result Operation result
|
||||
*/
|
||||
protected function debug($type, $key, $data = null, $result = null)
|
||||
{
|
||||
$line = strtoupper($type) . ' ' . $key;
|
||||
|
||||
if ($data !== null) {
|
||||
$line .= ' ' . $data;
|
||||
}
|
||||
|
||||
rcube::debug('memcache', $line, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 memcached supported session management |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
| Author: Cor Bosman <cor@roundcu.bet> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to provide memcached session storage
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_session_memcached extends rcube_session
|
||||
{
|
||||
/** @var Memcached The memcache driver */
|
||||
private $memcache;
|
||||
|
||||
/** @var bool Debug state */
|
||||
private $debug;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_config $config Configuration
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
$this->memcache = rcube::get_instance()->get_memcached();
|
||||
$this->debug = $config->get('memcache_debug');
|
||||
|
||||
if (!$this->memcache) {
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'memcache',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Failed to connect to memcached. Please check configuration"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
// register sessions handler
|
||||
$this->register_session_handler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the session
|
||||
*
|
||||
* @param string $save_path Session save path
|
||||
* @param string $session_name Session name
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function open($save_path, $session_name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the session
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function destroy($key)
|
||||
{
|
||||
if ($key) {
|
||||
// #1488592: use 2nd argument
|
||||
$result = $this->memcache->delete($key, 0);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read session data from memcache
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return string Serialized data string
|
||||
*/
|
||||
public function read($key)
|
||||
{
|
||||
if ($arr = $this->memcache->get($key)) {
|
||||
$this->changed = $arr['changed'];
|
||||
$this->ip = $arr['ip'];
|
||||
$this->vars = $arr['vars'];
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $arr ? serialize($arr) : '');
|
||||
}
|
||||
|
||||
return $this->vars ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to memcache storage
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $vars Session data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function write($key, $vars)
|
||||
{
|
||||
if ($this->ignore_write) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = ['changed' => time(), 'ip' => $this->ip, 'vars' => $vars];
|
||||
$result = $this->memcache->set($key, $data, $this->lifetime + 60);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, serialize($data), $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update memcache session data
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $newvars New session data string
|
||||
* @param string $oldvars Old session data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function update($key, $newvars, $oldvars)
|
||||
{
|
||||
$ts = microtime(true);
|
||||
|
||||
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
|
||||
$data = ['changed' => time(), 'ip' => $this->ip, 'vars' => $newvars];
|
||||
$result = $this->memcache->set($key, $data, $this->lifetime + 60);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, serialize($data), $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write memcache debug info to the log
|
||||
*
|
||||
* @param string $type Operation type
|
||||
* @param string $key Session identifier
|
||||
* @param string $data Data to log
|
||||
* @param bool $result Operation result
|
||||
*/
|
||||
protected function debug($type, $key, $data = null, $result = null)
|
||||
{
|
||||
$line = strtoupper($type) . ' ' . $key;
|
||||
|
||||
if ($data !== null) {
|
||||
$line .= ' ' . $data;
|
||||
}
|
||||
|
||||
rcube::debug('memcache', $line, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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> |
|
||||
| Author: Cor Bosman <cor@roundcu.be> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to provide native php session storage
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_session_php extends rcube_session
|
||||
{
|
||||
/**
|
||||
* Native php sessions don't need a save handler.
|
||||
* We do need to define abstract function implementations but they are not used.
|
||||
*/
|
||||
|
||||
public function open($save_path, $session_name) {}
|
||||
public function close() {}
|
||||
public function destroy($key) {}
|
||||
public function read($key) {}
|
||||
public function write($key, $vars) {}
|
||||
public function update($key, $newvars, $oldvars) {}
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_config $config Configuration
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for session_write_close()
|
||||
*/
|
||||
public function write_close()
|
||||
{
|
||||
$_SESSION['__IP'] = $this->ip;
|
||||
$_SESSION['__MTIME'] = time();
|
||||
|
||||
parent::write_close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for session_start()
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
parent::start();
|
||||
|
||||
$this->key = session_id();
|
||||
$this->ip = $_SESSION['__IP'] ?? null;
|
||||
$this->changed = $_SESSION['__MTIME'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) 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: |
|
||||
| Provide redis supported session management |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Cor Bosman <cor@roundcu.be> |
|
||||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to provide redis session storage
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Core
|
||||
*/
|
||||
class rcube_session_redis extends rcube_session
|
||||
{
|
||||
/** @var Redis The redis engine */
|
||||
private $redis;
|
||||
|
||||
/** @var bool Debug state */
|
||||
private $debug;
|
||||
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param rcube_config $config Configuration
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
|
||||
$this->redis = rcube::get_instance()->get_redis();
|
||||
$this->debug = $config->get('redis_debug');
|
||||
|
||||
if (!$this->redis) {
|
||||
rcube::raise_error([
|
||||
'code' => 604, 'type' => 'redis',
|
||||
'line' => __LINE__, 'file' => __FILE__,
|
||||
'message' => "Failed to connect to redis. Please check configuration"
|
||||
],
|
||||
true, true);
|
||||
}
|
||||
|
||||
// register sessions handler
|
||||
$this->register_session_handler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the session
|
||||
*
|
||||
* @param string $save_path Session save path
|
||||
* @param string $session_name Session name
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function open($save_path, $session_name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the session
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function destroy($key)
|
||||
{
|
||||
if ($key) {
|
||||
try {
|
||||
$fname = method_exists($this->redis, 'del') ? 'del' : 'delete';
|
||||
$result = $this->redis->$fname($key);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, true);
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('delete', $key, null, $result ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from redis store
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
*
|
||||
* @return string Serialized data string
|
||||
*/
|
||||
public function read($key)
|
||||
{
|
||||
$value = null;
|
||||
|
||||
try {
|
||||
$value = $this->redis->get($key);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, true);
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('get', $key, $value);
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
$arr = unserialize($value);
|
||||
$this->changed = $arr['changed'];
|
||||
$this->ip = $arr['ip'];
|
||||
$this->vars = $arr['vars'];
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
return $this->vars ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to redis store
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param string $newvars New session data string
|
||||
* @param string $oldvars Old session data string
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function update($key, $newvars, $oldvars)
|
||||
{
|
||||
$ts = microtime(true);
|
||||
|
||||
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
|
||||
$data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $newvars]);
|
||||
$result = false;
|
||||
|
||||
try {
|
||||
$result = $this->redis->setex($key, $this->lifetime + 60, $data);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, true);
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to redis store
|
||||
*
|
||||
* @param string $key Session identifier
|
||||
* @param array $vars Session data
|
||||
*
|
||||
* @return bool True on success, False on failure
|
||||
*/
|
||||
public function write($key, $vars)
|
||||
{
|
||||
if ($this->ignore_write) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = false;
|
||||
$data = null;
|
||||
|
||||
try {
|
||||
$data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $vars]);
|
||||
$result = $this->redis->setex($key, $this->lifetime + 60, $data);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
rcube::raise_error($e, true, true);
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
$this->debug('set', $key, $data, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write memcache debug info to the log
|
||||
*
|
||||
* @param string $type Operation type
|
||||
* @param string $key Session identifier
|
||||
* @param string $data Data to log
|
||||
* @param bool $result Operation result
|
||||
*/
|
||||
protected function debug($type, $key, $data = null, $result = null)
|
||||
{
|
||||
$line = strtoupper($type) . ' ' . $key;
|
||||
|
||||
if ($data !== null) {
|
||||
$line .= ' ' . $data;
|
||||
}
|
||||
|
||||
rcube::debug('redis', $line, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Spellchecking backend implementation for afterthedeadline services |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spellchecking backend implementation to work with an After the Deadline service
|
||||
* See http://www.afterthedeadline.com/ for more information
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_spellchecker_atd extends rcube_spellchecker_engine
|
||||
{
|
||||
const SERVICE_HOST = 'service.afterthedeadline.com';
|
||||
const SERVICE_PORT = 80;
|
||||
|
||||
private $matches = [];
|
||||
private $content;
|
||||
private $langhosts = [
|
||||
'fr' => 'fr.',
|
||||
'de' => 'de.',
|
||||
'pt' => 'pt.',
|
||||
'es' => 'es.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return a list of languages supported by this backend
|
||||
*
|
||||
* @see rcube_spellchecker_engine::languages()
|
||||
*/
|
||||
function languages()
|
||||
{
|
||||
$langs = array_values($this->langhosts);
|
||||
$langs[] = 'en';
|
||||
|
||||
return $langs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content and check spelling
|
||||
*
|
||||
* @see rcube_spellchecker_engine::check()
|
||||
*/
|
||||
function check($text)
|
||||
{
|
||||
$this->content = $text;
|
||||
|
||||
// spell check uri is configured
|
||||
$rcube = rcube::get_instance();
|
||||
$url = $rcube->config->get('spellcheck_uri');
|
||||
$key = $rcube->config->get('spellcheck_atd_key');
|
||||
|
||||
if ($url) {
|
||||
$a_uri = parse_url($url);
|
||||
$ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
|
||||
$port = !empty($a_uri['port']) ? $a_uri['port'] : ($ssl ? 443 : 80);
|
||||
$host = ($ssl ? 'ssl://' : '') . $a_uri['host'];
|
||||
$path = $a_uri['path'] . (!empty($a_uri['query']) ? '?'.$a_uri['query'] : '') . $this->lang;
|
||||
}
|
||||
else {
|
||||
$host = self::SERVICE_HOST;
|
||||
$port = self::SERVICE_PORT;
|
||||
$path = '/checkDocument';
|
||||
|
||||
// prefix host for other languages than 'en'
|
||||
$lang = substr($this->lang, 0, 2);
|
||||
if (!empty($this->langhosts[$lang])) {
|
||||
$host = $this->langhosts[$lang] . $host;
|
||||
}
|
||||
}
|
||||
|
||||
$postdata = 'data=' . urlencode($text);
|
||||
|
||||
if (!empty($key)) {
|
||||
$postdata .= '&key=' . urlencode($key);
|
||||
}
|
||||
|
||||
$response = $headers = '';
|
||||
$in_header = true;
|
||||
|
||||
if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
|
||||
$out = "POST $path HTTP/1.0\r\n";
|
||||
$out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
|
||||
$out .= "Content-Length: " . strlen($postdata) . "\r\n";
|
||||
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
|
||||
$out .= "Connection: Close\r\n\r\n";
|
||||
$out .= $postdata;
|
||||
fwrite($fp, $out);
|
||||
|
||||
while (!feof($fp)) {
|
||||
if ($in_header) {
|
||||
$line = fgets($fp, 512);
|
||||
$headers .= $line;
|
||||
if (trim($line) == '') {
|
||||
$in_header = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$response .= fgets($fp, 1024);
|
||||
}
|
||||
}
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
// parse HTTP response headers
|
||||
if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $headers, $m)) {
|
||||
$http_status = $m[1];
|
||||
if ($http_status != '200') {
|
||||
$this->error = 'HTTP ' . $m[1] . $m[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response) {
|
||||
$this->error = "Empty result from spelling engine";
|
||||
}
|
||||
|
||||
try {
|
||||
$result = new SimpleXMLElement($response);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->error = "Unexpected response from server: " . $response;
|
||||
return [];
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
|
||||
foreach ($result->error as $error) {
|
||||
if (strval($error->type) == 'spelling') {
|
||||
$word = strval($error->string);
|
||||
|
||||
// skip exceptions
|
||||
if ($this->dictionary->is_exception($word)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prefix = strval($error->precontext);
|
||||
$start = $prefix ? mb_strpos($text, $prefix) : 0;
|
||||
$pos = mb_strpos($text, $word, $start);
|
||||
$len = mb_strlen($word);
|
||||
$num = 0;
|
||||
|
||||
$match = [$word, $pos, $len, null, []];
|
||||
foreach ($error->suggestions->option as $option) {
|
||||
$match[4][] = strval($option);
|
||||
if (++$num == self::MAX_SUGGESTIONS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$matches[] = $match;
|
||||
}
|
||||
}
|
||||
|
||||
$this->matches = $matches;
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions for the specified word
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_words()
|
||||
*/
|
||||
function get_suggestions($word)
|
||||
{
|
||||
$matches = $word ? $this->check($word) : $this->matches;
|
||||
|
||||
if (!empty($matches[0][4])) {
|
||||
return $matches[0][4];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns misspelled words
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_suggestions()
|
||||
*/
|
||||
function get_words($text = null)
|
||||
{
|
||||
if ($text) {
|
||||
$matches = $this->check($text);
|
||||
}
|
||||
else {
|
||||
$matches = $this->matches;
|
||||
$text = $this->content;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($matches as $m) {
|
||||
$result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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: |
|
||||
| Spellchecking backend implementation to work with Enchant |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <machniak@kolabsys.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spellchecking backend implementation to work with Pspell
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_spellchecker_enchant extends rcube_spellchecker_engine
|
||||
{
|
||||
private $enchant_broker;
|
||||
private $enchant_dictionary;
|
||||
private $matches = [];
|
||||
|
||||
/**
|
||||
* Return a list of languages supported by this backend
|
||||
*
|
||||
* @see rcube_spellchecker_engine::languages()
|
||||
*/
|
||||
function languages()
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (!$this->enchant_broker) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$langs = [];
|
||||
if ($dicts = enchant_broker_list_dicts($this->enchant_broker)) {
|
||||
foreach ($dicts as $dict) {
|
||||
$langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($langs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes Enchant dictionary
|
||||
*/
|
||||
private function init()
|
||||
{
|
||||
if (!$this->enchant_broker) {
|
||||
if (!extension_loaded('enchant')) {
|
||||
$this->error = "Enchant extension not available";
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enchant_broker = enchant_broker_init();
|
||||
}
|
||||
|
||||
if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
|
||||
$this->error = "Unable to load dictionary for selected language using Enchant";
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content and check spelling
|
||||
*
|
||||
* @see rcube_spellchecker_engine::check()
|
||||
*/
|
||||
function check($text)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (!$this->enchant_dictionary) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// tokenize
|
||||
$text = preg_split($this->separator, $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
|
||||
|
||||
$diff = 0;
|
||||
$matches = [];
|
||||
|
||||
foreach ($text as $w) {
|
||||
$word = trim($w[0]);
|
||||
$pos = $w[1] - $diff;
|
||||
$len = mb_strlen($word);
|
||||
|
||||
if ($this->dictionary->is_exception($word)) {
|
||||
// skip exceptions
|
||||
}
|
||||
else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
|
||||
$suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
|
||||
|
||||
if (is_array($suggestions) && count($suggestions) > self::MAX_SUGGESTIONS) {
|
||||
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
|
||||
}
|
||||
|
||||
$matches[] = [$word, $pos, $len, null, $suggestions];
|
||||
}
|
||||
|
||||
$diff += (strlen($word) - $len);
|
||||
}
|
||||
|
||||
$this->matches = $matches;
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions for the specified word
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_words()
|
||||
*/
|
||||
function get_suggestions($word)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (!$this->enchant_dictionary) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
|
||||
|
||||
if (is_array($suggestions) && count($suggestions) > self::MAX_SUGGESTIONS) {
|
||||
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
|
||||
}
|
||||
|
||||
return is_array($suggestions) ? $suggestions : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns misspelled words
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_suggestions()
|
||||
*/
|
||||
function get_words($text = null)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if ($text) {
|
||||
// init spellchecker
|
||||
$this->init();
|
||||
|
||||
if (!$this->enchant_dictionary) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// With Enchant we don't need to get suggestions to return misspelled words
|
||||
$text = preg_split($this->separator, $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($text as $w) {
|
||||
$word = trim($w[0]);
|
||||
|
||||
// skip exceptions
|
||||
if ($this->dictionary->is_exception($word)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!enchant_dict_check($this->enchant_dictionary, $word)) {
|
||||
$result[] = $word;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($this->matches as $m) {
|
||||
$result[] = $m[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| Copyright (C) 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 class for a spell-checking backend |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface class for a spell-checking backend
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
abstract class rcube_spellchecker_engine
|
||||
{
|
||||
const MAX_SUGGESTIONS = 10;
|
||||
|
||||
protected $lang;
|
||||
protected $error;
|
||||
protected $dictionary;
|
||||
protected $options = [];
|
||||
protected $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct($dict, $lang, $options = [])
|
||||
{
|
||||
$this->dictionary = $dict;
|
||||
$this->lang = $lang;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of languages supported by this backend
|
||||
*
|
||||
* @return array Indexed list of language codes
|
||||
*/
|
||||
abstract function languages();
|
||||
|
||||
/**
|
||||
* Set content and check spelling
|
||||
*
|
||||
* @param string $text Text content for spellchecking
|
||||
*
|
||||
* @return bool True when no misspelling found, otherwise false
|
||||
*/
|
||||
abstract function check($text);
|
||||
|
||||
/**
|
||||
* Returns suggestions for the specified word
|
||||
*
|
||||
* @param string $word The word
|
||||
*
|
||||
* @return array Suggestions list
|
||||
*/
|
||||
abstract function get_suggestions($word);
|
||||
|
||||
/**
|
||||
* Returns misspelled words
|
||||
*
|
||||
* @param string $text The content for spellchecking. If empty content
|
||||
* used for check() method will be used.
|
||||
*
|
||||
* @return array List of misspelled words
|
||||
*/
|
||||
abstract function get_words($text = null);
|
||||
|
||||
/**
|
||||
* Returns error message
|
||||
*
|
||||
* @return string Error message
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Spellchecking backend implementation to work with Googiespell |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <machniak@kolabsys.com> |
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spellchecking backend implementation to work with a Googiespell service
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_spellchecker_googie extends rcube_spellchecker_engine
|
||||
{
|
||||
const GOOGIE_HOST = 'https://spell.roundcube.net';
|
||||
|
||||
private $matches = [];
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* Return a list of languages supported by this backend
|
||||
*
|
||||
* @see rcube_spellchecker_engine::languages()
|
||||
*/
|
||||
function languages()
|
||||
{
|
||||
return [
|
||||
'am','ar','ar','bg','br','ca','cs','cy','da',
|
||||
'de_CH','de_DE','el','en_GB','en_US',
|
||||
'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl',
|
||||
'he','hr','hu','hy','is','it','ku','lt','lv','nl',
|
||||
'pl','pt_BR','pt_PT','ro','ru',
|
||||
'sk','sl','sv','uk'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content and check spelling
|
||||
*
|
||||
* @see rcube_spellchecker_engine::check()
|
||||
*/
|
||||
function check($text)
|
||||
{
|
||||
$this->content = $text;
|
||||
|
||||
$matches = [];
|
||||
|
||||
if (empty($text)) {
|
||||
return $this->matches = $matches;
|
||||
}
|
||||
|
||||
$rcube = rcube::get_instance();
|
||||
$client = $rcube->get_http_client();
|
||||
|
||||
// spell check uri is configured
|
||||
$url = $rcube->config->get('spellcheck_uri');
|
||||
|
||||
if (!$url) {
|
||||
$url = self::GOOGIE_HOST . '/tbproxy/spell?lang=';
|
||||
}
|
||||
$url .= $this->lang;
|
||||
$url .= sprintf('&key=%06d', !empty($_SESSION['user_id']) ? $_SESSION['user_id'] : 0);
|
||||
|
||||
$gtext = '<?xml version="1.0" encoding="utf-8" ?>'
|
||||
.'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
|
||||
.'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>'
|
||||
.'</spellrequest>';
|
||||
|
||||
try {
|
||||
$response = $client->post($url, [
|
||||
'connect_timeout' => 5, // seconds
|
||||
'headers' => [
|
||||
'User-Agent' => "Roundcube Webmail/" . RCUBE_VERSION . " (Googiespell Wrapper)",
|
||||
'Content-type' => 'text/xml'
|
||||
],
|
||||
'body' => $gtext
|
||||
]
|
||||
);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// Do nothing, the error set below should be logged by the caller
|
||||
}
|
||||
|
||||
if (empty($response)) {
|
||||
$this->error = $e ? $e->getMessage() : "Spelling engine failure";
|
||||
}
|
||||
else if ($response->getStatusCode() != 200) {
|
||||
$this->error = 'HTTP ' . $response->getReasonPhrase();
|
||||
}
|
||||
else {
|
||||
$response_body = $response->getBody();
|
||||
if (preg_match('/<spellresult error="([^"]+)"/', $response_body, $m) && $m[1]) {
|
||||
$this->error = "Error code $m[1] returned";
|
||||
$this->error .= preg_match('/<errortext>([^<]+)/', $response_body, $m) ? ": " . html_entity_decode($m[1]) : '';
|
||||
}
|
||||
|
||||
preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $response_body, $matches, PREG_SET_ORDER);
|
||||
|
||||
// skip exceptions (if appropriate options are enabled)
|
||||
foreach ($matches as $idx => $m) {
|
||||
$word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
|
||||
// skip exceptions
|
||||
if ($this->dictionary->is_exception($word)) {
|
||||
unset($matches[$idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->matches = $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions for the specified word
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_words()
|
||||
*/
|
||||
function get_suggestions($word)
|
||||
{
|
||||
$matches = $word ? $this->check($word) : $this->matches;
|
||||
|
||||
if (!empty($matches[0][4])) {
|
||||
$suggestions = explode("\t", $matches[0][4]);
|
||||
if (count($suggestions) > self::MAX_SUGGESTIONS) {
|
||||
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns misspelled words
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_suggestions()
|
||||
*/
|
||||
function get_words($text = null)
|
||||
{
|
||||
if ($text) {
|
||||
$matches = $this->check($text);
|
||||
}
|
||||
else {
|
||||
$matches = $this->matches;
|
||||
$text = $this->content;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($matches as $m) {
|
||||
$result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Spellchecking backend implementation to work with Pspell |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Aleksander Machniak <machniak@kolabsys.com> |
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spellchecking backend implementation to work with Pspell
|
||||
*
|
||||
* @package Framework
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcube_spellchecker_pspell extends rcube_spellchecker_engine
|
||||
{
|
||||
private $plink;
|
||||
private $matches = [];
|
||||
|
||||
/**
|
||||
* Return a list of languages supported by this backend
|
||||
*
|
||||
* @see rcube_spellchecker_engine::languages()
|
||||
*/
|
||||
function languages()
|
||||
{
|
||||
$defaults = ['en'];
|
||||
$langs = [];
|
||||
|
||||
// get aspell dictionaries
|
||||
exec('aspell dump dicts', $dicts);
|
||||
if (!empty($dicts)) {
|
||||
$seen = [];
|
||||
foreach ($dicts as $lang) {
|
||||
$lang = preg_replace('/-.*$/', '', $lang);
|
||||
$langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang;
|
||||
|
||||
if (empty($seen[$langc])) {
|
||||
$langs[] = $lang;
|
||||
$seen[$langc] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$langs = array_unique($langs);
|
||||
}
|
||||
else {
|
||||
$langs = $defaults;
|
||||
}
|
||||
|
||||
return $langs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes PSpell dictionary
|
||||
*/
|
||||
private function init()
|
||||
{
|
||||
if (!$this->plink) {
|
||||
if (!extension_loaded('pspell')) {
|
||||
$this->error = "Pspell extension not available";
|
||||
return;
|
||||
}
|
||||
|
||||
$this->plink = pspell_new($this->lang, '', '', RCUBE_CHARSET, PSPELL_FAST);
|
||||
}
|
||||
|
||||
if (!$this->plink) {
|
||||
$this->error = "Unable to load Pspell engine for selected language";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content and check spelling
|
||||
*
|
||||
* @see rcube_spellchecker_engine::check()
|
||||
*/
|
||||
function check($text)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (!$this->plink) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// tokenize
|
||||
$text = preg_split($this->separator, $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
|
||||
|
||||
$diff = 0;
|
||||
$matches = [];
|
||||
|
||||
foreach ($text as $w) {
|
||||
$word = trim($w[0]);
|
||||
$pos = $w[1] - $diff;
|
||||
$len = mb_strlen($word);
|
||||
|
||||
if ($this->dictionary->is_exception($word)) {
|
||||
// skip exceptions
|
||||
}
|
||||
else if (!pspell_check($this->plink, $word)) {
|
||||
$suggestions = pspell_suggest($this->plink, $word);
|
||||
|
||||
if (count($suggestions) > self::MAX_SUGGESTIONS) {
|
||||
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
|
||||
}
|
||||
|
||||
$matches[] = [$word, $pos, $len, null, $suggestions];
|
||||
}
|
||||
|
||||
$diff += (strlen($word) - $len);
|
||||
}
|
||||
|
||||
return $this->matches = $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions for the specified word
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_words()
|
||||
*/
|
||||
function get_suggestions($word)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
if (!$this->plink) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$suggestions = pspell_suggest($this->plink, $word);
|
||||
|
||||
if (count($suggestions) > self::MAX_SUGGESTIONS) {
|
||||
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
|
||||
}
|
||||
|
||||
return $suggestions ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns misspelled words
|
||||
*
|
||||
* @see rcube_spellchecker_engine::get_suggestions()
|
||||
*/
|
||||
function get_words($text = null)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if ($text) {
|
||||
// init spellchecker
|
||||
$this->init();
|
||||
|
||||
if (!$this->plink) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// With PSpell we don't need to get suggestions to return misspelled words
|
||||
$text = preg_split($this->separator, $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($text as $w) {
|
||||
$word = trim($w[0]);
|
||||
|
||||
// skip exceptions
|
||||
if ($this->dictionary->is_exception($word)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!pspell_check($this->plink, $word)) {
|
||||
$result[] = $word;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($this->matches as $m) {
|
||||
$result[] = $m[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user